From a74de9c2b8b3f7c0cc2fb21e6f0befd3d2c0ffca Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Wed, 7 Oct 2026 13:58:57 +0800 Subject: [PATCH 01/11] Add pixelrag config and e2e test fixture - pixelrag.yaml: local-source config to build the papers knowledge base index - e2e_test/: sample PDF and matching config for end-to-end testing --- e2e_test/pixelrag.yaml | 9 +++++++++ e2e_test/test.pdf | Bin 0 -> 375486 bytes pixelrag.yaml | 9 +++++++++ 3 files changed, 18 insertions(+) create mode 100644 e2e_test/pixelrag.yaml create mode 100644 e2e_test/test.pdf create mode 100644 pixelrag.yaml diff --git a/e2e_test/pixelrag.yaml b/e2e_test/pixelrag.yaml new file mode 100644 index 0000000..323e146 --- /dev/null +++ b/e2e_test/pixelrag.yaml @@ -0,0 +1,9 @@ +source: + type: local + path: D:/ccproject/PixelRAG/e2e_test/test.pdf + +embed: + model: Qwen/Qwen3-VL-Embedding-2B + device: cpu + +output: D:/ccproject/PixelRAG/e2e_test/index diff --git a/e2e_test/test.pdf b/e2e_test/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..697055c5a6b6222e1241d369f6a062323e8cb39c GIT binary patch literal 375486 zcmd41WmF~2wk?ReySv-Q-QC?O+$r4M-QB%#E8L}U3N73n3N75Vq0Tw?e)qlZ(eFq9 zXg0YsB4bI!+L3FloH@xAMaAiv7&u_a`K#cJc#66J_d=H7??R3 zm|$^(_ND+4LuW&4J9B`dp}DCO5%b5WGC<1K%Yo;<`9J<(Ol?g*jlg`$|I^xED*zEY7h7i{W`K;PiIWc7r$=QXCQg>mv+h5XKQH`S zS=`dv+0+prZf)pnDr#zMXJQJFHMKQ&wjg372P zqJ5~Z6O%v{UWeVqY%z^-rO<9@ZyZj`h=6C+Tn`olK?9PnJOF)xe%qKBuK*E@OdQ_0 z3?-Q((@Yz$>da0GxsT%Ajlz2$?3q8?Il`^PQ1eRwJXN%9wT)r2yD{u~62m6-`(@0o z(Q*4ZxLx`ZhfFjkvFbQvIP7A4p-R$fW2{ir+TN+Ky?nmgxgGSn;uxWA_nU=|Wy4y4qeBYESPG3ng{g)Nd?mHQ_Y`~CWYX!G#RRpqo{LYPkl$f{W^2>i)fBiMYjD=!HLeNcPW7an z2dsE1Z!CdCtfY}H*5#e6gn#)Oos&6T6Gok&d3pXOi|jt;0Aij zvrHRxM?3Gy8*f7!JPl20-;KoD?0$Sm6pM17B7M~h=9=Hy)(QD=HUr*Qubxd=hYMbr z0@~%M=Ew52r@YCCKcXeXpzi`vuL}b491bImHc8n(>(gc(CPP%&8yEnSP?oU6InNow z@?nVO9sUnPvkc7Tg)*GVFA86HG>e{P=+ppBTH4^4`d{ zbgiMU2gzygjfG>>AHKUkFVORHK335A=lEFH&kPm*S8yKpPl=zm#$FgZ1G#qI4V5bU zptYA;#<4Z48*(hUHpgbND8pJ1og%n$T0>e7^sSCm&F|Mh)GsY9TcUeB_KB%Hcj=F} zhHX2@bT6XdfYpVb^-`x&U{v2g@2aN!t+9eL^r(~_Y`0{H<#`(cXHzy&(8=n6GFfu_ ze3ReNT2%Hkcz8iSdesCO*vJjwFQ6FLn!#lEf9)l#v`r7kn z_g=X6eEIfv?Csf9yV3yhwpNN(aseEVoH4jaXl$2q5Sy4%d!Qb&&y7{Tju--u<5VQr z5!_%*Zh4r=-oK^W%8lP=kq!?Pfp`83sAnpPMYnA)>1e1w(b@h|_vxsMPfy2yM5Kww z&86F%>(axKOgxJD@5p&7cYG0w}A%_`6%%x?3M_-QZ3eH;4Yv zRl)R=Yhk*a_d_VF1S~Il^;F}==}3y)VY3I9I6!D(`vvzsCgnlAEKZD_w1TwCOYKMs zUs>zkt1VaWYWiQVUe)oznOMQG+lRJ8{z(#r`|_j+A2z8E!N*+oNLfl95O0qy&tmtfg_U`mafn0bVvU}sztxyr&x zo|K|3I3yWr!-Wj9j*LVSi>a9?F!&6D@cWc4mriYMe1GIFII1Bt4pvg%H;Bv|BM%DH zz*BkJgwcIdMcn?{Y`S0W<3aXBk$hV!ats+est`uE#v>`p#7ETTUm!7oH&-7GXKO$M zb}%f+esAu1w7Om3MF{OP!k+SM_T%vJ&r%-}-#_ukIKs`c`9Np{j#PG(8TTG6B$n-k zcQCE_-f-GomDYw5m2#R>z^=`qlTK1f1U{OSb)DO5C!_Y$Xoc?e#Z%x0Jjdz;9JY$f z3JsRef=DWbAeb+EjBpTN)2tw}IAA7%@=cq716#rhatH5)A+>Mq_~PdFZ&YojrS!;d zZnu+jIf=||EdwL|6!ia)L{j>Q0kr~KI9(PA;SJ6IKuoU!aq3#RN5fW zmdW9uB7cDXI-X=yv(g_+#M+91%0OQZ(uuzjELpQU)-4k5*3D4+G~(?*<= za~TOM63lQ?4PgxFPs*)jt7*ZUe;i>$wd%Mb6u_~q(}@wOjp%a6*5gM+67+JUh}Nd;pO zV=0Z%i60FYtnb-?x_vFCrvsrv?J=s;bttYa4{J7TRUr3exzZL)7Fz9XedY(>5L>Nn zsrF9RQA8Atq(RqahV=z&pk#J9VB)^0oIdfu*h2Qc7FGDLFbtQ%h=rNQItC=C!vE{Ce(dYzim>f==Iym@htFU7oqWeR~G-ib*dg#lMY+K`c3KypoQW zDqk!YhOZ0QTYK?85JhuM4MV^UXj@EZMY54-p;?0vqZXZtKu*t)4}y8kJj~le-#3>H@ z1Iqq{`{75p4U>vOM}K@fG-b$gK-gUf__b1b@pJH;szZY$C|fe? z(aL6n5RENBc*XdC1*4Mi@8#FSp&}4dgk+cEvqvCUZsAo%XbT0Sn)D+Dg6Z4Na^fm6 zConN7(_&p4YObetL<)L>4b>H4EX4q28*mJV}LR z62||hve1ESx?$arXD)uM&k({2&OKiXIEtP!8mj!&DskWIcf+&wo`Um{V-fQlB=~Z= zs_)}fwFDLxO+dZqjIED~jjO3d=%6`~G_HJrH=R!*UbXPF%}Tppw0`e?rsREqJ719+ z&;wDYMF%teS8>B($i)CoW3hCbn?EO&R0SCx&S>yfyg-LERNjSjcYzXHK2>SoSaP5h z5fZJi*-_*XHr75EM^k3N38C zC#X9ij~c32ZRAQPE+w;HUMgMW2JH7uZvDY39*J%uZSaAvC6LKVNLQg}7K5%qJAx>q z5n-c(8=RHgJoJR6*Bn5L?gUlwGL-t`nCT?Uva2$noxslrOc85e39}H=ev36k%)eTQ zNG=-%*BzC`z4O#aYDRucW{-Nrh%sh3+Z6T;UuY-xt8_yNRK_Ud_Z;F8)%7vnQuTRV z=ofMI;~U;g7j8Rh5YEA%7oP5EQ2nVn-81puMBy5>>AQ9Y|tw5XAOMe1y zj*>|gW!f9K@F2U!RS`${aZ|+-LiSsBCK-FzajpLFkezf7ATFy~ToS2BL$%V<$k+P< z88Gyp?uZ@^<5l@<5O~WR1!CApx)v*gva1g;Q||8BC5mxrL$T--i|mHTeY2%2flmJ3 zQmeBDiedw=Oz8&6I_A&7lXw1kNvu$MS}QugnohPWrpF}SRqfQS^;19jJ zKHDO)ybh$aTm!#w(R_I6bH6tvmh=IKCocFLEkAZteioC2r%(hNol@-P7bc63%^ zUHIP|D6(9Du1o>d-yT|Bt=h@FHFxo&P6 z%oWk4I-*K|L1>YEr0dGv<{PWta+Uat#yC7slm1>pPLU2i`skH=<+sxpafuAc-5JR- zZy>0x6GH!0Hnd-Jzu@s1;mjCEmO!F<{un#KJ%FmFl!3lF!dV(4`}+NmU1Ep6+15e! zNq5f>1y++Ut@V7%;g z>-j>I2a65Hdtp)n8w@Nu-h&hAL`sZ`6iG<`cK^83)5^}gi0yV|A#oj54*COf^O}qM zo)Ahb7boI%Prv@I8Ua7>m&0ymgde1}tRB;F4A9UGb5L1Bf7pQStN{#Qj~>NMJ43f;1TyZe95q z%*nIV2TXlFI@->_^~nujs0enGsO*{D9-c|ywVfy?7Mt|f9?_vj`-J6Pm-j9vhUmby z7mMK?%xSQSmia4-Z9*vi@gq9~>#`p`588@FJsdJY;aqOz&e%Mc2PyW%`6~2jiCc}k z#JD%Fh7vw_l>OCOA=V^vuxa=Y85Ro*Dpb>JMkW}cVT)KhWiC4SuG=KZ;ye3r=pHM* zzB|oIA%gbAOoLhHtO2%YOkC)^jrNRFKk$gE`Wwd}ARehafvXHVO%meQek@7wIR{fy z#BWqoLWX}Wq+$O;(dY)+lobQ5f8eHgnRMQ4Q}m6OwPDvD-thQdKNxoP;m$weoG=&@ zh0!wW7oH!6oOGeLi4tmE3$#tZ1qQ^#+-c2q@~)~y>yEfPxOACD`&>}$BvMJtcl`nU zot%BNr*NB(r}x`+kVx!bo9p0cu;8k^d=G2S9a~H8y~c5yn}ZtR{@Q)zSf(I!Z?_NH zQ*%S98IBlRCz0kANW8hT*Y^R+O6n@x@`c^-WMJb?P-!)!+_}yAF(<$@8nq-{XBRC~ zHgp$*(vwrRUuyiS`j;nA$RP=QHv5O)!IioNG(Hiee`7lT!ID`28xB(DU?pPvgo^;m z9PC6KpRIr3MxP^}xRDM(N<>81(8<(zzj_AVKx9n$%3R z_gT`^oMz&uH1oF`3jpF|{W&O*#M8l9WJrG%ZnF5uIU?+pie7m1J5 z?~HTbZGnKHKyl*190j4vfa&uQqXV#OL7IOcw}Jq+5Ep<6!h!1i5tW4~mLMF3_?I9N z0v#2>t%N`=Ak2k$TES`pvhoqvKuNctk^H4DaK-}^43Gnf=tM)y;>pMm%LOyz`3C~& zh;&AT!Phu4eN)thsXv!50Tl%c_3SZ&<}dvLe>Q82|YmeM?&T<^tT|U3l3@} z0hYw-lV~RLkc3taVkOOy1UM2R6E%!rqCtp>F)3;1G*q2y55>a#+-m z-4KQ`jEO#-VI0W}ge7j!h`cUS)2WJ<9V;t%VldTE!RWhDSY6W66eo#1L1|n=Kls*; z{cA#C5_ODC?1#{&>3*p!9)ou655Fek@{KGRphCRd4zlfnjx!Bu9E;d zBDL4G2XW}o}S3*nL&TEJxaMH`Ji&oz==FbnltICl7BcyWYUxGo0jFXVQb9GSQR%$cbfi6i|% z)4}DPf}M`RtiiBBOtK>KDl%d6D>7{|ak6e{W-=S{Z{+tf%`*H+!cvx!Z8F8u_zCUA zY_xJo>`A|+4y5u@eCa^PEJoZ%tP{B?W&tb!TFS2gT!0RRm#(XNEQ2{YCbAA+#Nnwjovr*Iz3wa=l47F^z zX1R`~yq4aw7?(<$)MFitUPv)Yfum%xhN)0W`Eg~tN|UZjCUx%~GfTSMc&U{Mn@kf} z6HXK4F_ufJXXBIMtrt#hXn}aJc*sybMU`a{cDaY9lLfeE$j``DbT-SQ$48}IXg$^|I~kwPHCmxnTh^1ye%_uyjVUdCj`m_?_=1Vz)v3~REc;#T^*jDgu0 z*(91e{@jZyPq0ip(wc2C(>9wni!qz6Os^Eyn6JrX)L=zq|IUu7$w(JS7po0LhfWtn z|CLT(lUgf8b6!iXZhl$mH0hLMp?$&oMEwLjmfD=9bPS@duH|O~c5U&J=W6OIe~U^Z zY|GdBtOkz8O_R-ie^-0QrE|uil>Mt#_%$vG78s=R#s1n76z+2I`!OrE}oo(4v9ANhw|-uYkJvwuW?&(X*k9&IXgu<7m3x02Z>n< zeiw)ps2Kh>6f>ON{=E&^XHG&#DotWW%9XI6a9)U4SW$>ksQc~YTbhx#v9^&zolqU- z4wb{S10U`_dx;HRf9e@saaDHJ&+0oJ z1#434s0~*R9gYsF=Uvy$KRI0WTslq^u1n_@qv)gi2eE%xSGAg8n`j*uf1IUsjJdv# z!2aw=`ktkq_+IU=VQ(I{kvqdDaR?lAOI8;9PA$a&0 z=9myUdlkpo@t;4E?&X)Wk1ZCgYHSNy_VOS~lN5TmQJu|hs%DqZoT{8A4%8+!nw52~ zSMr;0n%nNeU4_E@hJS$%!>;F;&fev-jc_9Gm8Yby(_`p&qTAn5 z4Jr<56EqZre=mFHf>4dq&TP%){il24H4Cv7@kv}g4lc$sjvo&e50w|rgY%W@_jJcF zbsT1%wNIpP=DqBm)=#I512(_1_h}MFlrID=*yn!Rk2^oX>~?PRBK&-_#yzBbwV25T z<{7I6d@P4-e{WQ>eW%bL?$OJhc$K;NKFVQbH`&wVxhinL!SNXTZhPd@3;7fAmJpub zb*f`R;2rJR(t-HAVebG{UsK<+=gBumTlO{dRsBi#)$0JUlu#kfC@u930 z+Eg=@WrBw}ielE`tsu0K;jCiuV!)VRQb@jvp}|H4f?zlY(SeM%JD@8GY1waL=iK89 zbiQYJP1(IU{oZhzdb9^73J)^v9rH3$ZvV0RGZq^QZ5(WqDmpK<@AAeObaTD~8K`=| zt9^6ou?7k`!SZFlwYAgnkvh;BDF4)SVO&~4Yx~=nEMnYypSgx+?Uya)P0j5?KaW$H z$sW3=#NCq#U0-wgD8P=U2b*o8vO@hC+IM}p!=xb>lUL>wcyFoD7a7F5uq{*DDXipZ)k zYCM0!@od(VCRK{-A=o}13@C`6HG?d4UVx#MkK?E5C$7jFG8Z0I8IP6!aWi>BNjHn zc=Yefx1ges#x#v^6n`vuC{vwNS7oYzaRHeRI2KzMiTNg4Hf6!AO;8=wEO`AZ;LySu zqa8>$)>gFT+xLR7>7+w9EV$4BL&I^#$#f!FJTsOOEGMkl(6(so0a=6JwVG9M>X2VU zZhwRt&enl1NuNrclAj{EqC68w3{Y=J+sC#Vd_iGA)QJ5$q&on=U1AUG?$F7-0dgJ? z_rtC)Z438e{ekB}>IFn7@CS(|vNyCtFn%zMFzq&uVf00)Bv}R0M1CGK3}Ii1Uc~i1-LTLw12^H8JSt(MK&taiw;pa^=O8nVyTKKqqXS zrP-tzwiLfa3zJ(Yzpcow=t#An%#zHLtmV+z8p^4`E9+h9-t@qAsdUK&of%vZ941;Q zS|{2tAQxdtlUgCn2(iMpLWVD5KJ9VC>52Sm{sg2; zCY8dPdXqMuT9w*KL~TL@2M-aK@Po=Q+bo)qUz%Fdp@^jTPO?cFUTQ*SOlnnHLK;)L zMG8^IFWGREb7WxbH0>zagCr_(KJ_+nB9V{8OYU!xMLnEGo6JLhFG-NPn0lYON#`fq zt?w%@q|_@#ELNbrUC~!XA~LPcBwr_cS-2C8BKK39TD@HMC~*e1ge<9F)WV)eJm@(5iI^~1INsIn?z&c0K8cm2|3A?K0T)X$Xdab={b~ZWo^(S#H5u*g-2&0t#t$wa4y2ZLVTd!Xm zwXMW(A~+>dEnO`qv8pY_P*w6*T`>!5xd!Yo%JlWs7p<& z6HfH4I<0Acki96Mg`a^TD}wSwQu$0jYzH4a`Uzwv?-`D0xA8m8x}vtabo8GwoI$P)FI0S&(_TFS=5;54I;Pw?nwIxhFtqK&XgA1UHAchT_g6 z$xg*yMvH}eO1Or_+cIgm-s_S?OhF7zOnRbq(y+L>M$|!U*dG#;9Ak((65I*pcc;Y5e{i$t7bALic_|26DITxL`=AVD$R~1f)PQy1Zb#jIDEH_qjfO$Hu(XUz#6iyvi z=9P)miKwvFZQ45SAm_-j@i(MkHtX3pyA zjWfmJco0y~6P@kKx9$|`Q*vANw4CXx?PUl#S641FhdoPu3UiFZd*E18x?11ys?nt% zm#4w6?;~_2{8OxN@Fhwn&5>b3SH6wci~MO+bZRJfSOfI?#`*0re(P_~KSM6VCmPS3 zK54d^>zsbGJHOj>PIYoxZgUif!0_LsvY z=I$(^;`~9K`lrx8h+fJZTtrO&qIM~BG7&NTMauxnAB-&KzvQfsCL0m+C;s*UxN&k2 zF@H|sv&luo@|U#*Q08JHV)={1eKc8!SpTiY#YV*XSM8sU|5r|!xSg#tK*hz#`IBz; zIbSBgCusNgvYLdfhPF1Xyrqqii<7dQjl3O~EWMnmi6ubH_JajxX=_fz^sgC+TRJ*9 zi&z*se(=gZRYgsmj2$iQo$VZnnEo|$S;PMbd=#Y`mL|>?ALKK(&$a%4@!|Z41j|2; zPn*najDN3voE6B7}~htROL zl>BUe%5i;!{ZsC5|37jc{m=FO(+(5kr_!h3za9L??dSRLbUzeWKa71Uex5(4_MhvY zOZwaY82fC0hTv1~KjZ(U^LL;8_t>YezcL@bKW+Zg=l@CjKf3=Xvj2aQ`H0a!>5YFk z?*F(=|GlOEZvX%6?~k2oD`e|r`ColeOEWW5M^js4Qzso}fSDx`3m3q_#qNV1X=!fZ z{68FghW4Kh{^xxG5OOj${orA8FtL2FJnbb-KlT6B`?!r%Or5C!LI7ca2tX7d1`r2G z03-oY0BL{>Ko%eekOwFL6ah*AWq=Am6`%%C2WS8^0a^fUfFZ!p#@^J?$XcrJV`D;Uf~JPR^De4*!P4 z#K`)uGr-xx(bN>+>}CgW0k{I(0PX+} zfG5Dy)X|O_@R?UWqejon$o|PkHFqLn{hM%vh3(vR=-D{_ju3Hui!-2c&A|c zk*ELl#^UowkTpD6{P;%sW8{*l!< z{+U((^U~vkm;1lJ!uX%*RpdX9=NxRDoF6v+i#wa+?(s!svHdPq@=l#vZAR5zQu)MR z1i!+i$|V=vp^TGJ1Oz%>5QRvVc)Z^T0ZB3$2s(c}$-iu#d%nP>Z1>37>g)VMZgc4$ zuJe*8CqFlvGQNY3%~zjIzI#AIlcrbK`E%#gb!U`B41w6CJ8wWKFEf>Rg9Q3fxpv(( zJfdZI_VX>CD|1{9b7anOEhOYC1>ud0iI_Z^qobhwx04cCuN`K0o4&2WwB_%^aoH+A zzHZ%KYu>xvyHDlvxNShFsdj1;;FP4$Y1bKrkWuMfJx7{+Qzb=IlQv5vG%7}}D3dH# z=FzX$ph>alFG9)S6{cCP9Xr`#pQ1;(r_Vi?Zk9|@b}gceUO4tj*|IUypov$SmRSF0rPX6EcBNL3 z{$4jGn@&cRxt>K79j{;PdN`9^v1U%PQ8_2|Sw3DBg=A4w3~VUrJn>uM+|@SifW_YE zFF#+OTv2*>c-1gbNw)q2N3BP5??k@{Zblj!65j_qwf>H`hwd+x3F0HaiMu z<9DQ8rCX1OMBg@H-{}_LYy8o~{(LFrt<`g}7nv)Fna-NaV$*5VF>W5Tje}h)fL*~R zJWW8_pi5SvBhyq+T?HK zBPijPKh7Cip*H!i$kG=zD@K)7&t=WATQN1|XbbCC4Nv2@S}WuG zR>Rr($KIEfkK619Gj=0({iEdj)|h_x?VM!{8{Wxjypt)%m?N%>L!R%9l(J5xa`ukcN!lbq*?ajBgctoPDKdwplsY9#$>Clh}Y)NTi5+O+5c* z&e(K!&A`XvuRDj0_)e~}>XW#sd#S39sGYz+ZJ<0y&%# z=7~5O9&M)!$5&rmiw4Hz0w@M#cJ^fk3*KbKLdzi%+lA4fSV_s!!vmYtT~p&L%e5>iEm#ox%vu#XecxNBzlgd(hQPzXVaLo82xXw{SK#Y$!EyJ%7bb_@9YQViIfA)EJx0jN?Xf&n<@aD~}e|Jag0#crMav+%aMnaN%fYq%iy%XKAC$_YVm`c`TP; z7bjfi(W?i^BAD09dzt7vXKQ~N+Cz+^dU^zZML0^Pj+8}T33Y9zo}!r|-xLyHM)JlN z5119S>ul7R!&NS|(CJ?iA{xf+t5GO!itv(7+RiS zoN^2%bDNF}QC_|MCW(To6Im3e3rpNRWPcu!62?rz+feo;3F5^n$;_y97w@}$Aqj~w zMR@$EAUxd1Qo*%Fu(P(3mxY(u7veNO&UT7=#G~!P^3^4MZD@!dO5)`Hr-;Cp^E{}8 z9&TGVhJX|t+i4`=S*cA35v0@~VA)hdX%B(aR!^R0E+|*~wJnYEn&xaS>mCYqZEAD1 zJyv|$p}z;a(9bCE`Ks1qu>^cfd)zr4*kkb3yQd~CTrJ6eX^kfI%0Jk~B6$>*+9(); zmChS9O?R7GnwDi5=21*n-V+?glyQfzT~xxBB-G3LFk-rc+N7QlT!u`SGrLsbF%cjW zqNK~)cKcL!5Wm#A7d2&STHxDO7o&ooopON1i`+j1j=q1Z?o5I|k+kfaJ3Vmb2Vs23R49CuN+(2#Ecb>;Je(Y@Z>T+O#OqiV zbM0n15i7GzpkF_}0>@mYS9G*rs@}dgQ$JL?V4MIi#(9~Ac2Zllm?}(`c1{UczgZsI zz%bSlHwr1^9?e#dOOJgTVyH7P?CBgl$Y-^xj2p=SH!**|l#tXrE&U?7_!tKTn|5G|D3NY8)<>8LQO+d z?T?8y8BEKbx~m85_iYIzJ~w~!iwGpMfnOO?8Cuj-sKpp$7_>BvVoc#$trNz}%>?Y?=G#++7BTc7tpgise;G33wud~lWxi95Sqq*s z&mMw~rdRWTR(he6yu;STA$=5!T@VNtGg31fK81RgIWO;<8$v~0<~v73&)DwGlP5+< zzYgU?j=B>ZLM5mP`|W%>d5B#7Wr{-Weps<>URN9R{CQ3f(4YF;qbR(VmD`-Grzi}9Lm0|H;_Yk@+Z^G|6jg7fA(Wo6lO`?oHZEPn zMXea2ju%XVka=S)j74Am!tY(s$n9Q(d6#J;MwqHc?^6Bk6&7mG6C10q8|O*3nHn6N zu{8(dh8C?3Vty~5?8M8+J+h0{S)rXNM&-AW=Oy+zV6s_j@(T%(Gb!Cv%k__h0kGJq zkV6CSs}(rMq`W^jnMS(k+&p+w%Q$0Q!j30-^EWwFnPfAz*^`>f^y&n-Q?~LtCM6CVxWlO5Ao2q<4^Pv$qqpu{KEg z^#Okw{YWxO`KCU+B}sV3SY@xAkE>+K%b|rCM+v(=8Pg3T@#_hjvxcj$-fc5V&Iv}R zxD#JB#W(KCQ8|~@U(qx_*^P~nmrE9Y-D-(tz_0%Bv8bwyK}6kJ(Z6IH=H{m}FxTXc z9(por*=$%mHKMFKf+OKYFG2A&B>fJnqT=M}cvT;sYnH4vpeXk4F~M~jjlYPeiEE;A z@655HUCB(VLRZ*$g_roHq-1)A0)@9jm2jV0Phr=gAZ*9v*k7{m8*uc2|K@EJWwrTa z6b-`MvHUl^Ig_JPLoflu^QgYE@K!W)AyYe_=jhjSYc$U_Jo>tToa%pKNAi=(6lwCp3%pV`x%vRipURi`k4j5%`Yn%PRp zN&)La2T!Q2s*75*nUU+(4>s^}(FS_N6=N)Bg=ERenfWkNAtvs8 zjw%IXFg>d~{xm5(_XHo4eJz7*(8^Z;d{$O!$wuIPn=MdW>phaNIESh!O#`OpmF|17 z7Oc|yDQQ5FmO|UmxdJB^evO!|<ZnN2EeaE&)N;x`~1q1EmPQGcd&KdTU3EF|=F05tX?1*HPdio4g^0Marr)n_U zmI@hFl?W}}@35>Hl!@}#MpvUHS3A0N+!J15xX^CR@TDJC?w%nc$8VK0a7ACCHm3H6 zp*{IC$F?GL%pFB39LsW=tEY>H75G!SAzuVZ`h**oJI3siITgrrEplwD=tNJJ<+PKc z+O$O)zbPA}8Xr?RdApU7vKBb}pojtg8@KQt|10{FTDZ~` zaYD|%rhw=ywtJ>A-{zZc7dV-Rroi4=g&BB%ejAd?aBP>Lfr$2I&W})gPZZvHSOxW! zsPCzIY$)>g^m7`Cz~Nm%{cJZeRrPR2qZbI=TP8(=X=UqW=qS7*bQZlT1g-C$HG__3 zg2~qbNTwweMTfhMbr6NS&+~--K*8{7fF@SUA~n2&K^>wY=HV>M}&7m%2pwVq;E8`P((M9u2vq^jb5!)0FU| z*qI+tCg!gZPLKV`<#=5wWRV~UBf}8|i6uQ)o9Vl`y2zg~KnGvO1t{Tz-DujqtDhy2 zP*dA>Gu(e4I2pc}muBVpPz{-BlG1^dep@e9BCU{T6-PUPQ3Z!gCwoE!ofd~1*!*eo zLmq#PWI(y_>5LN$5>-VYs#WU>@Y8}3Y-^bj^EvVwO&H4szeltg%yUaAeW(!<8~cT5 z2PYwg$!G^Ayd!G`GAzrT5Y_mC={Xfipis%5c#tEC)gvipLp9z=n2_0ix3ZgjOE!;V zu^GR5MDpk4&kQCIOfD< zH{u33tM-O^K@CD-h10V)wU^v(8cMop4~ap?JL5*u!_Ke4Uk8{>)X59w#*u_7!}upD zSs;sl#3F|0pLiU7fkj4>^Rp4!&1>~%{X)K^2{#g5Zyw#n^ORw_kVw5qrT zb+D5Fmr3A&;l}aP9fRuTIF(f?3!?bb4S>Yde7s#IB=F46CSIA;N7qy@y|Dy92P|Q)j2EFJ~!coTJH@<~d#h z4OvlqeQ+Y|gD~78@Z8*P>_1D?>0nTMnKwwFy2I3W(KvvIeZB7(S)o;B61bTtTix@^{t?gj4(CvI1gB`BO;h+U~{aj20R)XUu{eO^cV8{e-3UR;7ui%id)o&Y%sp>f5d z_v@H%&68#oBIADdeaPBn98Lhd$9@3bv#CQzSC~GhZyuz-LTx?hf-D*@-c1YaZbCO% z6Uwu2>B+v8m+7}x&7fl3TNc3%{eB)j|KED*YA7p=aGAH|<&|yhzE_Z!y#Ww7ftM1o zNF$hdSRYJmfGD}x@4m!DN0~Z&CIudll0T9#&~fMzlp`z)d&t9Uth|qFVc-meKR}pS zQFA>Zh_fBz;cgs2Fj<-3Op2ayBsGuNxvMyfVO&t$>LOk{7>VfUetj34UR+$6S){dt z-BOlrcUMlDJf4wI&Vk;gfJtrORwoZ0k>n=j=Hm-k@)R+QPC({udp^G~BeiRp2z%LB_W9+jO%r)6X)Dz_LB+&91gOIuqZo zLFqqu+fCbY+`NB_HDk(kaWi#`#?Nu=@GQJGdxy@c(w)OGQ_^m6ngzbIGkRyoqf;bX zu_T>VcbtB*d^nF;vg(U}PHKe7>2B<7t!*s&qH0TBA-F$&Nu|>&Ta9qrj&}1{?F3(n zD>gqdB=4L-X7p!^!q7jPtWaxub9G^56?cd{aQ1$?GzDeSIqe4)v9>v5=b+_$evY29 z5tRfY)`EEd*s{4#+?K;QlP|*qH-gX9RBw0e@PmZG+Hw+OcR05A5s~rkI>m*_SlbCr z<4fe)Rc9Zsj=UJerLtxDB?PE1c+bwL?T-#t=dbc=Ac+XZ9Awmb2`tK7c{5cgtUrMA zY8rgPDDjojO7fjKCeO;QMi9DU*z>FUSW!0+*=hvdwEb>yuqz-;+h>D#dCmc~SZDWK zaNM)z_84H|^xhhbn-EJ%d9R26avdFy}tj7%HY#JGoPx9!|Sc9wB z6nww{imwe{5p*vF&|Uf77}~>Z7w=qe+!h1`r^Jzd4tfTC9VDtjzqfd>;YxKU!$put zMWTG?!xQs0VmmeqRkO3lad_%z`{T&=zDwr>0V5H?MGXzC=`Y|mxX0iAW{t|+QNZma zW-i23G0x#M6%sdplM!9-{LD{<>v^{F0}}t)e09EJv|&wUvkMcDPZ($4KVt2Q6R6*b zVX9u#d5G9QaxDn4NC;6Qz!^$ar>c?45;#T2CLtXsd&d0L4cT#%G7 zF}U`7h17)7hnJO+k|)+8kX!D9OsomyF|tMHhp9#hS_R_E6FC+5yQ}9 zOA#4u#{|rCb?4^Z7l!7_$Mb`96(z8l0}4~}-RXxm+LA2?E2n1cls4k5wyN*7{<2Tx zUl{mRXZhL|8e!yCzi9Ac==aLgrI`L?bL5H37@c%{a}t!x8=+I+afIp%^|iOz3!=n@ z{~B5tVF^Nb7dCpNgKq`=k!y%y&!ooXy6-~0FJx5j&yU%5aO@~$@a&_Y9*sCut&wYsan)n!!F9X8j zV+;lQ@tDJF2=6SDeXz@2eX!o?cP-R-L33_t;u_OcNFmN* z+9b%NF*75ZT_{gz>c~vL`^S@{x<=U@s_1*sypxf=J-RBNmiDFMcsH}QyYIGcd!xkxFd`VK5sZ2Pk3%fRmW4f$X19h+0YDxEYk64MTu^C@+I4{7b z?0;2ijzUsrJCIQp>l?BN{xBYX`5EATnIvqnb07F4A$%C`CGDKqlleJuouG2^kI-s* z*V~&{lTyiUZ=wJ1kx`3*VDZZSJAD{0I9h}8xQi745C32%MO`CFp@kTeImeQ5Z93%l z4X~ap1bYHaVn&Wh?_dI^mOuzsd{rt0e142+{V&m1GNK`m;AKEe=tFa8mYQ4Kc1-Mp zFFdP17jNJ%2w(lHI>j|5x$Sa6E3ww725hsJifyR42!%EqfBYy?$B`{ z#6Q!pIvlv~;n@2{wh40739HpB5{pLuFWTM$ERJSv{|p}7-3jjQPOt=b5AN>n7Tkin zySoJq?ykYz-C0OZ-Zweld(M9Q-`#7j8LGRhtE+ov=)0c!J@=#~oNl6Mk9Ci56LC|m z6mH~-SYA7sIE-A1hR>+At)FbLu!yTJv>b!cz_UII))&uQb2hd?RoB6)8D6O#MwiOp zL??JK>-)B;T0jc#$Cm`9J8%h%aB+Vcn{-R)3|ofC$9>B3QiMGhNkw62-1Wn&SgwBF zJ^O^)`s@G<2s%ymMwqf45v5_h!0}*7zHPslrqmb?WW|Z%`?rZNP>k*3TB+PGY&*-l zk8vr!Xt0NRO28qzH%9mHKDGR&3STVvF{S0q9m8-*a-5aKU2lwW=$at?TF2qOb{&+H0ZoM>dqUwb~*c5jjg27i7|e3C^67Dw8d zb2*^T@NZ3)PV|c*W!0!~0ZN60nE7g#av(4h@3J5<)FolUwSAM+(ZmvbQZ0Ub^xlGY zf=3xP>)1`sW78%w-FX4*kZzlpC1d!*g7indobF|v0*6orqW45-jJ@JPlT$9o$sST# zMmuIZHeAkwa`UQPvxZ*oq;HzQkJra)R)@Us$!qX21PK}h)p1A5n!|=O!;J55^+A)r zA|E}I)}mh0noTEeO|&%Cfp#0gA`E*9zY|||n^?W~D8m=o^dt&u9PKy3+07SNEblbs zst+ARN^)`KW}O(f-qO{z;Z-2+A3)mN*vwSxLK`0tQ!gPoN<;Q3xcWXPQOaIF+0OwU z24m@D*)%GU5d;mc(%6O?E}#^MGXI^kW^2QLdF87kUFb!sy`7t>$Hvq~-rHOPIQhrP z8$R*0k|!VY(vS7yY1JLW_Hn`AQ9AZW zBj5SMiP^zD*xEuR$JpPt2ELh&hQl~n=qREJWlG@+Pfhi;daJ=FITa4ebJBu1%5_hl zl}Xcou!r$=9Y_8gKGnON)G60kG5IUIv*Jl$(%KeZ+B%Ibwj^lYXFwEPUB(B$PZna~ zDC_5p5X?IQpOkI^wKtp4?IBudhiFKk=*mvV$^^}}{QKt{gHXEOx^tO-6zv}v=o^4H zK(~e_^#d;p-#subuTx(f_ZKetj)_Z-2m(fOyU{92{4F?MY!09RG)>Ifm6@$|R_b%3 z$R}idm36HONXmLPX*f%Gi^sAEc2b1N8ywhUD(Y1!qPM*yd+M|e9r3ww{XjF$Hlp54 zZ6|kl3jmD`*Vm?lyv%9I#Uu?$C*PSU#-vm1PQ)u+Q+X}naWS8)dQX$I5>2pdO*ZCM z#Q94s*m?2sR}Kx6JVyOb+~_RQH=$teVna5}@myi5+EHD%BD~Z{8`%gE3Cb>}jHE&J zv@22Q!0h7M)uB!q2TNLQ_o}CWMlck(FjYT2$Gc7=E@H1R_Z)S{Z=;FFA!9&7LSpnE z=6ngPQ0%%14De-Cfsb2}P$TCU7NVgPp^dgmY~kbRQJ&+aa8$je(&3{o&FSs~C$y8G zk)HGb-1IrQ{s$cB2d(q(v;2Qf`2Nk)NQnx|DT`1Dh|4Oo{D#o{BzOKsXlQ>-TmO#G z{J_0w{z7Qz=;>(WEac6NUagq$=wB%dnO{r|E88pK^ZLs4f(`xO0T@4P|8p%mI)>Nd z{{R1AW_-N@|KB{A=o$WiFkV~!{q6Y+lS^v?=-|+n(#Mpl#MOgkIMP8PEks@-x9rcgi{p)Cd_weV*{Cz5a^zV1R zg6@T@_;=n=C`(9BBsT(XuYoPX^ zR$aew!F<<_y+!OW1n1fCH=Wi;fHWOW~_7&VGrNHOh054@(%sW7b!D zS1X}}_RU(mpVLbV?R0pU+H?zj2@$OpNK)INf?<{r#t{OwJb;m=+&rYGwL9P>yph?~ z$u5SvgzzH8y*|g)u4u9Ci%CBa5c{|f&ht~8bC{3PB2d^nUCLpiPhb>iQ!~z>T8hTB zyS|F9F3a;RJoa#`-UGyf1^5cv;;2cx%}OOpPYBdNUf-O(BlBsF6zwR$?&=X!-D1_@ zX-?V@)nK}&r*2iIR9bWEjMJ1|Gb1aw)GIcm?_)`s`t##c@x|tYZb-34z6BL8l(ox` z?nNK&5kv`jlIk7FMeN4=jJ3CSb{|a_i1Fwrr7V#L34%C2+MDG}{~3zLt3oQOh)qKz zTU0|1HbY>2`)w$yc%7B6o&7F5H=|j8Z93C7AWg%W>WC;P+8yJhNXxiXILj9ohq3zH zh=w8ogP0V5h%`y+_rp~shS-4=Yu?GhX_%L^()P*K!X2WxG-~}CL&=DkXDpVVl};3Q zcM~Em9goU&8Nl&xfmY)s-{u`OSS3Vq@`ijnG<~03FJmEk=46c@Cn*n{y7f(G!V7+7 zg-hytNzqm{M>oV6y&N79xSq(SJF-+svQG|gAx_G$AfSGp(YxiFzX}W{4 zZoEzc+IzOIYp$d6%y2nqXuEt&o@=(2yK~QC^k4G*up51VJ6j~Fso@iE{8k0lA2mpXB@?J;^Uj7j!VL|TWPs0TI53Gvgq`<7(Za@^h45(Y%z zoJ`L2TYaU-No|Gey0O$;Z;WUEl%pMJBGF;KNYVg^qG&|Ml7}2>=XYaoeM8`)2x$s~`1m`cyCQEdnvE|o4VbA9@1b=v5uuOdZsDOOZvP%2CmY_hOF4_k-X z(lYN3-1!800>sb$4=UluE&5-#p?~;q_AfI#QyX-lMT4~Xd#bvDem6?`H& zxwWrj^`OJM;n4u)p6Y<5@%09~QaiG-_CA1Z<`YCpuC!Luhf=h|q3Y&Mk#zb4TaUhg zXtb5HHXzH93)};{mKLG?1SFCDi;SpuEh;G#dA7ojZ&-^BC;#cv{6<3mb@TgA0!mp# zPE<+ZML@~?t)Kn}qWQOwV*DRU>V2ajLXz43&}58E^uG&#l(-ZmHDB9f3TXrzwQ4!Ey2J@qx3JN_T@eOO;`MA z`agdY{{Z3tA~osQ7-@gk6(upjva*UeJr?y5E`sk}7YnCaSV}p(Eu`};9Mqe_x_cCm zaBbx>yWp4ziQ7rP2;=cb;R1M_fC2#%1;P~|s|VS2OH)$ICy(9btM5-!2TLr~%zqCB zb70YGWOxL!A5LMo9$dWV_%s!=!Q{U7=b?RFV0xx}}qbu={fTgg%to*Nn;=nSzk|{aZsRO(M1Y zC8GuQ1JQc>8~%iTD&{9tR|Cw(_C-kkot1$(qM$zZr^#$WSyr`bC21vNWO0&7WDJE+ zqLoU18W|k%uKSty7U(-dOqjE}jbh@L?KRD%97pr`ctjON2XemCMP?FBCIR9+borPW5V3l^`9f8#xTW3!eMO-@?qV++ zMXS`FUz{-D)FP7zh9)-(ng$3{CpS;+FLp{uSC#%y?YXr*7z|+-GwOXwZe%qZQKSwD$agpcjOv>DF&7in?kS1b_uiW^ca?1 zNv0)ixo*&&!@`w3mLZ)mnB35i*_Li9zk{1_@9vj%u>$_Bs7P z`kPr}Qg&+JHAQX|3wByGbnvjIk6ALV0}Yp<$!V zBCLtqf$5r~Dg-{YD;t7@mrSjZbeht(vjIGJ`a1iB;vC25i++cGxQ50!)MjM)UU2AT zV|sj~Rk?pUYPt(I@(v%vd`I7Yfr))}O8BIJ9TzyW!;8fEc-&2ID9x;cpCLC{4Gx|dO1noc-!cz+F1El_mo zU27<=5vDWe_z7m2^7@c9Q`&nGh;zfk&;nLtHI>knS`{O%Tzml2qG#$62c>-rJCUmR zup!3R0F*3&CU}T@Pw#8CJIrt!Rj>|exL64IJc!y*^IdLL-AIKIg(S@5b1O8m%PJkW z$I@}+Mf10V?P|jwGzp@b?09Z57Cl^bm{!K9nsLR)lF%h=htj1xKu{$tx?33Zj~yuQ zhvw!}W!J0~U5p1yRBvSKET-+=+^>ySc1!Oo9Lhp$koM8*c_A|*b~UsHkDHht31E=0 z5S&*IP&qvyF!78z0;XABmv!}t%=U9(+KW;bQY3}O!}<) z5bO%l1sP+XIe6IE4-3;|s8&ChE#0}d8LY>-%P|l5{&->yT&>sXE!8*r8mXA5NJt~% z)CW6Se38%C!Dz_3lv_0)b_MUDn|g%L{4X=7BxMFD{VP#c;j6@w`=LQ2jP-kWziO=9 zhq7*c1`cp3cl7+mrj2^-*M@vq*uVk)05X5MX^)GjH&1e%DkY1A=Xo@3B-&| z&x6uy(3))^$98t0N95`4r#@W?o99~*CWZeuP zQF-VRG*N=fr?!zL0@YrD#_E|xM|vlWP;2usSDS<gfcI2+B=Mp>&v}0dQyt&*&#u| z1W)|adMsMd@+YD&58J$SJRaPz3O)3Qq32AEP4T;evScrV*W68%ZDgL7lDSXgr=US!6sr-;fMQ3#>_RU5j{|oEAwmZ^s0lvm05l z;B1^EUKsEg0XFzAjaysp+G&cci?ZzMyW4Bvr&|z-gS8X^>`~lgOa?)xo^v}1eww&p zu|RVePt04LDNZq204a{L;-j%;s`L?jibWZwh#61h#7gi0l>i8c$c1N%>gRX;N3{#^|HCt;}~ zDd)*p`k&c)zX?j_-$Qdh19!jD-F{SNX8mtT zZww5tfsgtwFaJw=mH2B4)oTdv z*Fpc4JZ1DVBkCp6_mUFzk_TmI^pfxNSIUx>-S3k3rOp2lJENj|k-IOMh%Yg!mz=+U z)4Q)1_&2@#$EWdsCwN)teiymFMLqvvxcSc_m!6UNwT-{hk<4P9EHO9Hzvc8wPp{-h zD6|7s6~zGNUJ`m4iM%EBA|yo8v7XLm3)1VUK*o*I>#Nk;Z@pGus?$}CG7}Ap#?$G^ zo)xWMBc{b__rmfUjvzxKg%Zwi-7j`2ULWTlI2gq7IA|TYzfZlsjU6D1?^(5Ocz{Mm z{;I;};y~gC8AqKU{9<2&?jY)AJP;b zP45)IucBCn6gRy{)Oz1~uMqqBSh@9q`&2-@2Er&bTUiZXdU}RgH0D4c%Q2u>NTuii zE{o>85vi?hud%b77TiXyPgoR-fR z&DkNHa~%z#RiRZW=O)9>66gkG+tLzj+jPSv$$rV0^@OPPNCxm9ePr95KHo1mu?!`x zJVrURuK3(;-$%r+@^ix{3fDot_pj9^-9D5)3v5fQBIVk|R?Cl|Y5g=B4 zVNd*y=zAZHMNBBHtK8<6RRcGm)@qu=Lr7d35YX^Vp`Ns+H?xE_MQ$IFJNIMYKogdL zF^bP_+GW#lg&jEyfdXQyF%Ig_o(lIc+U5@`fdnURapJ|b_7 z>B2y7HcY^}w+k%LK#I%cIZ;P7Ye_A6@Yo{)C z3snvovAj8Cmoj=6k?q_*Cy<>?O@h`A&g_C3t4G;U)eDc7xu}E3ulGQ6k5i!D;)-s= z&_2na=o?CioZYtn8F5?6p1W|{Be`r=!MO)QA0I#cyAJtzzHONKKvOeh2(Muz+Ylva z3d%ebUsGIf8@wb+SjWWGJIl(QTFu!d%n5G|qJY4eG%X%XC+;hLN6Ji`c@I!pLf*Ra z9`$_7%RX`BF9-?AqshzgX5NpZ^W$WSA35qgFPkQbT_m59659*jm2`omSe}jd-DtH5 zg)v){Dl9Emhe{nRw-}&mVscRXaZ>YaYWa~oQ#>3ran1|VpvvI%gJ}#Jz{ZCl82MnZ z5W{k-!l0ca>eE&E-p-ADTC&}|aBO7}j|daek5tcE5{*J8%$@P>-(w00d`El0)^E?Q zk9+rBVrk{f9uZ>$Wu9?m((dSconk9)IgnjK!8X`fbF1{)oP$KSjc3bT)3l+DiYP~s z6_Ck-pHnq=-{TD+Y$PN8L)3RnKU*;{jLZ<8kPl>K1CTh<;mo-Twni$H^@8vzj#0|t zOHYSE*Ki)I7J5($eg!bLnI$Cq67QBNM6gn7N>Xi&2kQ?i)H&=cYo~@3k64f5iEj&lj@M(!A$Xxgl6#;IC9re2jGw5d?; znmXVekH9^d?!>dCG%Fnpy#J;=x*H~yCsYupd}>YHH7}H-P?I#s1TD`qsF%`yR2>1s zZ)pd%VHC4HzP-cYd*wL+jZ{xsNORgPM@Rm&v$-zYN{gI<_Zfsz!$dV0NUk0W=d>4~ z%HAso*e#imKz9-xbjMI`SB!xo{&bBeoyA+YpiK{sNIL>T#qHbHrM)3BqZBO02l1N5 z`R(NyfBck_!7%AF4xO$*ed}(0d&-$N(-~-X7z(Bd&;lqnh@ZSP0LGhal?6z2Z{7f9q*+7lLt2HN zr!gSB4dy4V?a1Rg0q-wn24hglrcw5y#}1nM*mK{40^tKR2W`;qw8octC^`nN5T>Mw zGZ#V71HA&EKLaC-`E(;aCnc0EVN=Uuy{CKY`>5qS#aI?kkU+#xHpqwI23b%y0xe1a z?k0y|P1}8S-5KF}1h?43L9i;UCP&L;n;JkZK7vYoVU*C^kfV%fHUOh^dICkyd8a3lw|>L;d``Zma86`F|LxQJq%tyZ$%rk$aB2f&yM386{RXjR zbm`4Xp9H?-g6I1d{-NA{GgUq?=eYLh&%k&1VV8BnQ=wF6{`2tN2@LtP;lKhnU^|5z z2N3*f7i-IrR8VEf_|;Y91Th>(Jz&IXE=~yLUg^2*E0a;o3f+p|urTgvV+E!V)h?Dd z8Pd*1O(yjC`!~Y?{hSW@TAaVk6|@ay$NDmhVfyp}D-ELLO&N69D$0NhqeLS$(#kt1e!7x!J`mZr+`J+w zyguq6Vgkb~Vv1kcgNDuBv4~RZO3>&|vY&A42&CBvAVp#Dor}_(T4xAEpHU4(VV+87 zFom4kv6v+bQ|4R&q(p(~S#wV76BrcBXuk!#z?V(nhs^*{lZ#u=X!M1%#~5IVTR4OP zYXVmVMEP{k;xKwiQx|s3F*`p#J34bF=Rp8~M4kTJTQ95VK-dts3+7a9(K z#E;5oXzH8-oksT1^PwmV#1Pd^NPN}Yf^e@JWMV&|5J`naga-Y6is(_2#Te%D+L-?? zQ%63Isd~1!PEQ>J_F$*}7V2QQSA#O};KNsQAY6Ksf^Mvbc(-64aL&gesx>=_)HIZvPRQOd>;dKo{dvq8=yk7=TXo_Qk=) zBE9Wg$u#~QJL)^&N7+D}?SLLl#+i8BuE9$0R8pv|guPU$-Pe_^;LTrew4D^1~BKo+T8HdUwvWxNy0vxA*HxuWCt3 z!v~zxDCd6a{iiVcZ*$21Ao+gHFtGd)$5fA`Gw-B@5_Cx6XQ4Htum?g@87C45&O8Ls zO@QHikAh^db9gUGg3ayyS@nok>?VWtso!sn3+I?R@xnwSQ{+uVoqHM?{uU^>m1L(ZnPen%# zJ*+%J`#bWJLfc*k@{8NVH$$I0(k5M-pZC|U0`DGWcITmBkN(lgQjnx6P!?eSvo@#h%scdwG)%YWAT zpG&W1AwS(wUibQ8Bl2Pg!o+{F)Uq-1v>b-1Z{a5*Q`B%NyeP7EzJXij;-(MapFUzm%f1dr%*H?>@*U}H`lz-Wr zysrDhLgi)oSD#(@R0)8ogU zucj_9qr%sEKipvcQ{|uQ|66naeb4{*TVK2TtChdYKi~3S>;I{LFYA6ErT_R6{Qjl- zKL`^1a4#`^8A{V=Tff+cyjUjD=xAA6nEz?Ug2%@E(}(0Wbnr(P{%Okbe+CMeSy_LZ zCi}7UD^T!%=**&P^kN-jV`TFeqC=x)`{L4~ZLOuF_rodWN9m_c%4^X?&-Sm?FXdkw zUQU@t+wR3%#rDW>Fbm#ENzLSx~*(dbhOkr4O82s&ZBM3H_g)g{oXy8F9cVZ77gkuHE^fXN(R>I0_T)`#t+B4oONO3 zcsvZsd5iT9x`|5UqR6<*PxPD^K{FFOp0(9_tAp$6tAvxAZjZ@@fviR=Nzzywnr_J8 z`PuCA@WU-6b1+fRY+$)%!o;|IkpdgG0_)>?P;@Xa20XEKq7P3Oq_4JUH7}v+MztiK zWt1OeGXFbYz`P-sC0B=oxh+PE1ss|@Gr&kZW|5r@6bB_V_=61$cuF`?kYiTAAe|In zny;)N!Giekm2WFV8heehCdqn5$>D*8R|0^~G`+g)wY;L=!(n3Me_0!mfS1WDpT&z5 zgIgm+_OrsT-8?u_z7hekArU|_Zcy_cH4>UMb>86iJ8JSf>oEzJF3OG+C4oU=&Oo*-?9wX-!s(AFJf5@P%K-(mM)*QU@Iw#w*|O74Fi0?P zyF;ntEPNTWusX8|stf{{lFC0@&!R+dBrI?nCq;&0iRNx7L>T33XFi*aER0g%g`Rrr za1!YWT#AuXc9;vtup5_01t5SV7cl}jVhROE?qIggMR+;q9u!D8^8RQxR_NR6V|`fj z0AHW=bU_F`R|J536hD%GeBd^mt@{X~5l{XaU{OOlQxX0^ySKy(+P;fiXaSE)KWYtD zjpRh-!oV~{0&SsghQDNk43dr!KU9$PYddo`- zayO-noZS}2slyqJ73`?_p-tsXnwr^^uDq9Hmn6kHm`k_Svw%iHz?%Jj$=Z};fszC_ zFP6|NbTS_koY|Ohco&I72IiTc`wa^P#{#=bv2Yt5ZTb#8v(U`P5B9MswDKrZax$h` zW?ver96+(Y9$?M)s`_!SbAtI4V3R-GC>yzz??bI1<04Q7%V0^&0g&?8X+h#%h4ma2 zF`^_o0a4b%MWH^%m0ZH8_4cfeGmQ>lpoOM&j*lE_!ng(cvi-qT)0Mc;LVyPTK zfvQNhBdFp85%HpyPb%iO$Z`Z@g#AYR1-+k-4plA&rB-*_n?NdAP+6)dnKM?p@Cl~{ zJnx+(SoLS%0lP*bquIV~s*OrhJMem8J`03lpHtLl6-7JO!Ja*UyB-|SVeTe)@2f4s zPDg#t0l2h$@i%?EU$XkIDbA@_-zWeWteTHdHGu@b!2m;HDq%gc(%O#mK7OO?eGE#& zor#2usc!$C1E`c@)_`krla25Ji)Qq9(S?RV!rs?xQQu%>`O`|*vLs?V0fGQ)!I><1{ zN&^%{nBg%R?M2i-RBtlBOsJ`i-d|X2 zchN1s__iXyfq&ED?6|oH#7*FEXw3^^VkHyiR3WDh!$m!Q1%1UNskOGgYl@7OGh+?7 zmjx+slJqGp{9BfC?^A!J!M4N{^AnD#iVzN-trg*isMy6)(pZr(lC5HlFqF|ec( z9y}B5!Y6aaltPHbecL~QDQiO?#K#5#p!VJ!a1bM!K^|J3KK98nkzBMyI!G2qlL56| zNkPEfoeP$|(vn-gpg&Hgrr7p|*{0g7dpIiE0g9MumR?~`b+S-=))|#;NmRmLl1`ly5w~F?O?Rp!_B9tg1KcYEesYm2YNRIrIl`;1BT?-j`uLjZwl12q)OUz zM`{yN4H(?EzS2HE9p_r$d)Y)$;ct{r@s|$B$9bzFA+^tKypt*=R}L-mE-D&V*%ed} z|MGbH(Q+zu_W#7Qe*;2oB#w?5(xrU4k?vjqv zC*f^#sE?}G_9yVCk@aHokb4bWL(o13Nd&R-3!0)!0?*@}hF+BOyA4Z$G!K405J<&O z7f}~cNH*53P*|Cx_qvlfefrhb)Vl3lBN(k{Xlu|=>V;oHhduF~9F(H##blZS3iWf^ zX!WrM@D%gcyPIYO-LA95WcK@qm! zB1|-J8q=Fjr=*kA&L(M+&xayDfm61Xceiqb$rNV}VgX5}nTkyB1E&T&AWo28MH9C0 zrLhBL8|ZE5N*(NzjFCKeNj|%j7BC< zE7EurZ*rjmiRhZ|ydkgO`rRGdG3qW zB=oR1qI9@GExvGCbW~ZdWFOdVn<2eJN5M&$z*wX6dZ<4L&p1ka;PoqV3&z!~t#Eqpm(ezLv3VJgZX_*A$D7%W?i1 zo&JTr%x;l5q+b1a;!%`>6)+STVr*~U&|2}ySDfaOF< zNnV86i(dTP%fK)e zNIOG*EWqxr*R~X5m^oV8ES2oLA+z!=?5fEpygW65S^zX!lc_vG{aJck*;sH~`cfW$ zaE}@vNa1N~nJOQ-s2CuOQc}MJku8Hwz<24+5Yaw2S$f$gq8mMZO&dlfaK72R09=NMk#jdMHB{n%e z)<|UL{JUUipzp2!IO7Cb3M1xyRfP{sQD0;ASvDY%ZiZMuGLnt0kA-r4Xk(FXX}_VJ zSh5;46ds#u90?1#%@x!I!x?(S+{_59M{^>*^Hdkm^!r5ER-Z#f39p^+?r zaHS0AI|WCNL?o4Mz{*-dd+s@zZf8WFd!@d;QTGBrzrTPU&ZimXI2>s16t|bfF|VY- zzs~Km?TO8LA`C(w68db*jCk9poR*zf%}nSN2V6C2Z2;shVPZ1eZ+rqII6|a#v?Hxg z25AAvIL45Oc5y!%nbkXWi-ExOp;QGu^1z1!H5{a9$01(-f-50RLrIuFcXTrArB*=!L4N{QS3G9`iws`DA^2K>)d z*yg@yB09l42Vl7)u+|k%emJX@@-C3# z4(gT~z^G5(xnMtY<2b)TwG6_ow$lJc?=XL!ZC(80_i%lPr=P80FY7)836S_^ zkx>=#ilm2JN3Zha#0SCx?hTBBFUv7*$OC{0+DYcB(|75@CXf6*#&iwq;RY(&e6Q1Y zFzZCB@-6;-J!~Is6!i)un=;`!qu#v{Zw_^Tjaf;aX9 zCPh^zncXeweoJ94KwHdNk!MYC?PD!+UMUxsaD4_Op5O3=!|?@5ZVWjaWIuV6Oukpv zFgk7puLj_@M=h#z(st(W$4b%v@(WH-pk#uL-wj2B%v+vbIL(NxN;{j{h%nqw7L3yZ zJs-jJTlTktdx7l~YC~B35e!yc6j)nOgzUGOXfe#&y6!Kb?QIDU+jcdCi8ZO>HA5&Y zBalSTEVIprxddkCM&hVG zv`Li?sm4B9M+atghTUAH1IdS8URyhZilmRhVB<;D8Iu6Z&KKU$&|1^J2;h9X0_Ax_Q_k`$TmPpzZCHIoA%5 z5pX@g>BaMw64vCD8{?A8v&_>1z0XD0O6O0s14@X!YS{-gD7pqfjhXt#fkND=2 zCq^msLWi(B;cV_iJ*LL6Lvs)m$O=2iW8YVh6qQ31!X?Z$AA`QhK{jx!oMdQ6a5tzG***UfV0Ojr*m|ve00NtwpEjsg6*fmf4qPbtR z#w?HHDK~M0hfk83Y;pTSC@V5J^L|wAJcOVDvBO3V(wu@1VbYb? zDb{B(Vid(b42+^83{2fAg5p5QLXNd!#=X$;`R}d}Q#`Yz7u--cc_AxM6Z$b6bSN(|+&VGd11tXB03Qu%$sU^2!{7HzqDrFpBmpHf4n zd^-~4DQk;}in1pU0_DvT1;f8*@9l~1TKR4mz8yma!2LWg^ez1GX`o`s1Egi=jf8Pnl32Qxtmfdz0KGv{RnVKdMybz0 zcNpN)c|F8LXtsMuOi}BarxQM}!5P_T=%9ob{sV=(X zd$pCVmuHZ-ooxeY%LW?pfrtdNnzI2SXt1~qD}!iXM)v4VrX6^6bYS)XwYN{bgU$LO%GN-ZzN2P?@9p}bjt+C2d0u8x~skcWFuZ0KDC z7)NQDD^YlJLvleDgudKmhn+E4piQy0iE-uE;U@u!D69FSLh+(oQKgphT+u>_X@Pliz7 z&66(!rUO4hhTX&>D!@FlJM>zIuQKTig&SctYmuS~-+Ij#LTE&W*;6zkfxGaeX~u-I z8nWzR47nc&&g?%h(b{W4Bb##Ta#wVEq#U34})QkSEh~Sp1CWpzd_Si zd540P#3DhP%I#lT%fkWrh|&`ytC@r|gG_vPF5NoBJk=^@s#=_IF|2^>V-8Upjn|ak zYgk-zL}LLq$t%waN+3peO+@50`$1Bln5$B5a|Hq9yf^o0I*5-qr6hv&TpA6WQ$}X- z@`kt~A*3&Ccv{ZWoSIXtLs5Xb*yj4P)%6d${e!E-iWrpXXp?fNDV4YFRQfN*M(NvS!NefzQ@K%wX~haJCuk{8giUdK^)3jp})Xe=DOiistzksm;Oi?@=A zg!1sF(e+CHn(M^#{&DZ#!sRF&5@u6SFh4Vf`bz-}*LVABHL%I`DjbR#P)e1Cknmap zhuqjyz9)eDhvKVG;Zsi@xVO2c7+YVCuhBkL$Dg9c0eUuW6YO&s1z&JDSUw~^<)G&I{TKo7T1Q9i)T4}n-0 z{3u6fP<~bII+%P~|Nm zTb0o@FG?f(-tZ#G!YD@6W2amF76_!-bD+-!Wq!c-*~zHJXzu$60AcglcI=BDtesAB zpoTfNZ>1wxQK5y7>c*VIfB=wm5isXhViE1)L*g`syJIHKP7zRC4VZ&jTgP+d%zQ@0 zL0f|I13~4@CK;TBW4xLdF|1)PvaUaDt&-&flJ!i_JCiE8IVTb~etB=-F^ zmh267sgjFIgsHtDCoLVX+bs(y2?{?fCs+*j_iv+D0rYl^##@MdMhI~BJImmE`d+_K z+XL$SfR}Yh5r5x+eMnac5KWNMZosG$9q>DZRf0iqjm;SC@QJZ>tu&~iQ;!qXGbhnPp zsjfgz`zGjdABHE*EXm)#y^rD+7i3yUi@*bqV4j09vzdT}Wh8!tf`TcLmrNe<6Fz*u zZigExAnwEF!Kq8nrZ^_``RqFzO}aevJi$sbkBX7L7)GB_v@;(1Kjv<&HXzwd*+cuT^AFAOq?q+QCtv1e&yCY*Mu@<_SPx^P+Pcof3xnZi?ru8XjkPl-Jb&PbS z*k2z$p2ykKzT_PSmBn7$;$U+E@fzt3L=*sBcvFi96tg!kmCQp*1=k(e!%&Btl#e>oqg{AaXNZW+ZC?UwF)LNo;>Z;b zxjfDK@hVU~tCP8D;TyW`k>C9d)6hFe*0sxu(<4pPKtq{NIq!=pakC}~N}-P6kO?Z|ZHH(+Iq?q4-D=xG#?!0Ca>8mx>0z zdPt=f4$kUQYEt)ov~BF}of+p?luyDbcj>ZU_NZF?3C z%V+t34!iqg^m{PhC%!G!h|C2RmE%tsmr>R#7?fnmNpwuKcS9u}5*m-!_@Y*jAHFdS zF;Ii-SxCvGI8_h?6#5YHkRKIa8N(Tn0UB)8Hv7eb9JEHRrjSx2XEHFSd3I;Ep0$K~ zC?gD*>GuC3?j55f`_jDaw9Tq?rENPaZQHggZ96M%+qP{Rm9}j^S^w^ysqUF~o_^-b z`{As!BV*&7y)t60h`8?S2irgXl(0%KBcVu~Ktx!i0QpNp(37*0lwKNwFfI^9VPq7a z1IeroROMz^F$7Ys^jU?zQ+^|_)+F=}E=V647H}9`K28lmyg%NoWCZGla<6S*p{)%_ zUpAYrvOVOxNfVnmD~%_eX8sNRKwR)+{%Iz`D}$z5Tae~Znys2%HN1(w zq9ePdq(e)IYM=kZ)a&^So&-OAqx|Xi$>}|^pf>db98S>jeM^S$W&=0$^tBRBa7ljJ z>~ZvCFK|=7F3&NqD&cC;&G8Z>WkR((RD#m_Ad-?KRk8qW{h*%o^x{DG3p4>)K`D3z ztVPFh4<%-6X#H8uG?wO4F$C`c|7Kxt$SPU&gndLUk`gl zvjdstl)~@W8a&P5KOJ7Khw`hUAHb_OOYxlq+%HpJr+GAu%#ENEO|i1pAHDjAfsOZV z0iKz5*qXXNet{|uHByzM2!^lxq5GxcU3j3K{#J)HgqQL}=Z(dAqzp&#r4&FUq|a>0 zDd{oEaRp2^d&9?b22oJA`$#fAmjJ1OHhCem#RZ?~4NJX_LjSz;Z((A+c{4OZE`KMb=<}3Wz__90# z^z&Px#Vrwv(sDQbEgX1<5D;tkt;-v%Z0tVeV*}S(+IphNytAcC{#WX%-FQFz5+Nm`_>t|r7S4RxiQ4B~QHFMcnz-hmQDi6-7tyAo z_|Px5$tBr&q=i+18~W6fmZQTdg;(Kd3s$AP$lXX#?M63vt{zFcFOy$!Bp>RJOzIu{ zgT_y*qk+-nUOgyoetkJ`7RAO33E}U_4B@XGd-lo$QgNR9xwJ@U*5OlNZJ|$Z96vW>haE6Q;%ffH7B zitQ8&VIY-IFY?M2oo4g;IUni4j*W7&7A-#1SPpu982kW{Y$!k+l%5&)uHR+HS{&hQvZ_ zdjT7PLS+0mAo5?Z?w`!$Z|3a(3q+Qekl+^*ppX|*Q20%h{(&F=Er{`tYF4^`t!8ET zTfysp@W*uY|H&W!RSf$N(DR?qOn*H0yZz_!yXT*e|8MPo^#6Yw_2;br{>cAa>(Bf7 zJ=ecC$G_L>&ujaa_CHan|C$&0zW`(D{!VxPGYI=ntNd$??myQv|D#BjnT_dB_LZLT zKZ|7lEBpH2WfCg?E~EXsChu>Q>p!vx|4JeJR|fSzlL(D}7=A{j{`fPI@OKd5Z^hnU zX@vjFAi}?qz<(oX|L?Jb{}wR%yBPd0VDvMI@bmTf_XNVvDqs3fQv6fX^SOurgp2-W z_x~vJ{nyv&{{V?HeeVC?V+ZLO{t(;z)j<|2B9|M2s;X)`RN+`A z&yT)f#8)1$d}!A^+%e{FqOPNv3_o}`ET$Tbcl`J#cYEX?dPLtmTZ0ax;}tKL@~&r~ zbjOcN02xL%G&B{Nbvo+w>YL2C-p6@7z9aPQCXNs%NS8P_|M)Qt7k%M`0wQ z)IqMvB!9nOHeb6qrpn>8)^-EX4J)npM3s0PnFi4e8}_Rjunwk_d}S z7xNj?RvQu&!q%swM@{FSB+U681&Kf8sV}$ByiYrLF=Q|pwC}qf{uM}daFPHF0U9z5 zvNV!-@Nl1v9u%E#l2|xl0irZwC4w~aY>08NY+q@ggdQ4Qicatg1fVTXwD2zSvH;)} zLN^j)H)4A*pn$a=-gHn?Dk;e`A`Q|5!UIxsA4(dlFaEu;d9e%@0hbpkMqPZg)p?Jt@w#?xHN7i)2&dpP<9 zv>X>46s(RPb!{)#$JgKoi+5hNUW35*zD%GSoF<+{7yRE649`|=qVR)^l zfz^s+#0fneke!kZ$kQrZmHGU55O$I*>GHKrrWSz-Rvy*i+ygD#mjGbUfDdUDN{F!J zzg?ALs2HV=8rq_15U0Mrt4M62A74QME2A?FnC151-URRjQ$u=9oinAo&59<*oOttD zlxg?{#%FDnPFH;zh~!A}b z`DnQqd*b(DNvsy>@JBL5xlSqiHnBw_9@rKVOpZ6893NYlzmb(8jwT+uSl##?Azw$9 z{Gru~^h7J1|M9KWzHl?8CJt#BW*(`tAleV(2f>nw>6HUh`yKZqfIH*G7$NR>-iE2O z@y!AJ>szwEbd87z6qA^$(WG_-8K?{hr~-<>Nv>fl7zz`2EqXeT+Z2V*RlH(*^tO>S8w zj!!$l8Hq0L#qvfsZe3(az;dpqU_0Q?>u3dl(>pkwLLSe9*ThT*Dc9xk<0AA8=U3B) z1o!FY-SGF-pOSUObsX{awHzE=oNw7WnkzF)dc5W$w@r>%7~oTQ-Z<=$THcYErYujh z^RrdPa%ZJlXO!j4wkbU>6i|Scc+;8{{Wh$R(H*WpZ~=40y_FL`s_$U|^?{}=?@i+T zyt0v6s*1!E@fp^JSW6_jx5%829kjQGtAKvJDhT%T4 zZI%x59L8Q?J3o}R6BMTKTDBi4(UhTV#q4HW1R}0NxJ?>kWXHsa+&89jAi zyAtUl5f$3*Z6x<5XxU5gci@?B|7)(OBhzCOV_@HTM zo_wW!WU*pD^TEQUgwXc74GHZKQVyc>5#`Ie53Kcg3S;V{k?wzS8N|zxj7iL4PkY`C z&dr$@uR5x*ehoVIQM{DyRw@z2EHaqZ%oE5)4gpe2b6rd%WePP|FOx0Jr|-n?rjvv# z?3V>V-_j23x!dw>NXiY+renu)XKro(u^S!iioJvd1 zo&rc^tdnYcRl9Wym{r?lY_ij+raq#idpQP7p~+}xNC<~!o*R(`9IY4V+AHNJkbC`} zD|w8kSIk?8UPtf(xyCinzA?-~AkeKf;Z4A_4jr0K5}MuEWKUIxPdg+=?Iy|+G&gfC z#X~Mic)@iN#4bC?%E*J+mAkt`{Tb94zg>47g7UcN z!4XqqfksRa@n~@iV)?5K?$A17*#Vx4Wz%0HTb5F*fHTFq+l$LA*Q&O1PwhJ!L4?Fk zl^BKfB8-LfGO2F?7f}ax52E#M-k5;7n=#Jg1FO$8QCDM486k1KQep%MW%)83 z60r(Qx(s{h7`x9FtSq^agT92+ZX&PQ5DsjR%*28a44Kud;lt_kKI4FjEcWSByh9yE zPF}QjJr$!BYmzIJnyUcU*NHSW$h5iOLMz(9pi2HyyWE9hW8A{5Q9O?m*$u4W5Vl62 zO7R*d2BSBe^2x4jWND07*O*3$)! zrxhGsI-nIk^jE6<9x;5 zaD@iDbEz#s2VqLmfO5MVAwHPmwTU>0p_V(YOwP2=X;YiHpiG_>OFg-CRdykec=yF< z5&aze#k+SA@a9a{Z3!uxicsYj=dAWFb;MUpJ`w=3i)d!nZyVFczf8>Y`3>-QMTrfq zICUPPn{2Cj=Lnm2cE8(0_)F@_7j<3-U!6SZpcZKsR?3viluuD>1HkX$k|4jsph7+c zBxMKmL{WFIeb1D?>_u)CZplVb)&v?Xe1XVmXhMbGX*D(*uq~oVQ42hGy`CwFn-&9q;ogTJ$B5B17QcZW{W zp5X3LXRD?vw=jhg#`24HdYP3K`6mjeIGtNzXP#Ew_WaV8{)Vtq%aUXS+)6=e%`sT# zP7QPSQPE^IXTeb_Cdy>p^(4U-=B9d#s*{I}@9SpWb+R6XF1HKheIRf&)nHU~K31(1 z)=VF*CaVkIlq50my7Tw*csq7EFtc1R?Y)(!Qt5K;`u(_NT^nE$%j`0##M%8OAiEpj zEfZmsDIEGx)uQ}gF=P#A{NrZzLQA#_XdgI+DF+2@(fSD1$FfL0m>Ms^G9>xv z;~kQfW0t>OZD))13^>Pzmk!0Iw?YtVl<1u=a&cq}?`{dP zB*7I7LV$$d)6s%(-4P8C?t|T|WHhQ!Fsgttvo1wiA4%({&IJbp(WzhoX3$(9l^)1_ zHv)fIHhD>#mM&%_~bl0gN3gib1cG z)5*M@i<9nne%>wQAwqI+U_sVD5*ux;X^HF^&|h9lhLRCFHT}5+x&uCBGYoG)kg1}` zYr=RfuOqt1x&XTH6!s#l71@U7u|_oE1es2RodwLw(i~Ia>^ZwU5E*NH#wR?5QKL~h z{Bd!fq$Ky+#|oH!3!Wn@nH8AIp8zPB?w(vntIe8kRAN%1YQC^we#AbrTDDwdEr%6@ z6@wjvGoE-Mt1=3}6;#%3PkmP!Bb5lsYHcn}&}OX^ihIS~bdC@;$@MVyD679vp{@u= zZW67&JXT;gq_Vm&k)EG|Vih7_8kM~KL@K1&@p=-eOfA@}OJkB_+pAcsBC15;u(t{X zHJiZy>(}l>{pR%Df*Tk7YWGgz<8V)V!ySzMH5BMOhZLo2D7mQ`wtC2$QD-4|E;s2( z?6;~QIErGaDv}OE=<=*2`fTa7E>F>coO6TkTc~znGoR5Auu8h1J`9GGNkU?VDzq<( zkZ|ojo@fBz4HR(Cwa5n(wNlqv&9y^!VC_*r7-1MiN20Zb<6k;YSHA+kOv-p3k)mLa z>$6k8`8ocw(S&j^n4s~*!+d==DUcr((YWsN4GfqTeB1awryjiilhi9fxEfn0b;T%5 zxVf${e)}tucmPKDhy)=6@S=m1u+TxBR_V8&o_1Laoj@~>?N~GY8~dfqsm!|IT$$Rs zeNdV)-$LM~(?=k9FoW>k>KjJXoq-GBJ5TA!Z?q#nsdzmwi#=%aker^}ELia&GGO{PFJJ&nuue zDVMkwx%Y52-a{UA85RWhyo{g+jtbc4Of)PJRiHu-MNyLU)3H^jIM4NO$ap({QaCR) zH0v)?uMk~+g{>vC$$HP~koQtqW@D1urUoNo?GmoIL(`GkG`52Wz4vX8;A%j7DKM$Y zHY{{t$O1M1UxG|2T^Y5g^lfH5=J#3NY%ZzqaS$IXz0!s*9|;SzTqv(!Ipmn|(VJ4YIz`a(Q$HD?#YtpP;I2og9mJ3OEY2ToR@vSW+=^Q=F+Sz$YSHi-#FcSY9)825htB$XpTEx3 zt_`9ECN7phg{o{q&if>Z?Mq0q*_SIU)x{#k+TUD0*xx3%es6_ylM3b;?WrLny+Ekk z!UEuWYgfrFdrl6kXt$siG_03%a>Xek=8X{dMCc=y^3-Y+-{jog1CA32Px(?^JNtE` z_gvE6s=kxImCTuDBof}pIGR|zyx$<(K}DLGq)n-Ee3Yp;rM}#usk(#x<2`abkk|oO z*_esL8{4^|IfuEGFiXVyOE9Q~=r2A??XQ}h`>@R_WB9X7V~qOFr{fZD2dY4CbPG*_ep>=Z18LzO=~`w$S(clgLj9i~o^XrQayAhj71EO|m{jg~r5EM0AQ-aR)RYyqLC^b; zZMKYUzo(@}8+7smUu?@A;y|U6hs@+|R`2A7XAkQL?`ycB^f7B5M^zotbQ)vBc5SZhIA>~KwAq3Ql6B}_rL zO+n|rPFNaCs=-8}pyYJk#$ul*U;UO?&0=7TA#t(4TSN)WiGFpJ`3_GC!cwB~9tf6P zu&N9AY4lOX_t52j>sZ?*8?3Lb&5~Pe*EiBS+Kj8tZ!6%@X)H6JW+Une`d0c=j|c-Z zjdNEgKD#d5LH}QrHR7t@$N)nf6`PzVe{uD@jWfprn}b7xt|n3o@WTc5Mo@`!Ft@A= z$Zbb|p`U?)#TSH1r?snSi4@44+F`rI2h~7hA+9ls#}$voPVW7=ocFyrCd}{nv6EA6 zlN3_PyA5<_v(jK`AI=vKdm6;%0!GAbXe=&!I#0u-q6_I@!J}mpTp0R^n65jmCzm$S z>wxY;xA(G2CwJ-F!jTPbjhP%d(;R6VXEzVZyLB!%lY!nNwlN)$%u zwX&*^sC0#zRxl7HSHO*m-JL07$A((LS%_IXfW){5GX;l9ZOCL=C*bF6d;JeciZ2J zBm$TY$Aa^)I&TjR>kF@gZ_Hfc2&Ch%A{nCQlwB89<2YXs8K3v$L*K2c%_`uk@THQ45oU=4xq=U;Jw0xx zY{hD+(3>8eyhp%lVB^HPg4?*XcL}Q(8;O>csAwv-mlbp3>pzq|%D7;;gX2}R0J_~2 z&OXk!@9x@%PidcBEd4lK1E+>R!=i(f?I8*19%(3WuItyo|Dx%HW6}{N#^Z19b^qnU zmnV=00)g)5cH|}l`v=0X2s`t4$kMGjwvMfji^ zPs$uOG#3W|S6JY;ZX-r`sn+Mw#i)1v+YLl6>tcgD%p{l9;BNZk+1eH9YR{ULDK=1v zT#||O=N|tYh6}SL2+%Z#2-kz3wznZOgYvo>F;`6v6TC6tZwMhEBD6T=0Edcd{Hpo7 zhH*SWVd_=wB2&5}DRqoXF?6iQ$gD7OQP)r3tq^kcTLA8VW^R5%{M8!|$|R=A)DYR` z5`Lv^rm=zWPVQ^H^9JID{W0zjU*XQawGAQ>h`vSRb&n8yoEOTDSb;+e7~5~69%HR( zs%ffiT25cF40)(DIzf6+kW_glTrPYnOeNLj*AhlgLWqb+y>$D+z=EKs?>+8QPMYbm z(a_V<)6gSl*<$ym`hs{O`w~5-OesaFZC%!hqb(PgK|0|puG?vGI3ScOK!f6==W1!! zZ{>)L_L$|juC`p0COGK!UK&}kao9#$-p%3)f`hJ797VAS+cJsUR`v1f6GgHGCdyt0 zH5_MF&Nq$^jzO)kKpIf;PTsBtzRP>Q2zo23=tp1d|Jdu^*gyE#aO7??-b2SNA z{~?mA$iB1cLpnHi`~td&*&*={cqm!A@W?N?F6q_sa5 zuSFTUDqS(S*X4B=uF*&RLJ2#NSXCc04g6>j;7s{n^IaZi)ZS+>Ik1tsD{Ub+ynBru zue@t%%8|X7^J}$MvY3Z%dBsKvLTMK+afPHl;HYOV6g+Q7Qbw-t`hVuH&>_44IDT<_ zo}kRX2qXbZVd%*uE>c%^P~jlB!q_y`lcr=v(p9}F3v30 zmYZ4;v2rVOe3$IXp41LA4M{55sy9Jc^jof$u)iF+ZVoCf9L@2964GgkysSJD6v6X0 z1JKz`w+AO>%@yDBM%18kd3>6$fPkjBdq(Vd@BBN#c$C73unJe}BfHsk<<5WeS{1>qEK; zT(k(6uc_M~z@Lk=myd>V&4mH2&FM(l>;Zq^$kyH^&H{0EjnS^AG_FK3Qn!SY`!diA z{>@qV``tT+*% zg=wpsnnGn9ve-3_&LY1qtbDP8sq_Ez#dyPyrdE*?cSJzu$H3OeK`xo~KVL=jT#F} z=652XoyR=+U_m1hyj)lJhP3-2($VU4RdHHSRsPMYaVO9|sHJyJ97G$Dne@8kbKX6N}FmiWdsG!Gk6xD2ta+ zOM{d)*l=?5T)Oc~zMKCx=8Vc0HvTeAVrKw8OM z24WP@3N8SegLhhSGhuf3b%?d$t%8;UpoQ;hh2BzNr2RzueBf9NaM(Z5rOoh*9j-wn z%oWT=pkGq&73<-F-d8#~`~C9_{Y03d$_uj&yCzjw_i~y=&^|VrhY{9k?_&chW<9s9 zuqiWo-Su?6MdbB?A3REmviuyQ7SI8d*+5NcG=h``w~BVs1(-wr)TAZM^F#J!29~N< zVwUyUGk^;#b5Bx=U048hVXNmRaP=%xPUap*AvljYkpg-ocj6bo9!u-X9grSwhLZU46U{bld_#Haq|1^l%9eY*C3w|}4iH2VD(*Zoh&Kj!({wfyeM_L=GR z+dKHz@#!c0hrjSoN8#^Le^cfCwio_(F8*mj{KtsD*8RH?@%QMz`hA{%kNM|&|Hl>n zp7VG6_xaDe_}%_}{NwL`dJ2E{|Gf84yW;QTAESQv{C!Qo&%ck~&;9e7|L&vT^Z#}? z{=5DE|L^y?2meGy{b5l3-=-J*T};Tp_6KhCPiy?%pMM4U{yX#FXC#-d&EEs8m|6b_ z==!hpl^Bi0XJ~;GjWmr6jVz7aUjbf#@|Qx87)gi{~2Jx_8DIBxsiVYS)Vx!{|sdPkMD2) z1D^GV!;j&&fA1ei)?W+!zdHO_Ss7XXM6&9gjh*F{Th5sy@?jyPRAeau3CO->Cn6vL z`4ir1S@<1olA}|O)`VK3mOc%^M}lu=QHq6ZZ9R2lXB2lgIBJtg>BU2}F?eF)pbLF;Cwi zy@+X;)W?uHpMnz8s3eA?j0~WOZk#Cy+XONM`lKhic(xBnZUnQz?VRyDSSk4NyE9$B zB59?t%98FCUgj;g!RXfTCLsO7@ybl0in|urws_@)=IIs}G#1Z`F|Z938R_K^s?>Vf z4WGFcFg4K@1@jymmFl-HFd!NIKK*)<%t*&ix+VJbG&995>aHagC_pF@x8rdohi^M@ zh-eT4qx%cDb#g1@$uwje1yGy5#fp@|t8eh7u?SfZ-^cy7aXHue&HMwV*q=ru{oxbA z)>s$#2Ctmdn2b(djVC(>9nVJa)Y*9{y!^faC5q6ukJ__FQTtGaRIWv!MWITfNwX5w zN=HM-V_M7U5{7R`;iYb7uO{JYF>ue%LB9jSQR^7re!mz{*;#!!-Y=fe!DRUy?qp1j z%Ru68b=3A$NsSkr7;M>xq(_b@NlBzdtQV{tZ2IZoW3Tp0;_E|X2ubgw)I+CBU6L## zyhkVvZtm04W1@>(5-$6;k8B>I9OBRiq{nrIQSGB5VnL`y!i;E*kQyS`hqf)LM^T+r zg$T2acE#CEsD+mUf!0k-j2Ih229k~!QVkOMMYv7G0C5NM8=cP(F)`94(j;PZ9~zA7 zHW_}rGnNq+gIhQ^Cnx7l@lYWYfsxRX39^ad?b9s)TP!DC!k1L8`(LcRv+@}ow`0%7 zB?B}V7b#sSn@Oz3dQSqk7Cp5aTqL0yU!Rh6J>QoP&c?oux$a*=gUZ$w`4?F!ahoS` z`J|7(8^-9#Z5Rv~^zKfOl@{De`*^8MB=;@%+5UKsZS`f@>bIq#b~k+&FV+5a6SGTn zAnuji0*NM-g$L1-Cv_a4^Tpv?SGYrR+i~Q+T%rma;~a~gX5bRLWsOg{{|eCU8nqwv zoL@=Lc4#j@tuLrx9bV{s}=A;FC^?TV0r+0~~^w0kkP9*c{JZSxCTJ0gh-EMwklt!2nrs zs6uqy3)U^ErX;r$DOO%EOFEk;zWz+afD43`#`cU3k&CNCE}$of0;(64&l0H2YE+rF zX=&(|19Z2L!6o(F;0Kcli1C~_T~x$=Lm#6%146}waM*`6dX?h$?ieU_3Y+W%5?8&nY`U3iPWaXF!|{-lYEGFd z8f?hN7_a)IkaR#g5M2ixmfE{7bF1lh(N$ooa;kDe>AsPmovJ$W@MA;qgjbXC%tNX4 zH)8O3a#QqiPv9Xi@~|aRK4jsBnq4}fSx4;QCb+Xpj2o!KdJtG))VnLZ2!Xy(Sd;HL zV;d?;_gZIzSlXSf3r;Zx8co0yN#`l_4~V>;tMS$NN=TV_TSmKQ4AcOG)Hw|)DazOj zj|j}1FhLK+R1%EPXt>G;D~Q#6dU<;_k<_-A)m ze#kpr_d|NG(j2;fINszUvos4&+&A0?ByhduVQ9ni404*2)+wDf1(~U+%eLT46|CxX zSDnC4#YX@I4M%TNl6G&F-q+2o(NH?NVhN7(RO>L8S)N?Es*NlIqU7c!E{mTD-W!!p z0-*5P>+C2nYDF8<7wx9#D`j5SJ=$5~Ui!7rKe(_^m-=w)I(M{XK36lGlDm_;CkbCA zYuacFP>*NLC0~03nxr*X_~j3uOYr8vpaj9;(WO8`3UoDhy1SoVDQgHm`3BIbiUVK< zShqkYUmiVdKE7nyY;f2XACW(TrG-=Q^_kq46oGcX>g@hVf=JgEq&rW~H=;E+1z`2* z-LhlZ1xB4J%ubv^6lc_=A0(Z_kugU){lXUF`RplZ>)_J=wrOeA-ZCQxEy4;r;WxV3 z^Ku1T%5pxcfzfOEUS0JCptbkhU%}QNOVH*Q08u1rp}Ij6D)9Q5%>t8flFy%pVSM-;Qg;Q^c8scE@bD42 z>a8?y8}e1<0bTGq>$(q0idiRN9lmqiRdJ2Mv2FDt+c-re(bi<1mNVt2?&Z2s=)n$5 ztt%kD;cMjGo^Qm(#zqZFDVEJrf$utsifWo3@xBpMT&UQ(i8=+9&uf82u%<}lvnMVW z4;INR%^9gqjkSO&UgN*xw;w4&s{_-Y9o?5!dGA$_QL%?1w`K9SOIj)~qpq4^7SJ*W z9#M4Dvc3n%U#5q4>^QAVGS@2su-K?-uvhk&!vWPa=hW0Qb+W6I%Z2=C5)B!i9@+I} zTrf>Cbf{1`v7}(K0BxU8gy8fpyE@th1t49tP0V=@Cds?ycUz+6Kxt4FZPg=Z0NmngCrj-Yn7pL z!q(8K&oyLbj+n%zbixI-XeyI$?*|!kWLmTWa%?U5HPFLX;&*LkCPfuU2J>?GxJDY7 zi!7zTpB;b*n8LE@81F47V+xrf8rY>GuL~>T>HI#ncD}0rGHLq+Jfbuu(x9yDlx8OC z(dv(!MOOgwjU~t1h?G_Z${X=8?TPwTyi#RPK}xHcM%Tm0;kz}~!_axCJsu&vk>S7v z0@j<>a5r9@lWcELuT(dk4S-Sr`~^-%zXB>5A(SSZ&WU${%>FX6$R+VsfdoItlqaFu zVw;f;?!)eRq=2@!@TSsydk3-&CWQ^NjAK3oi>qSrSN8_|DJ$4RZ>%ASr)MI}EW zg4io2@T>1*@r}ADX`jEz<8bcY!)sWyK~;D^2vUIqv1Mo|yXVI<0XCRPcWwOY*@QG> zyP;B>rGYap@>s?(&t5jD$COBmTT{O`IPM3_g*5TOUcyqkX0}8%t2sJTD$OXLq|XBe zCM#89#<8}3zDE*1TW(b?Rsd>P-=(tCAtv4B_RSRL%nQJvgWx#~h*v3Y`<4$R_KQtl zHFyu2QZDxqKyB>%S{;K;;)enM{1d)}7p&N`Nh3sN+m$xQJWiJNjTu+gS7$7(!UOWU z8XXo(f*EqBy<%pJlTLig)_7pswa zrv*sW!56=Z$Yp?LOl%Wd0E=4sJpiNy%d+}&o`m*%;t6xTCK}pX8A!};iQe%y`?@_~ zKW2}^6$`)W|2ze9WwH4cHg1(F9yzT`8+P}#_>d^Uy`BzuXt-|VH6pcevWr#Z0K#RY z9xu`0bfP4#5oQj|t7#w^fX6EbONF8w@Ke%yGC)%KBLTIsJ7kVoX^&0%a2GI*<+=0V zY@?RmCo}=Pn5Fv4<=McQi78$6_)^aw7mz6Q`iaPVbjkvO--zR9#CM+s@``GLq zruMB1y7*s=p3&S0q*?PJ=jU_tH0k4mSSz~l84PlZ5V^%u<+5S`u6(*_!JyzbVtN!d%UIV0x91M4V7sGy2r?t0y1!z%z1lJLd> zp?pTCcPXb`9u2DvY0#)C)P{Z(u)_AUE`&Z$oK(!{2#+4oY?YQ7*^iR_P~-AG10^E* zc?KsOT{;9>*u`?uBVveaDe`FLq^cxq2cg9#PV=(k$XLko-*(i*8#Xt^7|XjJ&E^T?bp zZwgRYy$Zz=M!Da_vlfm3O=+9Cy0sq90W>D4C~aBO`v<-+DCN$VtQj97DUs-08D_hf zAiG*hI?e21dYoM@3$L~{mYd;qCm|-7rnnxLc65yEK2E7$qTN)L3jxh^r@Bow)l(4W zzyu&f9nI|T7`J#GwGN%Ju}m%K!=j42!FhTxho^w`CWEyv=PioVGO}BW6nYF0r!Jm1 zpoeQP;)iH{iyMUF!c9slrhsK>_(IVikZ_)&j>t}zQH=uHLu^|{$D5o+8~G>5=B0i}h5Y&hbWbKBxQFRmgd9vy<+G2F zAX1h$ERDniDQ%*W@Wkp_RMCP(z^96(l}+3E=8~rD^|$mX8_*&m=W%q`p0~MC3GM7b z+0b-?MdLG3CGipO#+}I(BoE( z0MJt_vl1K*Dpcj54YCIIa;wKx36t69O02JY%KH(AD9c*B14g%NuVp)aOI~ zKP&Dq8I4kxa}F@(nF*$1k-Ydc^&P6V9=jtfm$+}H7;lf$7&toG-#ky&Zd4wvKDbj~ zg-Ii=r>7DLTCn4&fh=Waz{(xeI19c|lEQ}rz_Y17P}HmAv$ z4!s`vj-@rXS>nA@#8d`N;DLzBXvXOPM<)zN=Jd|@aI)&kVgiaPrEPnHJi~eM!$T&V zz<{P42Li+IvhXJQvgc)~+!Kg~z7*V}ANbpaD1V~Rke)}^ft@0NBesb$JOnss(7dEK z7U#mUyyoWQ9{uwT)Z7oQf$9WxlOHMG@pI;qV$}mHZo$YraTj4%-Q17Br6>;5puspP zNYyXSk*?fdjy`~_y4MLZ#Rk&R8wE?10pyHHsi~~}kbiA`vD#so$IbMAp_6bjo0V|h zRhZgnbSc9_FFGsmUAt2G3fq3o3x#%vK826+)2RGhwCTs0%1Y#7)2}sV)TkJk%%bmF z8*ayk!iB8iYbJDU-4@kPH+&c&`x1hq2gV{8k?!me!=)^&lH;0+k_`yWCBD6c6_Q3p z%r9|u-)g17XS@dr==KA|08cB!IY+;f#SsCuXXnAHl+4GI9UeuqsJP#@@iCaK3!TJ% z>T-4{U|K%uH?30bP*lNOy`H|x{zJezpm(k3}y_s-l% z`0exJpsr^@R9ENhJ6?aTMolL+%72)^b9^MJU9qMi!AIwXC4UMVx<>!PQi>>?wC41 zR)1)zVr*+oW^PtWZr&o7dVek%eI~tQkiUyPlp;A%$kn`icL`V7NWfSh+t3~M*5d;ar($sxHAk|W4Uvnq>ED+To4@8=Bg6&sLXs4g@yxgWm*ik@kl0x+&( zZ@Tb^Iez;^<3>>JB(5t$|06h2mJxm9Oj*|{W0PQfkl5pw_ATi3 z=T_-+@G?I6C=L<**TD1a%49f;wzT{2ZXrxM?{_lF%v#jc#82ss_fFascFG_ntR122 zj%9k`+FLMY^tGj8H9VTgnVH@3M34qwy>~}bz&gG@KSH7RLRhvim%Wih=%mKDOwMcm zXh5BKyd`OZ=vowEb-C9@d?^PNEnVN1NX7g)pl{6BF?T9SduwFun_Tsio}4fc7n&2e{q7`FHA3!@pdBe&Q&TwK|@ISK$Q zN8$Hn=bGHBu$#^ek+HT_3>eU}E**a|}R-P-;0}IOvjKTo1%d48WqagRW-c;=W6F&s4C_JE21I6iN)w=-OSu6f*?S9sAUe zYT6Ow-TCVTi!EyNF=kRfmr}P@FZ~D2iZg|g{W99o;LhfV3+A6venj%=7*k*#IT!o@ zk)(!h9g-^=ob{fdoFOjz{#FKaOJeH&8NgM5FCE>hy1U)dE`@%nmjW%457kWA&UNsJ zesIwfz?Bx%%b4~xdGljP;xO>^C8BEUD!bIjNDV(l*^~`7qydU`aX^SBa*o1DEDm!_ zW#$FQK7c=JB$QW#R!e8 z=@I}A^xcNS*Zxy^2l9xnBia%sR+-?zLdg+;_Kww}@s(Pqbu%j;rCfOv8wM<$WX)-_ z4mu2V$Y-|*@?3(A0@~0ya7s|6@^PKnS_FMUnr*l-&_9Na^j_Y$yF1gYlx)0 z^Nnmz-B{ond#-T56D=Oux?)uvWBOd1XNReA0(+T|!)m6Rl!=Auk^Jf&`R?zEEJAGT zRg0!)@8OC(!9MVW;^@?i9;$&}y`(n>-s^fE(Cf|`UA(x>BWa}9Ar_$-v@Cy2MrjAy z9PvG9bj!cCxb=hRGE3I9KDEr1`gvA_3r+)k?6fYltwA1p+^ZSs*uur;(e(7IQqcBF zN#0#5g##Y$?g0r_sGw-Do@G|U@>MYSs)bEc+pZibNNkMf!eO0K?g2Muv5w+&Q8h}5 z2~6BP`x2{hQrr7tG411no6WS}ajG_^pg*VRyli26; zl}+%i2xaII$c#4SIu(D`Rus`?2FcU#Q_I(oC}RgXur3Jtaa?Ywur^pCXc&GN-kmAW zKCv6389OzQ@;8!k>eG?^)Czdnl^W* zz3xjV9k~180U6zF_hoAJ1KW73AKM`ZGd#_rk1AwPkq`xr}K~xKj(=e8!O=31k{r2Nc5i#Op{e zR9#0{Dx^|=y9-ROu8OyhJKoO5sG_tn=wai2`Rx&D6$wXsLkw-^ih2i}{a2;rv&l+w zN?H#~t+0t&|*tTt3JGO1xwr$(C%^lmec8nb-JIRi7bNY0D zefs;l@4f$jpY`Oas#UXQX3dpV^Br%-7_aDM0NF7P7LHs^lz3rG7h@xpoe#+U;$vY8 zBE0$N-dQ-lh50-xnIwH-KcPEwA%!xz5k0FjGt7r056)RbrT}QoqbUegsf@?~1nPU( zma9xzJ#CpM{F+2zeF-!A;Y9bcU~IeOQ9mt_0tM+$p2jMBNaMkiy@9BOMM$xZ3S5hT zk}qaA1Dm`O*P!Orz;4=}^3jbXS!S^&y{{3o;gk<}H^`ha5|5u1BTQHlA?)kHI$vE6 z5AkcfK+bdnhutnc**nN+N>c)U3Zos)=C2v9@x~<`?f(AzxzjGz%b`3z7+!0R5gV=_ zXgC-x#GwQ(_(4}UHWGh z4_jeQ*N1vhhTNfkr9>I_TL}j~yJXKNDfnFjS{)B9?PtkV)SMJd7MED{2;p5Cz7Ad& z9(!eL^9uI+?7w}V8P2B)#kNSj$#8;BO&sfUw6T!i9~&?6y&cVNM!Ml-6OjES;XR&x zjyLfsvH0md4#amz;fzilcY0XA4ZmFq*7gJWt0Qyu5-d5AxD8mLN^^8P z4vxsx@!y?W*(3QUNno^`L8F8cA%O#%LL?SG8V^Ym^e~ zc@6p*>RxPgKH1!M(GaqelS>5n+#D|@#o4m^ZMj8jsqLJ7!^x!PUEP19)^|T7fEmRv z!pM7nAyy)oL4-ITp0;^#p{>~urJ*}`+1YiI4iL`HP$9Q~^w}n>Mc2>z#rcL38rHAD zz}u8yV!kB+)nzV{#E(+&xOb&H|{F|bPWMB3Q?lzDcPY@b!QF(KgmEt zcmlO6jF;1uk}N(~r)!^y=H|7a`~Yv|#m!lR;^t?RaqwjgAVRf*#`XOsAZq4fA+dp7VJ zV71yxuNv+I_-$T7`%Br=8??JRH?r$@nJq_V;;(FL5Ondsi{yG*Nsf0*X?nYATLhQg#l&rsJ^aKZa#+nM6(p6NlA0uPPxM?KyIV>cnS*%Fbzv5EPD#M;bW zgv1hTZD2u;`atD%NW`2+7cz&dSLcD-y3Vtr4S z2{1b7w#Qa2mb903qcSFpJlAZq4_iUG_2%6m8qFztY3IxtnS*zQdK#%NWhAYaH*DSf zT+*tH92uU|gBxGb*s4@U4Zius_qW0A|2|)(o~#Y7_8_!W3MnWdL*jWWM72LNkF5}G zM*uy0NtR-*3AK?CVrsdM3gl6$O$%5&p?yCK_#+71eU%T~T|?7L_ET;36RPc=7jySy zQBp^>Xt;1nYg&fi>DLaJqM~}r>ek}A^EHi_hBaG;3>)>gS9#f5uz0ej$s>!fS=<1n zj$E*`-u6w6Au1>iZg+6eKY8rPhh>2y292qyE=BEv@~XP>a=y&DE}K_42}v$;2S44!UDP|5Oh#lWr-Jj(NC-g*$?~x4 z`@sY@3P{V(jA9B=x(627gaEtc2{e;@iU(5yq9%Ly?i!aoZ~wt>?bLb-0Bam{?5hfQ zYL-7B(;;xI(`v>n2gzBTXxuYZG5{EWTQSYEL1h!?6_Uv*m8E~cwMHo+AwQmwnM@Pt zm1Hmx_Y_wYwK$}{37IzmG>BByfFVEDSb78?Mm0>JREGbO5Ygt)6YvP3xz0oFct(!? zd88edM#OC+tyYgLi{0f3U2j#{qM5aA*(QndsLu0{bh=g3)8{CpNOT|t(#wY2I>&JLo)^dhi+ zS8qSB{mKW#Nz-MLk>;~p?~Ex3_O+8H-X(h1UTMo-5HT-`JLo>7YJ64Lip|< zUbMVA_U>ys3tTVQVMD-pDf1+vnegO7@$p$I<~XE>fTarp9>jv>=(gYY z)~5`!Kg`L9;#hA0)Rg;@{T~!&gDyKM0P0zWza;`#){}Z2i|O z|Ix$$`}R)~9@F1P^~d(lG5`7gAN@0Pviy}W_P4wJKNH5-SsDI982j7NKT6g9z4q&$ z)n5Nb==!tDtHqb8mJPkF#TR4i@0MEsfwlE#8P`ARw|>38O3!|kUA1thcQ$u4F`;*M zv-=-BtgoT`X^X|o^taFMZ@yT6lu`YnTyiin6MT*CUszaw6r%o%E!MyMh5T4|$YPyj&9lH)Uayocs}XAz4;%z!(J< zQABtaBB&n|(-R#yllzT0jgEA$Mrqh+8ovz_+6nUgArKKzV-%1JM^2e7?>$74$|Fj{ zSSmb$Q&sA8yZQ8{l~Dm8WMT+K zlj1~31T)`akpmjY3Eyg8TwJ)WE#C}2@vW6@=sAxehJ#cHeluXj6x1B+9ks?Y!jy`O zF;$VRmb5V0p6&|GI_;C;#f~zhVDT7%=Ejrrg#UFzBK`hVC?jZu?;SP5JUiG;o)E!W z@*>f7Ky3~vtf05n$xp4V0+wq}T+&C}2cjCBQFve^c{e$K_o;@TtD$*e%Gc1|wo$TI z7^><*3VmJ&C4f!gI1u~iu8eI2lpYKH-I82Ivt$|F58y-FF%<^)NCsHPD#^%57%t! zfrtPPswJTere1_b*Tu>}o^?G8VU3uHHMp;9reQMG6ypq0=~=> zYquU;1tkf`Tv#@IL0G9^DH$#$AuY*5zK&I0D(cyMhfo(3_Jsaq{yb+1)yt#!tMhKl zW~%-}WxBNR0E=3EPu<{rGLu)5V}b+LhYyN+_#*lOEW!QI1rb-s^%NZ{OLy9a?rixY z%=6SM#LSsallbjmf80q=&E~$$;rR1tXF5Cg444E8VZf$-xW)L)DEiz059GBZg8ABA z1+-sr4KZ<7D$>`RZpbcr4h0vRbV38Nw(w*&ZzJ0)w`liOu_`KDq_Ao!YA4}a;q3ij zQib@g1MW8i@VYot@OyvN9qn!1y7IUQN|2Vw*6eFLT!6ce1khaQHsA04Y5myt(Tb2E z@!Tr+*{D!hbBf><1bT_sUF5{sS~y0s?*pbRe)CPhOy-*n(BIJrHsy-ljZtgmpiD_N zi&PX{V!1jP&>fdLEZ2l0LsKT0<`#1Tv1_Q!_I^_D2MMK~%84;fKeI985NRRB+>SS7 z^k2cN5#l+1_xr@0riEo+(e|2T0Yb1g&y0a(y_5f~!h0r~yEHm5CRl>rO)WcDdl>|G z#R+#=iTLgY*$q{tnf^`AntAO~kT|*2tcrj-Y{V?Ih_fPLS=1l}pkj>?u4pM?yUsiY zvt!Ho>e;3fw&cfAENv$usG@oH39_^`yhL1N@x1Amr!r+*0(27O&h~0brKIC>NdAP` z;riwwF9KYE$v0`v4MAB8NBF~H=BIRdKu%T@EE@|(9hJ?EPvZLk)_QG)P9tfWtsZ}oKqT@!kV-jfsM@rV+cqAzp0E2W0Y6mY#-m5TF;=57RaH6K!c3RE+wkE@AduF2yx{ z>(bU0_v=FbX_k`stix4;OW}*GMqJb>I!hdEF%Uco4hYZl@^frd+N{mvW8B&mx=0pt zxm3;Q%P!Ef{v+jYOz5sHl+65CTxS*4s-;ZPNMAjCK6d@%X7=1?ZZWtx+mH1^ZmO$% zC7GWod2I>If|M)<3PP4`AS<#DnvLsDwZGT1fMZU4&q~D-nz`Fe40iAm{2qW&9PEZ@ zy`2*xQTMm)Kx^eT%Q@4<-BSGu+DNZuB+`tVt=pK3JjG7Yq4%10Ufg0V2qKMN5PThJ0$wL-07j-sfyAw|AVxS7CJKP-@^S zV`+7P0}kI#e;?0jzksGAvISvUOuJ2WC7C7kqS9hmJPYXxeJfvpib0bcue^Xd%6;3~ z;`Th3%o{4H$MLSgeo=hrd>jGla9`)<28-)$-kms7a!c@mb-q3`nz_Chw+q0}UscuA zF3x4@y0weD*p7z}QA$Z!Pf5w#uXNm)_^xoL!qp5w%U#Eed;Q>vjGWcY&Ro576aAH8 zl4wX29B^?XDJmVV}4VQ&wz650DNS+AnuB& z^x#bFhxw!#bLUhX#Eqj=w|b%O8+*0!kF(v=hHB22SUivz<3y_gzbDKu37b=70hC#Y zc~X{2MK%BR=w3Zk=yH!{ofh(}El*!La3H52#_T?DxtzkEie@$WQN$v7V(CUe3%Z(Y z{swlq8%8$8GrKK#2L*MfEN?E)&^yB&asq$`tlNc5KZxA$>xzaBQ`uP8LBQgu+b-dh zWjpGk5)t1v0q63W%()rX1=FKi2s`f`p+uovLH<|dH(OzuKrDu_qxejT1F|iWDeqwn z2Oa^kVyqbba?FP^5A(WUv*+W_)lw1Y$$K~7K-Uw7m;rw^$4jb zUd{Bpjk}HBLwbDaT==YTsoDpEcw+mL${^KERCv_0eZU;BC%b|ky?`;DiUuKM?fDE- z)iD0U^z@pzfi%K@UIufDwC9AU!_3f@>RXXAU_ z1L7*F;JDc2<|{t}*seN&r;Fxssqf`gT9{-hO;@zy_NyePfpAo5n`3Zs#Nt}=%$#wI zYVDnISh!-z!~KAz*PFop9pw7&R>6N@r2hoDgjCgK<;1@rmy*U`Fc$&KpH|QRgj!!= z`&Tdj6r&KZ{ApPG7o_!HX3&42qd(Bg{~b#DXRCki{S!<5VN(5fi1weiY;6Bs3-zDd z{__mm|Ij0`ercTkdHz4`{R1NYasD4*?a#JfaP42<*?)Ia{x1kCXy9b>UkCpk-wIop znwmJ8*czEQX)(~-SlE7nt1pqxmpYn>zow_h%bM1~z)b zuPYfDndlu2j4g}|tpDX+{+QwaC#We+ul$!W{qej0?Lhu9(tqC9->2Xo_x#`I;_rj~ z$F2WG^ux*a53tGdFIe;IeFDxe^!MMf<{#OBzhKRO{c8N*flU_1zbbtGqM!V~HeF`? zvUC2c!bc^^%Ua0=W%OD{%xzd9k2Pz*`v^Y(!ihUN18H8GZ0u$jX??_B^Q!fG3$9Ju z!qxodHt+S%%h!gOR5U1c3x0*<~J3)1+tAb zF??lKjC<4NDoflNDv4@Jod+--xw;ItN>@y{1<~#ea~AbfmbvZb*p;kKi=+tNrK&5o zO86f&mJ4Ui+m^(k2Gm$tKYqq%+Um93?&VU!XvOiwv>3eBbGt$@8+tjwfp5kAatBZn z*HGtlg{>Q0e#$c$X*6ChzCc}nz6+bkxWiAgLG6m%Z~fu1*XE(k@e&1N^Qqx+*|{V} zjGrxZ2&&Baso_Y1g$=&X++#3zh6loa1QYZ*Rw4WXl7Sa9yocm4ZExYWO6EGPdDrp1 zd;f8eULAYQcH2t!wctHSh(|OGbt^dTw{YOP{uxO|pOPSytJoTG1M`Ma=vmh`w%xOy={cEsUm$><}V4K8P>igK`5p^jv|rR_Pw-Q%mpNjROL5_hHjvZ!q(ebYrP-0bXsOt z=0WTbB8<3`P@N=4RiIJQ<)UWE`?^b(axd;^mAJ76Bjc|~Fd>hTHigD82fqTEG9%ze zi@5Wo6U$Y&uoMQJ5Zy2;ljhX}!}8gM9lF{fZOn5;zRk~6%DY>WpgjIkghnLlAGxR= z9b_n0#$AVcY6D6{RU^(~N(q%7fnu|~4csyXjUt7mlq`(O{2b{0lnTw1=g(k2y7x?y zsGH5Wd)B7=gkOHl&Yw;$mxX9ck>_|u93UpORFu$HUA^*1AQp)@6F6PyTC6DZ>DG?m zqPcxpb7~=;N79#UC#keN{Vu7ufGavqbiE2Uhvro8H|r;5)m z+{?+!2#Svs84}RxGfhjAhT5C_VyXDMYZ~s-N*_Cb_(CGQ^v#<~9Pjht#m#(OnF(W; z5C~DN?_?(rNo{J*Jc@lB)SAEEwf@yG=8MmUsnhdI65XLw&z3eoLMhTPrj%e=L(pA< ztm`*lpw^+pJw;`Eh^oY-HuWbvfo*b>)^N%`@xG}8bE0i?iXiqUa$QPP6@PWRiCEkZ z9r)W@pvrNm6Q&YI9^$uR>EJ>_ak}Ywo{L+Lv0+Syr4i4ZKGi|{HO@06)1ib)L~?k* z2yL3waF2mjik5VNEhfS)bIS3HAV5n8LEgFFv-weaTNauxqIu{|3v@qvN>KEWDs#lP zc0(-Tw)*PN&#FRk63* zeaH@Mwq`k3zOEKWl_&9raCy|VX{ej?@DoP09OtGvVVn!U4r5Dg<{)a+G;1$1Jgv@5 z2!!8U8me2ZK;9zw$@oDP*3**McgeJQ7<}|wFW+0`NWVY1C_f^XFdRi{#f=Y-BB6#H zg?No09mO?oQeRWzR8%KhgNDAtN&!*k{JW6uFY05(ugHq!PmR3dmzw)4Z~C=r{dLv( zi;7s#!r4jQ#8Jr3#@^2MkL90(UdYbc&QZz!D*~VwHgUBuG7)n$@c7y_vNJZ3H*mCZ z;^zLNjf;(~69FSL8@>EjykTMFZ1)uze5qII|IIq~cOfz(JJWv!nG!YzW+s0o-v8E# zw27^mv-wxF!odF5we2zs(i&9+!{1CVL~~*p2_#tv@@Pby6>Cl$q-F$6YRO($K{;AG zz+OB&6s#DD!5o4hgdlQ5SWx8<0x`xtKM=7$l)ogze=AL}Lu;e0r`yc+V?AfRiSZiV z+05+2?|3s~o%17yyg-ylaSI?+7_QNrOWSmbAcY4rL=?vHN%rWvv}_+Np+)L~fJ-e(V!oJ{8;%e` zd41Ft4{v_AwB?B_r}N_NTB@EgX^ln4g)0H{&UE3jGM83@e0iFQZ z8x|Wx*{J!ATMP)vTdPD6iz)(VKV+l34C=in8W^>n;IR8^>q=LV+&J?(# z|BnGM8vYc10u>K_=ZrED z#+GeTXDgiMk;vh=caGD_6}fPK0}Gx>GIj2gB_~!&NCr`<0R;Ooidpq&7&aAegyMtD zb-ja(-QKwVrs-Z42h-_>k>QZbS`39gRQBSkEoDvqrumDA931Nj+QEx4Y!J#*Iu$vx zw#;K#JM$67ClKH}&Ga&TRNz{6Pi5B)C9xNvA4P>lo-F{gt{x+dq62(hsaH&LEHKj^ zc*ZTA)UP@`LxOy#P}JFt%NNe$kp1Y73@f?$uf<#JGbNQRRS@>fqdzBOyMe%qZP3fr(sXumG&` zVN{0qZi{BiAN>!5{%<;^dRHa@yd>Y~YrWJmLnbsz;}pM15koAnXvE2z(klFrc=A8>DiM|arOffub$dW&pSw!NnHj)dZob5 zl4g7i%8E)fKNV%x&3%BYZN1>y0JTQh)xNHs1ICX{nJ+F8m5^E^Tc}j4@5IMXxaDni zBDgK^ehs353*N}qCkf(V`&_(0s<$dkLMDQ4SFkqYc&LB^U{W!TXg!srvf3{b-B@Ji#-G1Y zs#Fkkj=<|x@R;AnGwEts)(RIVxC9+R{FOPIAR3QiS^EqFVyr*+#-TvttJWwZKI3H@ zHztH=C53l7xiF?I3&M&8kO&(|Xeex;{1vES1mq_8ve=A^J;(U63LvLbR+10UG^{B} z#=(;?RCwaY0#+~=E+`6yEr1+QVDz*4C1!=6Q&fTtx_?yEtgJXxD-%@ww1y0|t(%|Z zW&FW_kbLoZ20<886aqo{n?}}#ToBNq{}v^38nxdgYog|pDq*vJD%zDj)V2$Wd~EXJ zBxYxGzhLbo%VdcFi-ovNR3fGTvwC6T8W}nQ2~a{fdJrTN{`88dfQ6`Rh=a5}uO~4L z4O%!R@wPPUT+5fx!(F$SKogciK}3(TfKDu8Y>a;+mvvHF(+U*U*xe(T;Dr(t!Pr?N zm_VJ{ydcm{fkq~1RQ=niD*E670-o4d8lPzV90_b9m_$&qzyd+3F>%JjWC(>oa|LM0`|0;R01(gij?};AntMQ2)13~y1Xo&;b zcG@GLAyg(}Xf889!9&o?0-o;2J>T$LKbVW znlG4*-m9Q!mJ_^P3jvT=BmCT`+3L8s7Rp>M1MaL4+!RO~1$gEI6} zh=Eq)>6#CBW62^mOh6c1ddu67K2>#)wva#z8bshisiG4JJv^+48rW^ujkW~q3tmjS z>>Wm-qxfCh+q2%i?=tWU>tSv1G9WBzQ{)xv{a*eY%SikCCj=%AnYY75GCzw^QC1s{ z>$>E)bFGcOB>fma2{Q}bFmfT3B2weE~*M9VK3;X;;-@(%-~ItgW(inw*37~ z81TvJDsRsp0yL{{MVsySPtXXthFsag+8@iWe#nzN$06aQpMZyqB?bQ`eED09^7rJy z|28Bc_!ENvk7&fs(TU*8=H&l(SR!NKWc81rNy){~*~8w1{y)Vij2sMX{}QD5xV4*z zq}VNUo{ZRX4M4DnBSg=Mn-?0HDq{!%BS{A(a>e5?z|6^jpTY!2<_m^xmSZ67aFmA! zn-(K}{Z9-5frMT1#A}wvU{yEu>w31=44ew}+lpNc)F#)-lpp@* zC+=e}>aWi?ib$Z38fB_ac6P$y;Lq2=&_O@{ytmbr&|kaE-b(F)x!`$w*j($M5GVUQ z+QX`EHiLPow!3zFd|KY=4kHz!PuI6uPtG6sIj3?Bb7oo%&eURQPvojp&K*r}EMe&V z;5$!q%pMevSiKDtAkgeH)5&xWPdDxf35y=BFOER~13DDbj8_>W!=EuoB#(AwR;WBZ=ss>_gT_t=6jA3;Mq| z)4s-4erq*O)Lguy?vN9aQ0YuFU{;G8SrL6`0!_{%jhug9zFUwZjHv`qNx;whVcVt# zTh04Al@VjQh$_;e*|I$0G|S89un*#GIBGQ+#u7I}##$U3LFK4$wF{}g3aF1z?d+8E z8&xRwq;YiY9r0A$zMQuNd!F4Ay?dG=9Vx$$J_MQk@e^$VOOcgXTI7B#IYFYhy5NGm z2nQWr9@3lH?ut}Rx1*YOt1nlz-^AMH;h`#y^AW?xSEDimqbTJR&HPi)DoqPy6|tEe zLWTFxoZOtb91;vwk`K&UfD)!q)k*Hu0#8*M7tOXvYxN-aF>S_kE z4ulp%(y=7r=IWqD{~(MAgpXD8`i_kH1Bmdbz}wJM%r&Dc_!h|-5_;hftTYQor65X9 z;Zdj73h_wc{FK6F-kW^NIG#tb8*D^MiLFdKz0XUdSk6+-D$@YkMYny54#NTRqAVm| zLubbb8bu}AD6LEc9J0&^C&ZR{FL?tDW^oTy##)pkuC*QtN=WM*(C;yMOdZkAKmzC{xofZ07x zLbX4t4$Bxw^cL9)TEU$_cT#flku{B|MVtVuFw zS;DyWw0IDA*?4Zn*f9)Hgil|SaHn9Jn9*&v6D57rdL(bAoT%Kx))2Ii04VigRVWgt zk1#?mz2$qJDzirO=r>7-b!|Fl)TgWGz6WYIS^{A+)yf~8TWb`36|u{UCnWUni}!b3 zm0E2r{A$%6e?oKirpH3DS+4JQ-MZh8kA^`xQ9mouY85gW$o36ZR2xuh#A|-WzKmVv zg(Nj*(xOk?r~L$fbTz|@G2p+1M0Uf#!-Hwy)s~T7X}|z|)j{QOYiAcem(iyWuk@M#R94eK~`R;nDDesTOC4rV$-QjfNbLDg*WYy;d0>acg zzfY|d;~ykiLn2>6PZWzq_S+W^!q%GKSXEjV7QP`Rp)~$xrwY$ zS?&GaQ&v`HBTG&h4{GmxqVn=pvgQnitMG zKyqbQ7_tmdYUgJ_I<{Nwkk1Nd?Zt>X>ruLWn!Bx)yVclK?-i?Isn<**VS@ zl%ONQJBA_0LrJ`@+jmy|Wdb~7S5iG&fVxHmuwk+flZf9Y(hdqu&PA&G6!Cb%bzc33 z2?oHCpxeQPy%6kcs#dnQWsV1a!y-8+NELztx7ab$rCaRX}ab(rOCo89%`8Ek@zmQ;ID$j^yYEeDn}JA~oBczTK5VaM=c zRdZPi3ZdjhW0fVEb}j>D?|~)l1*G_U&zW|=1xhGfD~U&Dk;WRdl@2E7%JZ|WdsF@wQbDJGj7I!Rx=GXW+$LI zBw-lg5^U~;;rpJ~@U`i3>6jazrtHIN+>BKd#$l>kx5i5}^CNHTY!lq{eq7qMyx`TEd3J z=J{EtJ_qFv`2>{>Z$4rJ(T!}YzreSQ0nQB+{;<0uU``9BlY~3&4|IU~zFL8CKrwR4 zC`n2lv;zzca(bUgx+yiauE>*V4N>jl-ac>14v-B9SA1ZC(aF?n7Z1#_2nVFmNwCJp5qykEc@hneV6j z$g)r75I;Zog&2p9-iKGt#6O3@fNCt%wxoY~|K`Uq@~jxT7BgBO8Ki=AQ=mX&Hc^pWd#%Cd2P-)bGhb zHSg?@1&f~Fl;(1z2x2r87JKH~XV$e*#H#fDWd8q#A7k_z0gtG%48RhO0LUClM z#y!839^FlIL7=&GCXQQ0ag7e(f@-GQNXJ_HotoDgml z9yx+OPFJK!Tct=I5JId8XHv3H$nf>K-Oind+AuuNHS{QBitMw9f@x7AqW%*0wK@Cx zQ*eAw^RLwqgU1rZYQ}&oKN=!w3yK2xJIIIqhD8}H3=F_^b-Yi0FeVGdXzLRH#8Wk6 zVr0`S;$c38bclG`pfti8@b1;?&gTbR5?O#+3>NDIuuzL;J3=$kBT4audR02j&lpO7 zhwW`1eV)Mw1LbH#B4x_C_3*w>IEo8K?n6>ZA%3IR-)?}z)!Ll)KYBl`pizj|Ywg{{ z6qwf#GGiIg>^xhpYAL~GG@49dPg%A1V-VkhC7=-&iQekohw^gWUpGGynbuwXE5@V) zvFxkKg?*eg9$9Kz8UrxltOeHw>LzUKZxKWSiBq4>y|XtRgtIB)y&o6N|BpcrsXqGUt?-}{_=%gX9%+z^bH#%(4sGD;nz zH~lEtg!@rbzCzKmoRKuKHrw|Mx$|o?cY+{ zx;(bOr!gY-A8tsDtAbf-Or#G-+;P51iJP#quER?^w1|l@Q4)>vDsZGFWyq6EFsaV8 z6uNK&r!;s>n!49gi2n4?$=RZiSsdbzp8DZ*$fy}+5E$HFfes+-m(AcqqNsZIqkvrAXE?XueYcBBD83Z<~-sL`EB&!Cp#K7&i`AYU|)Wzb;-jCzBJ zFa17#cPZhJSRp}8FtC@<;E?D7g})foZ|o6CzH5HG zySv+f&tp4{>lPT=eqtaiZO$aIB>KQH{lIy~bZE~^rK=L_WJcJ@@;%i+%33dsWJ$E5 z^iASf2~4KieRMYFxSBW~0ANDWe~1AX;*-$qW@v=myXE10Rfr9ENekQ&hdx?!KEBe1 zJc2Q&@~HfTXleow&Fj3TE!TZNB6%V{eVs)F*zibLH&_;{wna*xExVeMpAknVCgS^^ zL{nm51Un*Rw0@wP$dIa!VvkMwzSLlJa74MDLNt~kd+=2xjl;KKfz8V=kL;gEX^f<4 z%uLe6xm%MCh-90GX#hh2fqu>>so@jSsMDKV{y z+)FJaiOhrA2_%br4OsgnaMTcFxEOF?*9)X!JQDjP@;DjMe;U?`!S$KivIy(l4tq-n zxnPRnK>J8+BhFjyfekc_wB$t|0 zDrG#1lLEUTBK-F)Hmv!)lgk+?jg*MJslK!li9cjWl&%HG(Ed;ux}Lki)LH|s3CBWS z#2v`Q&_VP9}H`b&s{tR^&k_=1BMso^jt2P#;aXmf#K+FizRmTMa3mp=k`~v@lkVZ(|dIT5JqgYCa3S1FNhXBD4ZgS(orizIt(aZ!ql7uf_PtdZ4axjntr$< zlWYonn$HT-lI*xhQ;6@>+5VM+hM~Yy{d!c8iVpEHFsf6WrgUWJp2CSV<_QR_)ABrf z6#i(WI4hEMq*e{tbQDoAZVJjviPxe&!YX$~O3&(uED?yLz?^y*J=GCphK!-k3xMl| zbcf&y*1N{{Eaeb!!do5H7lT+7GbP192iOI z-}J};i6nI~k0G2jD2ZS34nF6_aDA@pu5DOCLG7efQ3NwTQ?VG!vcdMvp9$uvf2yi4LM^0}* z4Bf?uuQ@kr1Pq5yBQdN!2VLO&a-f`+Sfg`MyNX#UB%~VGsyKLo^X^;sy6PDPhUYyk zGYVyL5_5@6lxzTJ<^VNW1I}d3P6QcbfZ+oSui2^hP1A9MM4lS(u`kIXg|bkmhGIa7 zzYz^#Mm5670U1eD-!}~4Z?tqU=_2`_R{J4-+^?EYuf>~VS36IN{5`+IRC^vfMjL~; z84}pDRa2++zVRn>sYb3jzR6hC#^+UmrHP3o`lk84oNuji3)Hh8_0&0HRSFW+PJnQX zcK}b7zW54c>BI*S%07MI?Fhsv_N`&qDvXq4jN~%K(}cmexOJ4Ma2aCqt+gNF1euNz-VCL6NNMiNIBJU} z2FA>B5A`#YaR&u@tJY`<+(pvF&*+Kr_3<%!6u6{s`)QFvn8Vy0DTm+b+s5si4dCq5 z;ctbeeh-H;>8okjfLz{KjTup4v)d+eZb6m&bX@VMR*t$s_L9IAof;kKQ<)nuBs4`P zm&GaNG!>oCFbAc)Uo@-Gsdp+!L4j#CbY4)kln4MytUHuwFgDnCj|fkW$43>q-V6Qf`7l@2TIPt^5~W`_e=fzo6g)K#_m|D|;i+OQ ze`=Aab=Oi`lC%;yaZz|}u%L;sAYXAp0_Yfl>HgJIA03}weWS~yDY_|a>Tk+?3fqA~ zNEPK{Es{Z25GUnYs{Np$;Ws_N#enqpFYNn1G|KnGnhgjhk?=`=zZxOr$xi@j1VW-f zqnrZVDjdJjB}!hL8WdX$MR+b2zb@)l0;=M_*i zCu$zm1?GtJ#F$g_aM%I+n6CTHfZOV9tc$NA_8@u_WG*?`R79FKHYqWY&xis7Qkntj zkbHDt+k820qv*jZRM2;7adiiepoGd^LokaMabBgCIVf!?)CdqlQ#(52kW1IjvNE&Y z_s31YPvDUl{5FGkPxlw%IPO3+l}P4g zh5$gbOh9_Srv$#PgG;-w@Gz)%Da%zr7w;kFw`;XaQnWO7;oct0WUQdm+8cX_UL}Xs ziikmm46-m5i$Z=So81q`sRpI%_IFqNf&o%htKb>qaWi+_bU{0I+YR-a7gAFX4mHK) zFEdwZFC{#?wcbN)PT19yfSA_;>FV@h3AiCw^KwbK=vA};rq)(o?k?Ny$zAXF_xC4f z9-Q;#gVnWXK(4u!^vjWZ)hb%KZQvJ4#2wTD%xva;dYEp*88koW3_a2*PTEqrypG)k zDFMgje0c4wGDtpcb^KblHT@6b~A8)_kt%_V7KqP7g-;K5o5sUUhv=?g|cr5Jlf> z!mLQB$m&NFNBF|dgxa$p0hYh{z6F=%Ju}Cn z>1q(37vpnXknrs<5{n7Moj+BMy`A>UZeA=N&2z^gPfvMPmk@D|EB-DG z8M+`7ogR_+#^+D_Dx3Q@@QXf}^#&DHmcuAYlBv+dn~TDzeJ^RT5a3a9uT}9P_0imG z6kZp@X4M?7_k8=>`{pC;txHGyXW_)_PbU?vCiZL-FQ;yn+NzT!T52t%f5b3B{ z7?>(F!TuP;2fkC?Hpm<2zcc&ZiJl#Z%}EdJe4%{Y)F`%JRFFds5_1f=`f$ca`YC*+ zpM3S7A4a{MsqP6ho@*(rO;M`ubm(eo<3-b z4LO+SZ`5=$)$>wW)qjS1A>oqIA&BE;FF4Ar0pSE5IEz_>dKtUnC_>=~cO~5M^~AAT zZzNss6pjgUXiJZK8fY~eaQk$;1O&gJCr}lQiiSw$8(b_v7sGKFdhh+@={FgIB8ESf zJ6=+f>prtm)4&9sBV4Dtj(Ek1tIy^>(9eJ@THN)9F(-7{K83;7soTAErIraPK5Rdb z?K;k^dU<&PS#ddOfh4zS8ls0?9!L8#jJdP5MS{K6QxJXY;h^2&3rb zm`l=f%?$7S$I}SAz=m8x+BbUO-B9jLh#JOZ_)YX&FNdjiMg!XGa9ZXPfv`34=qEVq z!U+3Auk#EIt!yPSdkEIb{1(*<#IWAWukWT?JHjMM2ZtiA`N14tzZrs{+t>k27$(cO z|E|OXh|^6?kd}`6S}hfVa>W%6M{!%@7@^Z8j9GUBzw^3pn5Pc}N)TMCX_|5?#9*c& zC!IC1VAft>M+hwBw;M-JE=l3yOQnfI06k0|OXt%XY82@(LL$*Od+-Sy>Xh@IhW++~ zxZ5mM##l;jg79$XeB;Kuk}|WEi;k0A`tB#BJ5w$~w7FBkT`_(j9}&W(hWh96(>N+P zi6q6ierUm8P=i`c!z)_5mD8fXD){((`abMnzahMY_x-RUahN|9-iEBKe9EqkPyt?l zWEUD^^4xuHUs=EA=m&xgSnYn*x$gSC+Do=FRbc`Xr!38?9L#`bh*WPp2`HBv0-CfW#1Q1B>rqbD`6i}RR7~R)^7$iPLC)4R3Hc`%~ zb8)y5EGXXDS%s~+Qz~JSc#gdth60bMNY#fk8jJn;20g20QQ5+X+s$D)8LHCTQm6Op zK?S&~?=+U@Dt*@P3OW&$Ei`PF`+VxW?lTo1jS2@`wnHL0_JB32Mjx{fb&qwiboOa^ zqvh+F{jwjT$9=~mCWtS?M9d!*d#-r-_$GBi-|ifMUZi(41Auq{E}&5Z7VXg_%{*!7 z>gXh-QuM#rddJ|(y0BY2w$riGv27wr$(CZJqtR@A>i7SLg3uReRU2 znrp2&?=i+TI>Rlb^T#F&E`*=Zs8zGwzCHd>$lLQ$PN&L~r9J|~H!m--I`jvd{q<=c z4^ZP!C-(|=u5U8<3SH?uW(B;&{#0lL!>^w|?dk2jWQ~1XqaO3N%1Eag0DqtDl*z zmrtQ+!hKK|N^&qd!4mQ}>cuI$F2ZaH3b!3t50_zm&m&Bxx&gpL!cTd%Ino@=#^Q_( z(x-N<{AFWN5SdF0OI-#FqQgkPF-(!c`Ciq~S+WkurAt+);gt0e4T7F6pYHg-EKYJ< z18ev`6XG^KM)2y@)6POkd&w!DfAooGEt~Ce)DP0YvgCZuli0T&2b#@pLz@K|^#!&D zTzdcJMY`R?oD4ye!`vK|dS7=wbbBtTv^GUXeUm2i{Xpx2^aq21GK2B@TD8SeIv!IL zRZM6@4r3T&ZT|SmJgD}q#MUXu3kV)OkA8-?a9PG=i#i(i)+*rJ^LVSJsEJ?i9*5N%+eyniB&f0s)bo@SrSI-PoH;19QD<`UgG`#7XC8MNKRQ-^8c2x)~NNcYxTE z)sgn~Qt)imh~E>S-vrPNyi}+mPNqAM6&ns}MLgjULRG1H9Sq0t@^*O|wx&~}Pl?9i z)26tbLCOjJD}^g;*Q^L=rFd#zb9L-esU_W~d<0Cvktk+4(#*io0%@ z=~mPGYVvtrGdsy~L)fJ|9xE&2K<9HV0dJS>SOmI+39;v+@Y!zV@sJ)~8amR(ZUZv(DA`UI2~BF!;(RyH>2$)7n}!?ym1GvT zXt@R7V~2CdIA)t?m@scIoK_skDBE2!b?`?z)S`%kV3w%1?OUNBb3jIKGo%;)nE}S2 z{2viL_?-}#wZ5uWi|tU3ePh8XV!$Jse)FgxRkUH=5QKx(z;MilT()=(1f_dmiL_c2 zl<8ndUXjjL6YtCFL-)sZ(DL^Y{fumeqr6e;wri}_F9(yQN9JPMkNKmf+O#@VuIlZ2 z1CGu6Ti-#bSag*rPFmZ?BZ#n2O-h~SeBs!pbv_Zs;X=$3A{ByItMFFdzn3BrVt#sl z{yVOHIM+m5hdtgqUhN$po?=yAN2HndGVWGIz~+fDq7U~IJ&pEzfqi{dsWQ~bs-LV= zs$TuU&_I%_?pUs~3+UN}UbulH-Na%}9bdUx&9?{>O@K(5MNfa#x*MD1o^qOuq$ydc(GF6ez7W&@PmK<9he&a2FQA8CArWhVwnn|!M*Cj;r);>JCt#Gj^#{JzhD|s)77`)FkS65pbj9x1v^{8u7M#IjU7G%NPev9?`$F+p$oK3&gDq|9_UV3|2w zMI7Veu>_LOeUFY@c5ivNc!`;+NH$f0VySC7?;mo1csxts=?ge6ZwCO&b?27>i&Az- z7L^m6A-!_UK!_TVpcJ*yi0#Ydf*yO|jsjq6NGl4nf6P4eaN z$)G^XbXhKRJ&rD!XFmI4*g`?CIG4ydEHRFK%r z^gkKoDJ*&|E4qLi^QHQaF|IUNE2>>H-5FHO`GDS&C}<-ln_+c`gsH)9kNqqb$JzY^ zT^8FH9HH;*9pHalCo9Q)z0tP3Zl&!W?wZ->B6{zFQma4l`mTtWCivHyvfPz zeyMy46A`pOU+nPg8IiwAYSOOdsvEF_;}f#ERlUt1`rPIMhyS8c=BO}!?_X3@eNOnn z%;d0oJ4;Vmxd<81pT@ho6H8x3GNukvO?L)f2N=Xht?qU7_XEw?TrSDn%+=>0K~qD zuJg3NM#pFBc2=I5qziKijgycChML6^FkA8lJjD~*)V>TebvH2yH2f7DP(W!OvBD{r zF&eXe3lD@|b${UFuva2#_5tfC3SuMS^_do$R${V}(Z3*8+<8uOeq@8g(uY(wS5S91 zpv(-)q~(c);d%~&B7P$D0%_#mbh|n7@7%xi_~^Irx`89UuS(>;uFfX;AVH7)?wXqLA{s@ql*&$syjE)?o;et#ix z&zG6YLa_`GfskuJvsZ%XRKQ%vvdl1K9t#Jlj&5#jgK6y zGzSc6jL>UjayF(1L#$(}glY`AVxYcscHe|jkGJo^`f>aK!q<3;-nQ9J3z&X^2R|bJ z=j~#gw9j1;M!EajvI&$O>0S@9G4VynR)=K zIsaQ(Qcc@?xn+89%D-f4O3}wqz0khW;Y{wEOs=N^z9u+sed3nG*MYb~;QUgL+7P#{ z2Oa*_J4k(GNPOpAR6kD;j#BobUN=~pXNW81a>07S=<2eb}>%9_)Gh~E)9q7pyy0vrN79(uUvvYS$nKf(SX%Z979om-0F6?vjag3+it z|Mod-;)nafWN_F6XGh|4{Tm}wzcfgWvr`;^k--bI2EmPlB^V?Y#qYu@>`~%y;SKE} zN!zgv7sO}egDPcG?)70;579KqockfCz|-jd@;PbhQ78g+rAk9Rk-G{?qU81=5{ZM> z;FPaK#bcC{y9P4hJ@gIRwH>9R%5o4TTmsY*eemlEuO`BNM5M#+UJ@a;uIa;`7%X5P$jKFfK@Cq}BCDwuhbm;P_ zy-Qc;hblOdp=&kW97L$C=o=ZwU+>^T$UnE&$92c(B?gBLJ&yv($TfPC0&2pB;qI$f zbg;Tyo~HhUC9HciV&Hx^ zhHBM#2<3mEPw*EjKNoK&JGVJEyR#*(3s8f~_KmpwEKFLW$iLZe=}6g$@z6-ne2aJk z(a$3u7_HmmLr$yMmWaPl$fY$WKB5gRs_%!{yU)_xe$3p|URLXjsENkRW`4KWJ5)eM zdxaOKj;w!`aflvO3|y)D{ZMps)P%n2%vf#d*loG>h<)Eek!adW0TCW#53(?7JSSl) zpQTzJ{^|w`!meg#v)1-oR1o96?FHQ<{VmI6Ib9>e!XY-sLmo#5M}*POYI93ABJAM~ z65Ekk$jJsDVBTCv9I1A%Y!y=)8Lwo37i4WZNdDJ;ex=6&USy{({x*~dTk3aWD7=6m zatA_#aPS1k0N*FK>0~;+j>B6C-y0>WVmOqE{m8KWflXM?R{}pDNN3^P{Un&x@G5m? zNWTlP0vzlBB`_*U!LMFoHtSUekDf1Gjh1SB^r+DrTt2X=2qLu1@FaQo8AvwhNf8`E zpx*|eb0Dg^#-|bX3tABi)HBgPBe0}(Ax7t6B?#L5vkMELOezHv)8V@k6>~B&n+0>D zya`BqueZDUIidd!J)ZLNRxVqe8aSgZZRdWeUg(+rs!v5qD-<8S7^*}zB^CKKhI4!< z5D<3Zi?=@rjT*_ITd2XC#vzSZOuZl4mwQRaak{QJoB;;^69bkJC3x=f}2Vt(*`| zfw7MuWMIX25V@?j9TsE%jmmAW83BH|eCg>zZ>UA9HX|6^#e6ZeKt@P7I$xBrHL|q9 zu!`x?-4}w7tt0LIvK1I4WFG(TjM#Vh5E@I!)o^qj(N%M#M>0nc=dJj}t^IC-HzNkSAm zgR>xfwjk#-7|vQ(cLzS7b|J5FWB`!q_g*-oLTjFr028EKM*VR$t3QQ*(2{1+M$dM| zM#K26(}$Ga_dK{>=RcJF0sE~P*Ne03J`p@mgi?0f&sNn2v`8L9(lw5 zzs@dh{GYP}9nzVL8!H=q%!*Oh6sl|*Y=twPL*rv-i~5%lLQ<$J~) zMn7L<{7;8Tzdi+l0TV^tW@D`G5cn#f!Sk^1foydkz)9s}%4_?>b`uQLy9z3wYF*h) zu)!a$Z0){g0*ZQnL$CXuI`?~WY@2!^;OiHAbQ2REc{eU~opWq%Y>dWKKck~&ez|j| zt7Gk78aW%5NCC~i9)fg%U~M?Y4L`3ekH=`)=+V zQmoPkw*H{It8%*tx9LfY-d#|W_^O{AWs>REkralLw zto>4H4gS_kpVZBM*qKA@!WmFlpZp$%!GZ-L)Qmd+RNw+15*M>_JXuJwzx`gvCuFkN zV(_mZH9w^wJ36vtu=QSQ5UnxzyWd8{@LX6ZF4D`C>;Mb#&Lny@+O=ZKxnLs zR$by2yy8q3$f{(W4SN+HN?S-Zwm#K_;(_J}{dt;Smyj~J!EdQNP-cVP z*^^wq@1`1`Dqjh9RFt3lYJUDNXFItWS?;UFyQLO|R^T!Q5VMo;*IWMg36bt|)r`|N z#U!3IzNi2L)f^qExyeCZs~n=|Jm91UUjs1Yc}X(4j6O^EDdJ9D(Z@5nJ@$Qp z(u}DK_7;$uFfe#K4?N{PJ$TFGp&9yjIdMRqH-0-Geh(cxjh$*L{PziEJDty2LSP{H z??(f^>#bFbI8%G?^4aSXj&d}bC@xkee1^&YxX2ScTsbw{=Tf&h6B190T*{WKv571m zQ=FK3dW}oNkt`1iax#4ccY*Phz=ouAn=O|F_6%=xTioaR<-HXm4VWjnB5W{0!P@7* zEN#3UVaPu?`}U|7%-H~XxE=rP_e;|kvCPh;rrU~=93QLgCdaOIA7^i0N8iXsck$YD zlK22_l@KlEd?4AYB6Jlx9jkN%lcms*eM}8jxW(Vp*C0_gPTHDJ$6!X zWg4SHbNE-weGBH|DqT(_1(WU0wkUMk#0W*k;bbOz)N-?JHPIMLc*K?gq|P*xp+KkC zyGBj;?}ug3?e|GQCC8i{s^X>&+0}b*5aRByKjMk~%00cv9>Blgc^16fsOey7l||A% za|ofRKiBa;Xjl-}T9XF;_KxDg5lBP}S z)#?*b7mq%#h@V9UXm_ux;B65U6PLf6|Hf^iGG?A)@4S1W?`o}{yXE1PZn=$Y`{{!e zAgxkGB_}=Dq7nh7dU|Jwhw`wsw9NzogH75}QDCR*jGrKtK^vT9g-BD)8!BRgHl1u; zgrGaY#5zhyv@Pd9e)$c~&uMQFhWHJ-l7(qR7l7HoAjJ@(zw7e^0e35-=SLt;-nl*F zbh>=H5f1jx*%>hKuSaH686}M!I(-f&jhormBiYHg8|V zS;9kXVDb<1-$X?XqS0evx(6%ZWzDJK z2y&mxla^1o>d;>=Sv#-C5#%`(8nMPr~Y zA{RJP!=tNCmdEtEUkc-((6b72%jQHmR%e<#ebEfFpIn06XrL;lA#J=iC?=IR!X(qt zwB%j04V1P8NjH9sf?%XjgOJ9doHRap3`s|{=>E4{S%X;d`aJoA+$rNW3Ze71C_%4DkjCU~CfC11iIq!7$i$Jz#AltQ zMui|D1WRIPv=%YG$>AD3A|J#3TIe!V9oqF9D|*1tPIQmf4NKd}tld#0jIU13<~0(V z3t^UApai{vvkhckK7@C=gG;D%9V|466h4V4wCs$1=Db6*n|?3-utmvnKtT5>xW*G) ziv}_gN;{d#ZGk#-R83q9?JNb@U2Ue?8`Wf|hxDU9z50@Z<1#nviBskr_ID5P|0_zPL$7L|vO(%v#O)#2jO&P|< z3bQtLiZ%yOQy5-+3R}yEP95O0Nd`hAV46H>W>!u}x6UC-a*YBB*mlF?S$|C2=R9#r zEKY_da3$b9reiUiRchDGhmZW{n659HurE%OdPs+3to*ed5@Z3Xb|t=!U!ra}NYwu4 zKBYogsDzP3GoXH%XB6N+F-#1slW;zB--8Ndj!cUcC+4EGBN3pJTC(T$9gaSuPOTY? z5EEoU<`2lZX>|ehm%452GsgXvjg}yDkBFIQV&981_ z&q=s7&zWYgnl&BA+PKtV63i57Ya*y3p`-9EYdQTYPL?TcdBi;7=h}E-!$Zw2$o>zs z-8!l8EQ>HXhw&;JAUvs_-d9GM=`=fQsOn7b&Cg5bu4030{|1$kC+d!Nwjv4ZG5 zA#t1Qb4L@-_~ss+c}nVf)yh>-wOO=bP8lO=Ki{I5Awadv;w)A}h2_|#J2C}ly~}*M zqlv!ZO7`kzZ$5hmLwOeCFE}EQn{8U9ja9QBrcm;x(OMF(B8G+IApaB1EDK2yh0km@ zeMu_CHH0ZzGU#0Nzge`aP^u0o{eM}-1+@(=kvZmeo(JC zJDW@ix#cudBVR*-VDmvL{g5S5rPJ|zW#{K5asQ(-qN4Y4URIJFD%IjV(Ac!sK{^eU z#F#(g-ri|0QJn6G>r_2Z-TG)&qNo`AlzQg0_Pe`QS1eBJJIcRVjf_*@lz%tzh3rzGSs?{K+IH{w@(3qmzICL_;gY1I* zA$-3dav_O2YW2l6i_n4?O^>6osMV^R>n{22ruRdnoLw@8KM%@BN>fPWaa4+xEtIi@ zs}=`g$Q-$P3q49l*fWE3NC;5LWH+&!$;^<$&pay#@NIX8y_j%z(s9x9tlV~-Uc$cL zgm=9WN`9%t)6Jg+Z-=@bj=y=rdT8Z-fZZi=ba7ut{%rT{Uwf@LnRVjT9XOTtp#oU)F{Zcv5U;NJ=_>Y{C;T<{Y6X2f-MN&4pr^(=`ey=iO*m_fJZBe76-2yKg0D=Y1QLV6~SmaeK{1B zW-;{oWuwBEZgTLQ72kDn-w^Gm-o#$%tjJdBXxZjzY;Nfwyn+nYS*0F!@38cJYPcuc0ycaLBJkfIHG$k&ZEBtv}V z{#ZG52+GJE0!)@w<#3Tnbgf|dE-_&+3P*$DvIjp{!t`WJ#S3qgl19;-7nK<+IxYQ} zv5XkHuXbsA4u3i_P8c77mSbWGh%aJ)OK6_b#2_$52KRU9k)kH?^}Sy`Ug&kaXTs9A z)#$kied`gwDtb|HmChQ>io5&w8Yzh zC0x;PUB{Tyu+OAPw%%{+xMve*J7_cgkUnD#cIrWaEc)# z>4+>h`WLA~e&rC;sR?;n7KuFD!(;|^S`DGkJ0|l*zDb(SG2jcTFk!yM>!cM!BYN(Y z##MKNbk&*+D-1d)5nGIgqdoRQFsK{l`RiD>x?*7#N&?{bCa3s%EL^`)?0pPQPEpjW z4~$g-GEFP-5ZVb>vIUdCKuz>}aY2@4%BfTx3A592G}=GRg2?oVRN4i`V(vZYVJ~xfin;-k<#_`tqgr{_;NWIz>(ptXRc+>TewA zynOGB-EsJ&4Pul~=ORNcPKtBs6KXu)(ViP`aor`s&@#j6Y)+rIdzA#bnM~#h`X?*W z$UI>E<5vyo>GFOuc3^@Ec7V|cAnLs8OnZ}-;^}rBQ~=R;HQGL&DgaVdn>7flq&uf3 z2V;;hPL)EaE(OSxVHnfHH(f8=JzrOLt#aBFh6x*cQn_L%b^WA|l#-4ddooary0Z%# z9{}|rD7CDRqihO%Z8F1bS{(tp*!Y_xu*}K?Ng;=6-nd;lBFw}dKy$^LCEGD>mKzR! z&8)ZH_?&F4`-x7Owx6a6IsY_n7Q3R>L?YAIU&Sk#SE(>ejJtzeouj6RG7zzQbjF0yEfgS*{jf2O-Ic(taB8TACgkxsOHZg)}uPQ&WLJvmd{M!UkZI%1`cxOk`6YIE#vVhzH zP@NitB|`Hq^VcwRnrRG7m;y6M1}u(iob(fD%Hel*))aHt8%NCkXqtSn-DX+tJ(0JP zkwvkbdfbN7`(DGQ>D`gGKJNyVZ2G)xg{&_q7o>JKgGErd{VW3kt$ug&&A0|IP1cpZSu=6j8c;{%Wpt9H!WtvD_Z${EphLduM2#z+chrE z|MGe*zdv0F2aJTKYJG`v-fzjP|3d+-x+y|&Y)x^5AEJpq=yzXj_5N(F0}N;2JBdN$ zI5R<5LGW^2t~FMC9@dG~qGcJLu%+pMB@D=rgR#&3eFF=k@p;$nFlTM4iw_yd_4!L0 zt1FJ#Ki((w{t8BnY68KD3qQSQc3ks3ryFTaq1&b%L%X@XJa63G0xW#{jc$8n>yk{& z^UW4*(}&71)*dGZJywA~Vq#!lu?`A6!z<&{oK9~|)Y%91KYtqN+Ar&6<%>Zg__+9h z?060O!4_eN7%d1qOpqT(6v#U_N;NOq-}r6$9&3fYyu2c8-+t5piSF=>gONDc109Zr zwEHeiwX4U48TEQ&M&mzBr3=s<>I}bsAxW5v0@tfMm+7i?v-z$;Yt}EYNKv)OQy;h6 z(W!aBO($A}y7*r*PS5Q_;!BS2V2&e_rGp@S4B@p9us@~W=HuiGFPck%O45a3eF9C%qJ56yAPpH6n?@@MnY;GHM8H50hpf zBqk{-O^e4Y%b0GKrZdlQISoaoi1Hi*OsMoanR3Is8dAf_suE(RV}8vZL7MSNFVVA9 z-(g8gbkanbb;Gc{IcPN@B23gk4iO^;5X)28BY^%g_zCnJ`4}bm+zs8{=9?YwYq{#iQ5zm7wAu2@Sn+*urX9TS^zKtn5c^X9S&xp4#=DwnGP$UTl7W{8-G*B zNT4OrK24|^r`X8wfhw&xS}bgP-7I0^TQ*6pF)3K%ce5t4kf?7jY(2~lAPVk7BU`p? z=|&mF403(yGj*XNghU0DXja8wF+UGsO(pJ6+olgS;*s4HtM=V z7ofGr9*xC!eu}vLa^dxvrIwQvqIQW9tAzUdjWva}vdsRP(p1&{rSz<^uOx-Yf@E>@ z^_Jx}E6eWt4B3tw?mpWRo?d!C7a=TN-|yyQ=VP)bdlRaL^{a;yf#iuq6GvQnK#=!q5q~b};a<8^&B0m}77LfzRu^2=+wHU7BJ&DGEO+ zIZAXFWb{voOxAH)(nR@D+ru%PZe!_ywcp==h1`?~QR;RB6DhYj>WujNO{q+XDqoVL zh3>p(y|*m1^T4U}SeP-H)d5?upLtLd^BGW$32n%V<-j5%BN#omZx!9HtUhvfyvQCE z!y0*1!FJ}{sq^0bTCca~6*ZwP3`<-PoO4_jJZF9sVH1w`_gsEZ-%%K}%TY!all*vq zY0pXjnepl*caw|yh z=zF_tY*x#~3Hq9fdE9QE-QTx?z*}BBHL<=w{3{o}T;!$#h1^W!l)NH|{amI-FKbDD zaG$RG4&=Hm_v`+pD$P9DsMK;~OIH!8*xs-&kp)MGWK_e#8hL8TE^cei1u=`6m-Yn; zjq3-WQUln=g2p|BDA6A&1oeWbU|?r*Mts_vNI%AG6bJszF^2-$F5 zV9E)r=P)|GkE2y28y-(;)Eh_Wo0k{J?SzCl*nnd zy61H+t7O@*Y{y1zN`)OLyvK&gu$J^tmT~S>F?K{0B}VUfItbWxxkKCa&LVIRpL>`n z>`^lLqZ73a1ozH$T!_G=FyNpTv?Z3ghnm_N5KbO0AuLiOiWFmydrOVRKipiGR&{Ok zXqPXmFmLeXV<;gl(KHrdgBfyv78WiFlCGh?d417jv$FfVV^ye@v!*}LXSD3jhe+Ha z8hvLvn4#>wwfZv4@-JVsrVV1$;Z>uau!?|5M+~V)5KyY3s-+OLIgy}yf;Ao{5qvDF ztZ0WUq)jNWqvlRBk6BJ?ZvJo!nh#Tzx4bl z=a>_FDhS8RfI!?$gYOog8>|DZxzn%pOi zK-u<(trL*eVf$x6vOu#jDBg}N4E876^SzAS`vQXefU%)pT6!imr>1ZImxe6uTs-X4 zU#cr$vZ16(M2!z7D=OH!f*7+CGj>X9>Ed2H;hoC)su)1;`QT6-mX z88g}M88=(EI_nz*?ExNeyXAg6suf!aDH(~V;o9}0(ZT(G7$f7WMP05WlQjHX!p}D0 z^_%!x7%}k~$zn{!OxDiMgPkGYD(Je5O zvM=4BC&&r#5vI*qG+*G}kyQTe?=*sNek%7sog!7nR|Z-Q+g$JBd_j z_Wi9SeWmWM@V#O0+$%cCEjp6|1jkp6J}w&&y@A9y-%AmzvDMeMmXylKsIMkWH|Ef} zaQDVnu~ncOsrV$)K678S#eQxmSU-)&Kl@ATsyfkfP*;&NaqYCwO(23cT7x96R6exH zg7fnFA~tUHzu3UQDIsEONxjty7&li|AP+s>A1xXog*6oi3x{@+=P|4f>hXLNKaHZN zrlSn=Uhlu#|NY2lFS9tLCOm(YN#n$R_8!wDdp=yn+cVPVH3Nf;8 zlkR;Oh9KiUL$K5mPbx3$5CJ0RWp9w3aI6)Auv^X0nhsqN=kP10Am49G2Xiyw3!Jt) z5Vm?~i>H=0w4BiQ%D?D%E|@AY86z4^=Hf6NZ*n{`NZu*7$Z`> zI*;f|zbzd()bA_Tw?4&BCA}}jsM{um>X`0!sK)n+`WDuB$bPyo$Bk$p;O`3Lg;I&^ z*U^^SLH!_uL4%APQJzNS+@?AtOUo#t$a?{?&mqxw|1Bo+V|inLFx=ER{i)O=VmVDg zjs||18Ofeie+!y+-+q6Rw4&tMrJ8fitH}HO{UVssy~!MY)liDkVC*fJ9Repx@I~#K zaa5}4`|gY?1#Q;tvV-@NVzk2W&C2}eK1TQLQbl(peUJg(Z4NaWH6IU10V@l9rE{Rr zC$W3>lhPM;sunsj6U9?tew|aZ;p=)SB`M!uVvP>8eQw#hb+DTEr2{WAWyF?qOx zMq{v6K{>e@fN5fT!>+>yZ0@Ir=Tl48{G&9V;+NC!FjN#wX`0&%hLpwyobeyG*zIjw zuyUd10mCZIa&MeaZV;;10<`c2zqz|(_O7>WFTbmu^Np=5vp*3w`QRZ}RlHOy7cSm& zWb0^!?6+$LBw<1sFl6pAU9f;gAqeou1G*}<%|Z}?@)iQXbXc0V5(ySH1=UR=JB1?j z;pX%5-0S+y%=2B$MTO-?z-o|!prESY%d{V!+Lu(QVD0jmKJOBups*+>R!8*>$OShA zW0b1(EHnlOzjqf6TuZfr&Zvpmi#nlXR%Zsh|*f-|)QBVzG% zHs1fdOsJ*imn2u}?H#T31K#?ND7K>g0B~CoO=LF4Vo;syG<+@U-jFtFUSaV63=xIl z&2KFjErgl2C#M=WVr7g05N@xdzY)VqW7p#;C;H;eHJkcat=;z^xBV6o?b?yq_Ybc* z&@(&7U9^JL5u`j21L^ti{O3j8FJL zrY!*vueueY%)X^-)6oFQG)`NbT{~@ZPoh6VV}xJY z`f6;W4*+fgkh?JU1rQZKMlh2w3+Ao^yGI<*YC^KH1AZ+o91HX_TSH9=+UT=qwb%~3 zFyQ$RU%0cpEoPhs$;&*;S-6tW912I$l{mF=jeOS6KILxs>!e+q+A7-y2_r|L(`WKp zxdO8B;c#=ft*E!X00Q3#!Oy~V$CYZC>Z5g?BhG%dzRFL0HC1xSD4zF_uES6o-%Hy= zUx0m`3u$~y`3{ij>2a3dz1)wpvG|@nd?~Vwc!~4jC=1Jc7?_SbwzBUG*tp`5aWnl& z`g}q}s5NJmTCB5N8*?ixl)nANF8of{9{fEU-(>GJc{Zpn(4e?W?V?a}WS zOrs)t%3;OS@b|V)seAbLcBSVoMsL$|qZTAA*Q+7?9sV}m;^2%%d-+W43qE@Z8RQ)ZNIO*%_tMA z9=m#@t6)LV)Aq~CmCW9z=l!$?vkr-i{odU={N6?;NS^OPq?9@wFq-X$hD$IcuIptq zwI7#;>@cn-{QRu;U-vJrk8re)&xTyxvGm|JQhQ7#-_1IDUhiRyynPj{BPWe9)Y$2h zWUP&mLf>G`BH_)ouk^n0eYAG)KP;P?>vjem&`I?wxAnea$SBdl=8cmE=|Dp&@c7=( z2B;z|TBFK+LkzY}`QGK#5Jf0Pe2`t(TblvQ39@&O;)pblY#xndiA%D+iKG5*y((tZ zFN|+F0ebp;yHnuNz5Ay&R=P8dmWYPS;O&hiuAyBJE8iwb3k|Y}ZmlYr(2L4&Hpz!z zhhe+<)nbDkwFNF?oO?# zNzUXid3aAfk~u%!uiMOPrcE@3tymonWP+53qts3$BlhlAn>nLgBs1LE~+;)vgG5I)8G4(>8&{PEPOnIhdWkMcY_OB_p4{;%NA_W1vrf*fxff#QKU`6W>3RF0 zbsY43QuS=I>}|BNV?zG8!F&!R0$8~Y0m6LbthPoAEQpE^bnVx}bsKsRzN@9&&!A(V z55IxE>;rxD3**Wiz1W@Z^V<26Tb7O{r!Q25B-1ud)!91&R^n+8(+9^fHQXe25}L8i zPwi=Ny5vyMaJf8a_J7OtQuPNGtc;Nm#yx6}Cd)h`HWj`r>AvH$?0CyuD$0GCQ}e^U=w8>E7PgI*dBB7^YFA2NzziLv zYZVSfpQqj|^FYnl-LUmDSDv>iha2<0J9$U^ha7gIG_#!I*2qjlP)oOb&AID-4lh&83YL zOb}YP)DS{MFzi^lyfuZb*hTljh+C>C&Ua)e8M47i$Jh5jc>4h*f?7#Y$9|x{r&l}+NwhI6Rq9CexUT_RYlxI_F2 zQlt5aRo>&v_B1sHPxL2g8{Fn0JcKQIR{t)D>S z0&ciBF4n?ZyggOJN8uaz{s++@CKTD8P^7}^yhusk{6Hl;FwCqc{jcY)-!bFM2vMtc zEx27+HGEJ2GnrYJ{YKJn7X{E2mFK4)8tt;6ld7Zscv_xu~l;jY=174)^;`9~Vf>#*ARQK1!|C82ZY7?rj1}=H=e&jd4=~)z6i; z(u*4p{?N1s`7=m?$|L)qkaZ`=*CUY%!qe%X-Mt>A{8e?gQqOdEY|$vm+Pv=dNL}iw zc6ciw#WH@FmikglQ>uZyO;tZvgJyRwr99|ha`xaDjU0Cw31>Ju5>pfG@>_+3KToeG zPX~Fp5cN1_)+tYoJ~6&z=E8RYV-BWLx9o&8h1A>i7x8&&)`-zK4s^|`hkL5@ zK@a<}ooJ!j&(XOBX$Ybo2;ZpHtjR?VOQHZ$QirJnjOC-Mxex1LRBE^J4({w^LoQP`X>>MRn%cD?u2q!CvpmCPR22ve6i~M*$QV4Qm>6E})BuIp zCv-xSfHn@A(V!CYvcH14=W+XWQh2*T!Q(o0@$#sP`B+?Ef9sWL2kLJ9og{>^oSS_W zLdvk1`aluWV=|a(_VVy={D051n@Gk9B7mW)-lFVN#+Y$)thHy=A8xa3|BzcgfaLD{jJ(u zYWb+Oq#jO6vjlFp5Q~nn#)ki>w^;7YkwI%!plJivaS=tnuii*=;Raq-4k4VujaC%D zp8a)XI60%|$4$EM>6C?zYJ_c-IuScb9x@N>n?49sTSPvXyV8g`d3bkf%>8z2A#mc{CsLdM@o0 z%j)1XSzyu6{}dpNh*h~!pM$A%^A8@kQRYYkj~2 zU1zGJIlqqxKlU)XOo0LCsF?f-;;G4to>0bvdg~EM z$g}-Fw7q3ioyoQ?jJvxA3GVLh?oM!bg1fsrf#9ye-GaNjYjAf9xhv`J-PwEhx#xaA zzQGv0tLmLK%NI50^H39+XAzU5Aly?U%KpOPT`41Qe346S%8R2|O-P=58I9u^585bx7(@gE-yPGwem0)@ z7AU%f*O1Ua#_IvavV{_6x|nKUq#?O^c3|Ukw;P+f^JI{$pP}S>{H*s~hGMSv<*^0@ zPR^g-=&oXGDMj62Hi>rxaEnhvJomFmJfCe^?ze^w#-OF-1Z3d-$N{BnN3S)OFs^0w z>ovb(I926AqpvRz6D9$$y@rU#wj@0DnL?qaWj3Ydu4~qWb zC&;o777#;-yz{6-Xm{kH?4IUh3;xA`ZLoTrUJ%lKqdfYKB`a)BBj z(f}Kmc*GP}P429@C6d(W02!A)E5GmK${i*`*gO*YigDd{#}IOj8O5q99P^${Of%I& zPb~5^?N&g>2I8X}MsP<4CL|?pmLq8{UAM|w9%Se;bQrNW2b2<1@Oj3Jc2-a*tm1UB z9rOhp5lyI!&5P~SRK`}n^StfFy@_vCA&uL-ASg8*D8Y-cp4CFwrlVRfG>}(`E6x^Y z$ho*CmHfA2mhgO*;q-)d-p@wlJ||78R5|-#_|m#r(H$|kquV~8^dvipSjDd4&Ge-c z+GJBKp2AfbQrCuK?bNi=z3%UWz&xex=^`6M?v+s0X!WJ-7`#tqaV<@es2PN*dLC9#p|%q-E6Evf2AYn-YAa0j-5 zC&m}oUS z5I0!o^Um518$^mlCqzkQ9;vBu+qw>#nCSpf*|on=)@HA-k;Kc zyrZHJE`x|dgLNw)8t0hayH+#2(C`2nTPzDo5}n$KHMba%5NP%22`eohBwG{aah(l< zRB3EI=aV|(sf#BOF)pPto`wJBzBLi4kWPkmE+b#-OLyQ3ok;;^Fw=r`m_aC%1z)?_%_HDmIo+jld}U&3^N1iR zK$h6wHEb7RmG2UV z#Isl0gNEkGo&!g-fwUni)Ok0Q=8F0sJ+uxtw_&|BXH`(|h>8(nqdt!&n4m(XuB$!} zqB$Z9Q(jPXPLJr3ebP9_@wh2~6l5k$*uie|e7XV-1`wsa;$1bfrR9`h74vG|k(xHP zwucEPAsK(EPvB|-<)qhil6;jY3$?2d`MS-~b9#HMFYReMCPHq`^I5fsu%ky4+{?rnGzWw7GFoIS_ex&{YipH)Ck+`j?% zu3ViTsr^iy*9pg2t-?^JCLrR|M`vN-V|fbuzKxSTO=vrNkL>eQ%6^|d8-6KC6FHy6 zuxS2qu2qa+%@KB?5!r5kpd6fB(Rf00Nr-z821dVUsy0=WU5gQu|Cz_Drvjh#8{h_Z z-K0p#J%O_d`vt|Mulq5nyF-ONIEK7y@Q_x+_Z-?zh{55pq5TnD;HSa7i@b1PAfF{@ z?N_+1NzMIYwO=d37tB9MShs6bmX0p(^YNp?R!}y;^IzSP^x;iU3YC@0NOkD;CgBm| z!g7~+qd$9wL7qJ(5J)RTJ7)wJa(rX%g~co#4EVl~<%HuzYNE=Ku_dUl5VGqt>7AL{ zeXy5{Fr3UNC%U+Joe#DZrYW=siGebB6w3a1U1a5QJ<2d4ZTMiNY`tYbLaZ%hF@m;^ z(EO?=sDefIPd>h%Uc5iOV@iyS%!I6eceib0rW7_zsD9Ix0qJB&n})K*Ve!Du5W1@kLNf6kC4z!tpi4fHRt6%?TK`$lOPJ zG~5(P<4Ww!cK3_%&zl4fK33Tslj~*|-V3cjaj${kJu({5esH-OXxj(I936h1{ATt6;< zz{VWr;8o8cZIU1U)W{+`!5}q3tQo+hmgsm*qwRH{W`f9h!xY4WJ?c2IV{*B-M!p-( ziP9MruaAv+0QK0y)JbL(=p-2;FkrV=gY6~921JptMz`;%Y-~+ylt950K)dS>v?VdD zcR53^F*b=hir7CfrbUNBQXGa&_CYx$IuSK;dva3tAhLgq0f9aG{SM<&8n!0hwgZsq zLQbd<>ycv z3v?iz6JUk5@1f)R^gt1{oz4i}AK$fl=_>W@(3nw|udRKM2F|vkq(8iP&PMPR*7KTF z>RiA0X5e;{$dTa0D6GqFYwA%X(l<1*WB&xVpB?&00Qu(p4g>j>q%({@NmOLCNQO`* zr9pcLH4HpWC`8~fJN*%{_pM;LB_(|25qT;_1Ei1$N5JPD5Cs)7A+)D4_0)wo?0`7v z(|F``+NaEy$$&R+^=6s3Aq*1V7$b2Jre+kyiPQ8~Ef5w$AW;F(V*%VlP}HwrjbD+Q zfe^PqK?Etnz&`3TZUvyl0gDh~qWLxCU?l}q>XVm1IOia<1hUy&_IN`Jc!3#I#D4L@@qkBNUXMis1)~ZIf2CvhX=YWa{!t5TtWa1#83uyY9 zY!B*%bRHz%o4SRp0mJWKjO>Sm%#|0(fQ->2CKT^R0*y>m9*0r@Vj-{+_pX4TB|;*; zct{%!LR6GdK_&@ygaAEwCod&eEtf67Oc7EcvxIL>b&kRk<_=j_%qnkudg2hsiG~Y1 zGm=hBJ?HZj{$brA@-4?RIz(WuQ50hgmahyR8G0Q^9d;e=Yz${G_CSweX3hp_T={J_NdL+?I;^a7g31) zHZRxf7p)jwqgXu3xyelkxEdRyoqwlob#O6vF0%cyfAYS_>_`GVZEJs zU_B*zL`0@ky;PP|^vFkaO8i{hY0$%?Tc&4&XXj^bRa#Yf)y`wGdA51#va0$068Dm; zd8xUTd9k^TQsz1Dx!Jjkxhkx-URb9h4Q%sS`dZm@o=~=}nBE zC^5ceEOKIX{_afLde<7*%Iq=Vq399papv)G3;j&ZOnTdz*$y_X8oAD9 zSrQm_b$2el1qS8^4&{y;%uIh}oUU28f`}Y|F#>QlT!pyZIV)Yb_HP%KyERX}mYy&% zi=@+~BS-Vy2W}p3a1Nqdj$0f)oahwV!ZnFBb=WGo>bS;u2EKqIdLw3uCk_S`mztFw z2u_;mF6a_hAyh3|o83#@Q`+^dPPX-VPKEBEep5bk+*F@rUv8Rr9huy)3*Mykt@n-l z!S(|eSPOW~Z{M#}VDM|QzqepP4!GdFAY#CHfM_=_k-dce(0yFn4g3M@fopKUl;6}@ z?n6&hZ%VK?K@2n^Y!$RGmI10F(sY+!tNw663V0Q92k|KJg-Du+WS&PpdOm92=X||E zx51=A38P9w^#R?8*)Y^T=Ru?0{N2wx#hA}DYc2LX-F&$m+t0+iqVAD&Gz%ITosW+w z{iGSB;waPMjN!E57o52n5EHjEeuWLv#m&~|bmyp9kam!E2^S;u2l6B>WXa^bQZBL^ zSv+YwSxWKODqk^FV6%#!ix&zz!xZV5i@*d@Val ze(inJrR%TNtuw8iqg~yo?^Jj&@~k>lR#@(#eo{rJ#oIYxH}2mM)L>!dOHofpL+{l> zXR0F~uQr{YS99lcq8*7IA==N*)@)s}8n!~Q#L;YVUQ{MaAnitqDP1W|EwyR4Z`=8p zJg%Os=e?3pZ&E#$YdxtprnbFsvbxrUPoYjXNl&o)uC~Jw(nx$bCY3w&v|YhKU9l;$ zTDn$R3;V;ej)_7;!?Cxs)SX1Mx~ylV=~>arVw-zg)#=sJtXi?Ey4HhjmHx%JN7wTN zM1Dww_a|@8mv?Mp?DxLo&#f(z`K9$oy*}GMNB&`;-s3Uj8?5 zZoaeY1J>ns#Z9U2Ktf4E-}!!iJiF@^4E2nyj;Z%Hd(^RkY{)M_J^rx4IiEa3dKPo? zjt|GJzG<+qc-M3E;(=h6SIzOmH@9Wzp71=>Y`OjH48CONs|63s*%!);H8YR{DiO^&vcYnqOj-c5U1 zGaV5x=?}O*EZ=K%jtP1Bj(NE*^}7SPjn-<&`AvS@`m`SsJsI=|O^DpQUlkuD^CqX~ z*L`$*m0UEg zbyM3FeI2)GPi%g)eR4%*Zn>zrpyvPNbNGaHdv#tMC7{dure`5BJ%Unn=dk|aGKX1kGzy_2(n_3!89--5vZcM&BMYXc_>7n5J{-7RcP zoE$8SelPr6RQUfWEGVf=Xlh{X2tefp2^oJ@|4+K;jSL9sSQwZ&e^dIaxBuLU|5o~U zpMI)h{9Vm|QUv}Ui~m;hKemmTlkMM>{)X89-#h%Dt@(FjhJ}#rPh*CW5iq}h%{wCp zA^Wd6{kb++2m$8JPpSgoGaDhtUu%GggYXYp!OwNVNyzb6Sr$e@&cDh63>?nC$^sS% zBg0>X0ktwR{8ckM10eBNaez_92%s&b73E|lWcj7#_X+!%@%z00q-4>LQ$ z{;(Nk4E~e=bdX-fPSw`J$j;b=knN9csBU5GWbUZV1lVXld;I_J4=Z3m{;GlV*Kqum z`s>Nc2-xJmNGU&mv$Fo2l0VXaNdUe;e#>PBOaKdDu>hVrzmM$CuK(xA{yjAGf{sQ% z=_%~2Z1lng_7Wx*X6Aqt0|)(2nV)ZHnK(G;#jFj?90^%|jjPZfl$W3Rgg=`OD5~@8 z1%Sq4!bHgS$J+Th19Ap7CiMR>Rgz8y))q#Bwr18Qgbehu7LJYs z#72#fk%f_7QK9#f+Rxf7$n}%xp{m75^gAG-!F-f9_A) zeDJ1~2HX>0YdpM}LL+@$Y%KjI6J^95vm2L!g%TiYg@OYIO-e!sEo=H^@f8?stezp0 zGU)T?{8>(+LuWEN&~7$YlHMRMf#M)aXn#|w5Kzk;$6*=DLU#Sli{AuYnp4nj)!mv>^YmWVc(dkxs@9 zy{bBivH9%ejh}_7s%^JXILLmHdv)IKe$q_$gPOO?dsT+mZwF#)=YPPjG z9@?lP1@r!tZc3x~Fs7(b@CJnClq@VJqM=`Kqm6|UbB9%%WI{rOKHGJgP?1wPszk|~ zCgf7qxH!SSRpG+R-G#4yT9)%@&l6z7TYR@BK12@!Va=}IG(23o%@pNwwF*7qey(w0Z6#dGd#=v zCa=#y$^{Fe_3FJ1OG;O;nW&W4@}#gK(MmrGBZXR6a$IT)OU-8gQ`!# zrw8XVlf%x`9z@|#Bi>vD(*TuvQ|<{4!V8c5jxXgp;I_K#hSK>RlR(BNN56 z&8nuCts0v8iCJ6j-Kyf{z$3o5xV)l6b{JIo->!afnYBT5gZ=K#IX*u>-a`2@kB={0 zSYvK`zWt;fr8gd#IhuNL>yq3`-2pC`y{NgmlH>ySP<0C3Al}30yISA!?M8gu_l+kw z`4bX>0iD0BV>lBtGcC`sk93W-Aw)_h25~hFB1WPof@&y{-vHKEzy(M_@*3uekXalz_D-qKKK^OCaYdV8Xw{jyPW$gF_a2-8y0ONPzL!h_R}LQ@z@QvI-G zjZ*bb3GX_^!H%Hm8@a-45DaYuYw*LV-ibX!&!8m)a%l!;#&_V8&*m{`@_{xsdoZVOkp*b)-J6;cp$GSz(H@JTljK(0i|xS%VnM2qDFRh|l*VYN z$4Nj;!SReS_6u19F+CGwvLH?@m@%r&RBm_)15z~XDW||Uo`s5n?K*ta_O%@m`J(4^%t>&n-WB#;ju`V`CY~Ph0t;3=Z zA|3Zdtd`8{NTq6dBgIegR;*pC!W)^|L0^9Kx-~Da^&72c$Y$%Vr%`?vp7+Pps~rBd z4{9qPc3)lz#`D>2nmyJS`AsSePi$ocbzv*kjm=~YSPhq&fcG9lN&ULOo7PFtz?VoF z0@&uGWu2+-+oe<*4s0n0=)z_y;7mU(Nve2B~18QBLl#(#AOx7M!(Co|Ie z6c9|wVGctZp~E(m2U=Ug{uEX)Vh{ZOfY7xhUY;VYI+D)Mpg@sCnvq>R`hgt61*Crb zBQygcL_>BWb$vM*HR&h+))@}>K1Q6u7~7jZ(*hldg-<97h`d}f$5lm#@v#_N(>_`n zxp{W_L?3!rJEOaW;bH)P3W2E;L_Lg5C=()~=+sD3(=$lVP2RaObBt`^Lx-6}R$dtR z(3l{Z4d9uKaHEvi3h}APy%huBu}>QRs&v9dz3tbTj9F4>~Rsu)4{1 zGZh3uO$bb9lEKLG3j|X2?ny|O_M_-BqI_lsCI1Y7szouY@?g*P0_3qdpcFm>jHQaK zY^J4AkJhB_;&vfOf9Mt%_Q~b(ip3l-6nz=MMV2l_-q({6LtVGNpXIB3S9ln%S6Yk5 zix)fC9Ln8i5BNooD!QaCgr%Uh2{ILG zEM#hgRT~{NPUaKXBQCN3DIqV@T6hCVimX7|3uEO4C4mJyyISr50{MDvcfQ`92{;oL9l|MIjYj1(d6Vygj34zt>12XF_ zI06NoELFZB2&zGWZxOB89Vhv48-4{+mqq4kTfz{|`WPXNk&r;|ggsy+(>s+^g=?2a zWVNOX&}Fq$6FpRodQ*z^MU+`O@#oOMI7!urG?hYJYtqLV-4eTr12+w)e}r`tJN_7g zTOIts2g@)RmAQS4i-v1+$VtB$rWZDqDwS?MO>FrVkmG)=#ypC6Nsf0Jh3g(q0@a*o$;UeZ+B)YO^XURh7W zyIp)>>s0HUJE*)NJ?V&dl6CbR5`5-uzpQCNJ21?az$!Kq)gUSG35At3p@@d$+=rGcgo87) zq(S7)QdomdTqN@Ydtt#0Sa)aLLte=Y`FKzySV^0zJ<6D>+r}80SRleWP$3;jN|{oJ zUBn8V`o;)I{^K_C(27stvWk7q)>Z&4I40&AMRN-~y~mA%o$l?vD)Vr)^m$RUf+}Wi ztAr|nQsX>CtWt1TPK1zkLV=}(Qp52%Qq(3Ri85=sXq^@d>daA`j|g(7Xf4>dgy(Dr zM!06prcr`h|NDOAUBtrhx98_>6;V;k>dsDnMK0g(ews^j2?#DeMua%{k78yF>oY3m z2wqqOGOB7t5W8IL+6TTWN~lAT2y06lKDz=e^a{!j(YN@{ag`3F+NS0h`)se^%h#9* z=GE5d!j0+4%8z;0Hu}sC8cKcN0vv)Vbcn1|jin=Z^-^eyb9zQ4?1;Q2ihHfY9fWpp zh(1Q|u*`T$aB9W)L?!cgJKP#sccap9;R6Y$293mcH#&Cv2L!^>p?z5M$HaGQ0j1}< zbdW{1tk2;PEi{Z%<}#pkRE^`LWn_*^Nt7}yk!-Mhq3jcr!sMJvlnN=~>LJG7t}apY z#|EL=EBzK%oPxXPP^|bRV&pIoV{ag!zUgEK$Prrp32fY~LOIf`4MY7s$cY)P7Jrx( zQb1c~LA@=w;ZSBH0uB=4E?Eiw8OjM(L{p+jr+Ecv+r&@J>iD#VWwXc-t4Pns5280TrqmT%E1!BGua1Zf7%SX(0iSV{w!if|jf%oHcV6=Yat zx)!7N!63^i2h~)!|EZnC73dJ?#xQJg*}Z?{q1-v4wRP|K8(aX;N7bC-b+0grt?GOM z#dq7PnFiImv`gV^!5lT15f{W~q9ZNVSIj`<^E(!PDD&%%cvX1$&M}fwqy$3LPt8iD ztsH^LsJLT{f#I zidPn$)v>d_SxMt^QE?M@jGI*EhTMad>sl}0(W*XME1E;(dh$&hW#%067CG6$Q}zed zj%dv;MPx(F zs~0|vqZ;NYca|#4gLnxOc_yB~1uH#6(Q|CLg^dOye$3B!Q@S@$$R3Ik<~;t}|P4T&62I()d_a!sACA z%JRaP8^Q@EUI<&wR7;Ffh>S~;REALq{usKa|^I*sRUV)f${ zac6jB?s};;rFF$5|LFEmU~W4$NsoCGMVo&uL12j@1=gS)+EdwrhXeMBR&`-Nr@dN@ z9fWkI9?D^7GnHJTRrfun#ryY85q4ZfSTsXZqR*qpGz4cPCtG8+1OZUQkqSaVfLmS# zUBgI6)G`FOy-8X=(_ej?W`2el14TVZN~PX68)41XcdZYo3PiX9pY|xiGaG?%P(ZDv z%C;k5T^j5x`kZ;fhcUViM~3|x)eTJ4M zh2mbUedIy~Cuql0ws)+`LIT1LaI<;MNQ%TFyhhPgEB*0JaKJfy{*4{h0;?5%L$5bx zbY&;0iBnc4za3pNAE^OX*ND3dHUTTfWSx4JkdRkTp}K^gPszNZ=7u`wT-H8y<^wj* z=$0r%r1+91w5jN9Vcq#CMSHOETZeq3=JYES+kGm&UE9)9)pJ*DlP0+qlkU8iQLS4& zsU+qBqWDHBoO4s#L!}nqmXyU zCw|_Q0>q0x1Ir}5nrPsnq2s0d7{r-AO8+G!ZN|HFr1&B&^zq5X$EOg4O(8N6MFDvT784i6?-Nv89qWa*xnMQvL1RBc!RcG@zbe>2iruiQWsC z@Rq%}E^}mPC&S<} zq(pHGD9``|b>?BteoiSro%WJ-p#VQ zI6Hic6FF|KDYpoG-!_*;T12FASORt;L-z>vm)mhMmW&zV+_ zE4%A)Awe0{ekq>?WyLqzw*8?B|JG`$r(Hj(N>Ei|eFoq31r}#FnH9k?0_|PU;1k_8aaSkO%)khp_u(5O{A$JvG{o71BI8@8Xge2*uofhJC&fcIGyX zrA7ufKS8m9WRMxTD$~%og;DleLVHZ7RikMi<-SM1Q&hzqrzOB&$`9@?mtrd)ttkK@ z?(g@}lQG<^U@Oc&S9OwATg-mdHQIc=80`_g=NVMpXpcx~M!skXDxtn{t< z&hW5_|L%ZZhx3a(Pe&G~olg8)oPw{(K4!}6X$2hRgLD^p+v!8U{L0%kAmFnpErz2t z@gl+`i7y9Tm{;nH-TBSMzTIcHgjLX&j{9Fj zKWV8l5Zp~t?kxsQy~h+)|8&}0d#3l$)=qD}-5)#(HBdN=4ZOD)5xs!s&}Poa9&S*; zRVTTFu{RoS>Lpvj=0nyqG~W~(g7w%x*S!o6&g+X-xPW)}fH<8!VyEfr+4*DzoAz|G zka6eOf-xTa16cW55a%DDsb6mI|ILj`$i%|(yWdnNp3$L~5J}Wit{Xmx1onfu`RRZt zQyK~8SM#e92E*zPkN0nFwaNvL+&5DPb#0ai#k-V~<+h#2D8AIPDDltf^*S>aOK!^3}LAdMyZXZjJq6&e#PJ268-9r%&3-E zArg-X41G8ikjRGDv>kU$vi!03qyuV+c?X#sG50#|!op2=W6e{MYs0FreZ<4WyS;&P zc9VcYdRrSuNj^5)?dYFI?k7U7P8 zE7JbS)<5#u{{aMHWB&Q>4=GjvWc2SE z{!Xzn|HM-Ok^uR?<(Pg!dklnsJ^zwtVFMsT41bjP1!(*mc=4YY_*z4U99}v>NymMQ>;BkUnq%ipdRt%Ekkl^8?ACqmhpfJD zo1v@c9pZyAR^Ats#P>%ViSqsReB=Jl2zb7)B?~57C(i`@*JHqyc06IC{JhP+*4eg) z>Skde8Ot6>a5f)eKT6^8JkOP0`|$6c;JX{zxv#Y)#uDIjt8VDUvQoHMpB}zB{7_x9 zjOD-DQ~vgL@%S<*e@Z{et}ooUE?j&h$R%oK5fC%weyM||SwDqtnk$_UX?_=!z-GcI zgy9lQGeau#AcH(__c^budmA>&+tE0J*2g>IxvaP}Vo#hvlV7xg4`bD<#4h1XP`nHf zJ3fbWaj;QlWSABZRqTC@QUyzO71}BaH%FtLQYxA}+$nwZo?#*jix<>GX77mcq=Z{b zi?4A3=*i(D7gvp(1sF9RU6s8?g&+7nqoiIJEgNMqM5rsAe9S{Ix#zQ3PXx*;&(raU{28(hy19%!nB`OPE)ikd*6`0f<@fQC6LsK#N zG~oUx5C?}FG+Y8S;oUNneK=J`^f1<$(8{voCU|(e5PcZTvDYW4s&bJf5Q|0Vph`WY zoOHx`&ZQcfYChxL8PLz#Oa>!W{XQrt1 zcZ5g?+0srL3`yc_3fSi9`q+%zXRZ03P648Q#zvieM^3oIB8DV(;JNYYhfSNdM_Ld=JUL(rV~>1Xj3oT>iQ z773&6R~R^wK&2g)Bc0%vYJ2Ts1OlcjH1t6bs`v{!;Zy4)4V5n zFUvGzO%;FpZbOw&4*$CaM66aq9kaqh-~mkt+^0fN@}Rsi@tHW8{S%J3efmI$4v**e z5I) zu5)*bP-?kksjqX6O~SUAkVq``G{zW`5bD$Xq!oa8RQq~o5k|TGwcNbaoRtk-8)gH~ ziq3`kwTY#8uDScuxoL3wZw$>fUrqgmx$XkX91s4v&7^QX{_yUNK_pE&ZIBaO}|FU`V^ zIXD?$AbMjfaceo7-{BZ6aG5KUzwJ$9pQ?$w6qje(@de5@Y_Nz*_zKI!>U)5lLKVp? z;$f&7=x}GrIyTLZ^B305vd)H3Q)Q zE72aFD-WK2P8B6PJYEzY3MdXAQmhd|cv0lr01B4cm$n8FxHM*C_$V;zGMIx#E)F5C z&OGR7ppul(a;RJjkq#`Tylu!U7Q)z`ach}qcA#)uXu1n_wj3)^OV-0i4DC4#Q4h15 zK5t<&g!(1|4(9q3?aD#Jz|1Fn{Au&S7%zkJi}JD0@_p%pQf)5D1L-H%#Yt}MR%XgR zCDi9lJnk6hL*EY#9^<2XZqZe^2o>QYfS_B)Y@+#ALO5mb7*qzIpx;B>w&RGYnu7^C z6T06v+A4)e5$^h7FF+eVmgP#NR)2(4SWl+Yr{QCZt6Z-wKI?07gzPvf;r?WM3on(* zBP)#C>bl!EY9t+~a<$Q>quZ ztEnMlW2mo5kp27hNZsmi82Vc*z-LtTYyDMMiyIHVo`;nBA1jcCP~5{ z*yff@44gA^%LOD4vA)CH(@`rXf=-hJCQ`&8-dt3qjbq{qjPxgtUKN9I2N9?c$vMZO zZw(v;VO9rmigc0TJ3<(#P}D`=`k_RkfR@8P?d=k*e41M-D4{Kr{W2B=ZYGEU1S_Il zC?r`{CU+1}*^9d~egE>=ky~g+#6sBf1wxK@p?Wf9?bg1AVI{j#ZF%6S-%w8vxd6Xd z=_?%&KLU}auU!NRCwy0*1M3un@EvnWfKlIakcpMz_K4tB041;1NJxSqa~+)Yu_SJSp~k6_FM>baWkx{j{>D5z@Y zUAuK(v)`&uWo^cddkqLm;^f3rOWB>>Vl4>Gn{DX~hMxMBIP!%+0oTNM^5C(hk79zf zhkUF$9GlMo<9@qXu`Z=wfiBIj2aY0xod8WOhruD_ip`8TKX6x#E?%v~CU`|Hx3O(Y z<=ej6*;4Ult|mJ31|Icv_X%$Z@gyPRHF#Tr+UA1XgM8mCJZ#b6_+Oh)T+ntdtxr^46D5h_#$31?bzX1ujIREB$`SH#R z{dpr{Qaz#|#pPprF!eItn(D|=U}zZfGfwb98sn<^mux8htWJf(PpwW2*OrPi{q|CW zkRtxk*$c~Mpegm7OE^+@EFll4X&mD*nGY1e<(&(Jw-(Ur&#=dOX zUr!}KM2!yG(h4*^HdsUl0?E55fX-aERyAzd9|d%22vo}B&X?X`x*&GE&YTAKlIt3^ z#>!>CnQsrczr82!i0N`oi^)v2EW|X9Z5K70{w}du;w$*0)ga$s#$HFH;-2&Zd6TS9 zh_E+(^Q8QXDnG7-l}ucJagk4{)z-0Uj5>1K;?>qZ|7*mk>EQU)X4Bi|2d?MJ=9A_! zb@?m1`f^4eOx7&hr*-3RTCmX#bS(9X@3Z^d#uFmUNcLre@7349ou==IKic0mo^WABznitP zGgMhZ@BRSlaY#V<2gCT6Y5Kp}HH1v8KW(9ZQL%L5YixTN;6z+~gWtZ^T1i49*-NIv z(MB}#Z%H_95!L2!2><95=Q^6&hT^$#^OHZjy7d^jv0RC5?W5NnXE{)LYFx82cHAG$ z`22<4BIYE}Y2ZX7{r-_HF>!mJ4V z!L8l=)7cIWZv}4!u@$3#xXM>BJT4{8Na#~Z@KbQmQa|8Bq+ON~$)v#P(&g}F_yQ0F zfmoHQVci5?Hr*!OrWh3g@@8^$dwMKM@TjksLR0`HVbD0}Bvhvo;AVCK>0kKUg8$26|L^Bds^TBHe-IM?TjFQVp9IC< z?fYL1`Okj--Ozv8_W&#Ye;_9U=z@Q66aUk`X8@S@OoXfeHZ?mV$M5{aU!IHK%=>>B zyZcFkAizl?jn_AAFTZ@2F&I#nG$o*ivI zeMop+I~?#VbkN5#ids(*J?(I-j2Kl7gYBR{&x_;4HuKzO7;o)_n`0nB@dX?PiyX%_o zLCkt$>l=oy4L89jhHG_(?5@P@XAA7_aUp`?5eHNh^6ldN3wjEj1?@D7H^(&YVq$v8 zJD6z|pFC-#1Czyvgzq2MX*E&C17{6EsnnRUq7@0-IGI>%77^C@a7IOnh?UFP zWJN=WKeOAWyUa;NgoKyM5HrbIi8pO^*s^e~f2Pb1MNJ!%o!ZssMJP3+0)^LZ2&q&8 zMe_nBGP^k*s>dh5{yu$p`__GZv;p8EvfyYkdV3i=JwNKZvn2W_S@{&@AlgyRJGvN6 zd=c}*D>R{X5)d`Dl$tl3KH61Q{-&%E`o#>~`}1@;wgv}vwUxSY{~PjhJwFDmnHUXD zq@h-tAlx?#-3}Kga59YX`me{A^q%)C^iRC0X@L8W$24!7Nd`sn_u`!AJHgr{vNo-- z1^d%4K=Sx}AU;38%V_o;1*QGY?edD&bDidytmKqG7@SZf_}GJAp9ST)DZ)Hu5kF62 zy7k5um`_Muis)Ecs5D~16~VdQSaM$A414T^MzuhTQhhkcB9bYl5AmLRcs&SseqwA2 zVuYy1Gvr-2aaw1|5u+^sv=iXyK0A!9_2sC!Xwuw6V%C+Vqrie_mX|qc$|<^tn}t`z zUFip@Y_ANtCZBw4V)3Bjt*C6Q#dA!NV>$-Ydj8AB_S62+$D7;c2wkluyQXvU#mYxf z6~dE_^q8Oeq|?bhNwuL#bU3DW314%<4}TC?SY4XOnl~6!H!0B<(eUi8i2j{rz{KHi^Ae38cK3tn8!W5pm^eZD8m}VdeOkW&OUvGM#%v>3!}WS3^mWc{c;Z$ERWknn3lJ zh9K%c`YAp6uc2}(a6(Y2uYpRKDxEn(Wcb%f+ox!uHJc0YMg`}t-=ckmrJ&DWqN-k* zE0ChJ5J^Ci6e1}V!BL4s)y~RmQZV*o1#-%-5UO#Fck;kzu7k*6)!+C^&{KrsW$?BA zV;2*I=Z3};sGp|M()*$(ITJDRdPt<#6^Y{Hc#KcGphhvUFdT(CbU7MShlyl8ch?9; zq~vqg)+Mt3+S)q$Pu{8+kcYG`o)s%Pi)nJIn#nzwPy8VAvn8jP1c@(s}yuJsq{vSK6}*&LiYpkAZh z$PeD?ltD%xk4kwbx<9?{P)6!DQSa0ozbA zWv5z|TqIB|%lKi?v&9$@)s7)t;ztIUql%*0d}1htN3VVRmV$sR1OlpDN;|M2fgGv% z)5ZZ&xY?*2(I8nuo6S$@2}4*qPc*vvOlcmo378K&nkHm|v%Ra~7T6+N<|luSR%aIf zmpmEe?=3n%k4<)6D}vWY1U!^WVby9;FZ`ttVulGlAcAF8hYJSr0NKk?r-73woomzG zczVyV6R}}8Ob)f7*cptJGI}wQyyyh&+A1%cDjKN1H zza&7jc97?xkS(iw5vRSr#X7PSVZ>u9# zUbnquCDU9MFV~1=io{Rngh(eoc0d2(4;(W=0O07=0i(QO9&uX8Q$$nij_(?&0%c3F z_hzFF(h@59F2GDSn?{@AD`I)w> zh$UclGb$ux0g$lYGG*Gl;qJdN0JE_nTK!FBOf|=xKE`pz5a+8CMXEV1nqrNwYc4$+6`L~`g( zoorcUG*W6I(1rz+(jk2hq;mafE@LlHN39={{b@0qywP3enD5UxxShDA((9_$ijGK< z4?$W*5tk96X0Y5bN64Kzz*1Ot!9^%+X1z4P^H_M_ig>6VR>w z@yWCa1M8utM2Tf}_ctV`>(E+O)`~F^Bb#fZ8iYG+>&at4I7-X5_!3Ydkegw6cTEq8 z@Kpz_`*?y0(0*V|x2^_QR!Y#JAyE42t$;+*UXw{3Bk6@4=AEimZJiFddtB_0CfT%Z zzg+DW+w|6AbE0qRx+3uol~MJ)9sbeJP|D!vmVvIAT)q|)o@;(jidCYH!A#n1u?md=cl^tD z;p&Fnu6>uy7cGCh{rKn__j8dXAB*~gEB3>t#VvUov%j1<-1O8}*L!O3%(#uaC5)g3 z-u7VzY+Tw}g{diA)4b^?ux@sd)Gw;f?C&AyRBvGd&y+)7l~>K@=;!@AnwBZ!n(P+S zOJ2lnkq6EXXc77e=f6XVf3bG`w>~I*29Dn*#($yhI3Bn_dYIs+EJC&(xmi3&<9ukr z%8E-+0XsP`0c5?C%X9RTnO&Y!$%_>_^-V3TE0c7dJu@A4$#X9>d(2cbx#krvx?2I4 zsR^w5On&hAMK+wRT)zaUnHsNUV3X-P(4XYGnG-9!$c?>a-mwKha>-2+c~>7bV@>%@ zn#SVE;s6H)oJSF$d@^d3sQTh=l*rJ2S=V`LLEt$}-5EnA0Iywh)C>i=93-V;<6N=b zu@Hu0+6ly6!TY-Yf7c0rF@yc#5&X^D{tsNdf}*mhtP0gO&ZG%kz1tzai{DZK3}p(EoFn|4;Cmnd3j~f9!v?-u|IR_}?KlGvlX=@jn86nCMym z4N_04ddeY-VqWFstbz^(8|(Ibs~i8;ejDOV(08j6_e2#I0YM25D)b3Og4&5n?Lti~y#HALMwLiY|+LV-d zmb_-xc5gpjh@cZNLtpZNm_th%PKbT)pvt4rUDRsWdf+*hh1ku0kiK-Tv85IrvGf+S zo;>S5#!ZWilLdxZ!_wC(E27ic5aodgiGyI4^m;8`L7gd0q_;Mj)!-r_KO}5A>3oh*gTZ z4vDYz6bkAEGU$;wXw@LX z*bV3=AAlP`?)IiS#tr}yripKTYeSw!9vo$^G-9eSNiz8@*z4F{Us02=~f|MjxfPf@Z( ziVb)Smlvj|Y&@pksrIO7#i!foPA|rpZJ-x|yr-aC?M9D0T!$?a;wjO>32hfNUHb(N zFyyqgjHp&O!|ch)^^$(w+gv2bUmP(R9zlgGKeo0gR_hJ@^59Q=0X!fTb#ER)L9|#D8xy6Kj9A0+LT2$-Gqb7J-OS*e@ zeXVbE6g73;>zGt^J-LOxbABfK7K`Tszn47j+r+)rKR)b8FIU-oIJoM7v|kl7_wbCt zWtk}4K!W=&p{(6XLq{={@zy62SB63C0rgXfSKKI3qv#g@%K)71LAl`_pX6}x(02ss zmyC`;Pmyb(`N65Eh31B)BI7fskCK2^)7S)*g|*Ed%dG32R|Y+fM6rWghW0^&1_f>d zO~(9dWQ79wczUR1!kdAfls6N1chUUaAG%*RS04LXp|o%2l8=mQm)7XE#49D{eoje8 z%W$f9m9ld&cK6=A@C_!26n?D{G6&^o4e3f<$`imT}8GZ6z}NPGS<4qd|2-~6*Y?E7RC&MLFRT? zRXk-R1Fb^7hBLt(^Bj>q?%;0j(zA6~%ejc6k*rSL8ab0;1^H=s6iRc|oltqti-%}c z?qhs&VqA?%FV7VXOv*7wKmj^pL=Ncv~2!yrmUC##w{VikH&U)iIO zLAI8hVu!&{9xWNl`lbbOBFw%N1~-zrd_v`+uD#2C`?fX-G<=)nhc`!Ls%4zE(m)`yC?h@`xd(d-LM8>h6HMGd$WxH=XvdT<}k z3~TnJsHuv+N~i5&%TK6W@9^#zrt9<&>**i$x|TUGPwe~YnLNk#v3T21t#uK#Mu&bx z2#5I=JH<|=JVa(N)cJb>BeL!@xOmbg8mTtUP}HvAC9_6xU($D~1+t*(#4Yvg1O%t| zlAJZvdkTmz;@iE`jeTBMVq*NAk0z=B3UgD5v`XYBWXi6}>5ag4NXa<9wLA3(oCnHQocKQ@~ajL)4(%>V@*y_lM**kau}X7www1x1PPy0>*OQU zS2UKWDHoBHpJ3l{$Qru|fk4L4WS~{aQbLq^n;8JQneZCbLtf+Z zW&{_j6pp_?Fq4{zQKA?bnAz8F#p~!{U?K*ipr!~t<`}oJHjNoXBxE^jvFUE3r6T-z zjoBe!4)p4u1HCd9XFWmt)Qm+co-_h_Q5%&=DC>8GN7w;4=O8sD7s z-KEnThrQOpU-LCOJ~Lq1@3pTkb-H}rqY7KfJbMn-`x+7)KvUb({veQlVcpm2BdMgCceGfx*JrNG(m5udfd(juMpd zr*zpgr7IZQn%P$T5OgM#>tk2?cbdx=bYHyJK)TzQrH6W~M3OB>Fj@wkS_5}v-+kpA zbBp`OeDYVoLlAerq=iIjL_)iuGs*)+@*1R4A^Ab!L3y&*kdVe082U!Nqgkp6{p03W zJ!5dkfLn3!XiL9X@Wt;^D0B^a-Y=rERyVP>*O|Vuxa?R{S*9^kvq|BI&ldiu1~`WL zqvuSyD9Hw+OcBzi8a)o2<7**Avl8V(t7XDjUep z72hY~PtAk0P{J7I$OAXc;_5_&4*<>mN054P;q{ZZh?j;9OYUxpfx*G>?i^}f(<+6DjofRmUIM z2{+ElCN?g}dwAUm037-A5-7t=-U$pOQXYerVEa0x1ayrrGi(-c>ca)3pMLdBslCf% z+BZt$fS;SgC?K0~pVY%AZh`Q^^2>t?BY{-Y1!G}7;uTJu4GM%YwLM%;lL;VqF-W>`p*-BekKU_P@W z{Z2E8%^`kvq^m)^J<+J}a>GX?igZy-F*Nt=xAf}^ZWjvAn9f0;>2MC{)QMyVoDH2H z`s& zp%&Z_uh1)R+d9-1Uo=wAOc@i0c8X$}uciIN2~xP@kqNz7-KrqIMWH&K5S=InALRjD z8$kwOMhe7Xg7&S>K+7a3f^}z4;^F2$_#pe3T6pWtc!hWBwv%=xY z?DJsTjbeo`Ek|95YRmt4;NJ#gQojBQ@!<2N!hyE}q)k2v$o90(@;v1E(>3k-3Aa~) zyWYFoM}2!m504}8v@pBaoBOye-E0O*!~SXXeaR71k!&Ie##2K(nB{4L!KnvG@7lj@B}oF8YJ|CP``hC zzY}je*(2iNHhfiA%$lM*Hs842^UmHkUAxuKdSTf-dc{=q5#QcVN><*AwfFqeeX~ZR zZtroKQkzS1NG{etWxL6Of@e+jru;+N{>QxasewlODVMS6@jW|CMsfMC7GnLY?=Z1* z6-q;`8pvuC`&#?3!WhQ_<|oO3xG5EjB3##)r{;T7`RFNH+n0d>n&pG zB=u4i4l9o(w^sTWny|2yF8aeYOQd zsOHzEb%j=z)~@XD1fw4(pm=`wcv&jB+C4ix54U@0x81qt9v%Q26}jc`ckigR%wV0O zIi4G`&zjpdqq{TE=16!4LP;3N`kPDlM-0+`2D1MH)EDQM(h!&XCoBG2b}wsSWp3|m z{8!ZfU&4ae|J2w26ZNw*`~~&1|4E(yAH?^6PlEsc^gqk;e~a(``?Y_!u(SMIrq3To z_@6uCZ)o#RA^cxx^S{dCKZWprB>J$jurU8A5BNu_&u7}yry`$?@!v9i*#FFS`X{3Q zAA0tG0{5R}{eNWpF#a|4>yN$v@93V1iS3_pJ`5~uEPwr1Yf{BlN>dq`_o;JknSY#| z#z}5$%hG(@47%`e+9^m5{6d$JvwhM9a{bGSULPwN^*T5b^Ll|a^E`S<_)TCg^X!VW zG!(HRLu9y*8Z*OCTI)sRxPbxjM=Zv|w)fR6icuoGgo+nqItFAkVs4!|~ ze!@YIJy?B(%x6%i*B8-O88eSv9KuJHU*!zy^QT}%``6(_g&AWNVV~peMP+eRohrnM zNx{QD!lHkD^Mbqa39dJW0GrgM_p326bGm5s*`jfJjNgB6>y8^{i+#*qnx@-vJ23Pv z3~c*=YugFmpN=|x`DC6P-} zU#3Wp6zJfpaqq&Lb4`4hl(qIXE}{J}zstX|w=%{KrX&H@A!9FG|K1Z(qK)WuLK-}A zR1{&jXCHlmy_}d3dyxoRnFzm!8x;k_bSD)G-j$uQU0Ljeo>9kyJ^0h zI8n`bf?)9H#Wg2R1IWY>r<9aN61xGu5M%Njb8JFah6IP=h~P3)R03vh_*)akJ#YIq0T)VkMLVfK;6KvVqn#I72YiIer*-C z`eou$bhgPt@>|Vd1*KrIu(>citaHo1l%^Aj`ekF`4Umj~1$jy7Ym}q#2|38avg~q# zT%&@mFjo~D!PTi#78;UAZmwueMjGg)jEzb%a*T?YEDAIj6}OR;s9)F&qw*^Ema1DR z$##)gUR={bGE2i1S%HQiUSbrSI8X&D&&Y*)Ie;S3T`Sqe89`|QLLz{q@&}hl9!%ZM zqXd=p2Y6zE;OLTE+iNS$j&gJZc69*E8wH31G0w%sKU#qx))>|Tv16UIo5M5_gO$W5 z;OxU_y`3BS6(i!Q{I~&5AK6dJ)A9v04?~@4vHJi(@Fjb5Da-s1Go7Sb1Gppvb>We;4*kAzPWP@#+$yD$fPGC zM=<5*DT^KNJ~hpICvsc9{nN*hZU&>5^F zZLAYBc_Jpak`O|U>7Z<Of|aa}w0_9dF~EVB!$D~66L>AqOg#!ix^qSzoyE~>ldr!`zQv{FTAdUep%*fo6r zT7OCexk~+G(faKyt}Kodbsj$^<2EP12NpSA zP@84E5L$CPUx6e^eOb^pL$(mF*nOZ-x5OS8Seu69LA5bLZM{ZV(0cGvibwB0E$To{ zu-n_4Pj0K(@(L9X&omh;GDfyPEAOk*hw~`H76Q~@OAzt?Ue!&{vv>VnXoe9VY-(EIit7;crn({W};C(HluAH==lMt z-&7$Kd|AS}#Ep-l03QueO5leZ;GwN{^X*Bnz+ntITTGxE_P-lp^o^C_h3y@xNSLp}g@ipB3quNg2jVk&c3YfeR(*1fKMV3DzG5@;VZ;>M_`)?-c?;ih1V*I`De`GQK zuHpW<%>3t8jGwQcmD-=j_#gR>e@SKh-1qN2{63Q3ub=Z@S(-mO>;F*GeR5F$O-=Xt z_5Y~q{$+FgltKPJEVfU_Asg$z=^+0&-M{D{|K{TU-)gzffULh4LjLg$|J#p?0iThP z<1=CMKWLlZCe44V>^i+X-IRutO*hwVdDCg`Rgw8OT*;{tfkcR-0+F)6|nB6RhKPq@Am79DxR9< z58sI^+l}i<@X2?qdsCx*wxx_?oKI@~NvY;cKKqPzQC7A18)>9+hO?6KYOP{h)tpFj zg)V!ASD+woL)8^*tVZHChVq@nP?9W%ig&TTlKR8p;ob7|x@~m{R(lSkgtq+U~Bp zQf%V6y#&vCXn9C&I%XFsiyUa~ykN~s@Zl>?d~bex=%a1AYGb$MmYqDg(pTSKy}kMG z;KwMzA)%jiNg` zr*xPzcGaD-3|=)ES7G#-eaoWcC#~H7HVeS{mW$D4%uDbU*B9E z+#FWyV;pUMsuFOZoVgq(d1~hf6jpfgGEA#cjvur`mfL(R>n^`vuhrK65)qsZf&CSM zo8p^DSBdIe>5DRfS|K592WTgLC)(k6OX2GChA zM58<&78$bW@L=2|U`!>sSsWj$mnW&RiwTh0hx1$!1%)sus#xY!G)UJcfSbFM_<3tfgKhA!U0@%)7A+=T1> z1Bavc<1w2aPH{TS%iU z;d+4TG%S}mtnv&FnNt7_$O@tske2OS?3)j1HpO$T_j1+CrR|wD&ym zwb8*xt|Nc%k7DK8M1uM0sE`F~Yz+GYbsFdLvJ*6(vgNhu{7Iv`wFWnX>nXn{D&RhW z&qT+iM|D*T@9EL?$vLGZSa6l2y72L3I=AW9{4vcOQbOw^b%HkqrR?IW=1{r2^O*ed zrJOWKl{Cm3kce`&Z`7z=^tYTNovNhJifg%KZY5#(*5p8lyfA=rq%5WiiBk>C!eKCF zicWSBq)~fotze~!3Miw=qJU`TUscK6iAV6!6#;6q=m-?nk9DJEhfx}bIXv7$LWQo1 zCPmSAt-JvrAwbMu51TR7+HpBvh0u!DS;9N0A!~arEcMlv|V#QTfWb4QNNJBHNo@~epkz!qGdDzGF2kdf%;+M{W( z&H!*$$rOnu7A?F=L6kwYh4*G9+z8R)4Fxar zm7w&AOx85n&q=pt^b>&0$2x^~jdu!dcLRT@&_P z0R|8&8hzhZ4Ne;b;|8|F-iZKXb^-}Vvhr!LJ)0!d?#S!`lxWUD6#l6c|DlBE@Dk^U z3RIZQM@OQ`mglur1tk!4PjJjlIu8V<)d(Kofv$(f2PpIeQoJnL^=t?XT1ZPULY{mJ7ZT70(H72hL}ZGR2GORNb-W$!HMpwT9ryuQb9o}I-sGHb8mXS6JpKq{ z@a~7Mns(>)hx;Iob$wQ}u=H*ghGp}n_fs_o!jJed*-LKUmTPt+DcjseCr-kCMc zx!H8*8j6lCS=4!M_)@<8m9N#dPfxnPTHYVemwl}Vd9ND5YJN$1f?kOJ$nWA>D16%u zT674Zp|!cj_g_r1>T=H#I)js7J)dU;Vi-i+D8{B9gQxtFEBO@drFMqZcCdrXp3r9W zBBpI&5fci0!}GqtzKMID3)#A${gx6HEJ$7(bPIP@<}Km6grvwu8B%^W`7{8gAP^fP zM>}g6>pc8W`}A}_(!yxAkqP0x(Cr!gA%{2GV}v>P`Uct7=I*y`l3-D6+=C6qZ8yCs zH^8A8x1WmLI5P_CK-wf{AF_w4ke`b+zBbM@yMPS^-e;Y^QODBqEB7M8_gnYoeBrPK z!|Bf9!WQDIPJbyd+wCbjBr+*tgGs%~q`_ud3*b@DLR(mtH$bHv@pP5ZAx%_A{KX7S zQV?)Xq5d{F2N$@a58{rsA3vCudMiL2L9}PKi)8W?WwdzQP;~AL>xm6!0`P^HT{6zZ zs|NMz+JNEu1we8Cg|Ndp=Rl;>Ny$_m!(}P)L|bYPdNCUXN2AoO1=zNvUR1&?b~LIl##9FOZ+Uuq5VNbtfiEEC`!&%>q#-yGQ7T zYmLoN+izd0S17j|N4$`qB@Kv67sMHt#b)oU~AGT|^F?44X_FP-vNNAwVq!UxCRjZj6B2~Ks_%0 z!$goN|2ji*os!X$g0(;cvDLM{(M*w;MEqfQSS^9nyQG6)vQ|}?+nG+_g~a|gPijRC z@)pWb>bA{U%$jSV^;LN4dWQh_U6D$fEPWDciu0U+;JeI*@j{Md>tz4QrwyIWJE6g> zA%oPy@!dK}Fq4!?3-UH=H|tAxRTpP=x}@{*+EemMGg;3@B38Om6PL|jlx;Lza@jbQ zkU{$>Ozq>H*k`A?9ah^Wx}=mW8pG@*Gvs$%2cdg5#0&Th9L55oUJmYiKgjs?NNtdq zm5}b`6%&p|4xiI284;11)0iR>OPqjBnTe%OAxwpwMCu_hbLtUzxspAGP%FdWys(F@ zywiIKqL$Q0aa5g^B6sA z?Cqtvv#@MNgD$7uuYA0exakb1dRA0;bZx|=*;YKQ^PsU)%g-^=S=6}PJzO2Df+$va zq_i~AT$M{esvK+=J6-I?F478X-tC` zRn`kQ=mk65+fQ2Mf`9;V;Pn zX9u|Bs>s2rWsDYXXIRqRW73AYU<2#Z$8P~?$kgQyP1WsYulj5ehA9?)TmUpyLE3slU1tqnNfi6$ZaO5s~m zq;?oUNjOUsk{h%IB`9Dp!^oj4z*k@~e{U~vK0BVRbO-dW%hVQdUr!OZhhVMzpN^jiT-Rdua57tk=ItiJ#8^`M@2$alULE=S=fj(Gnli` zJ}2c;+ga$sT`r&MyWGtI+gZ8x%?K%Ss!QHPw5k`WW1A4kPLV%y+SdnFF?g?n`>gy2&yjDk)SIg{q2$DWb#g@hk*)*mt;= z0_UZ_7^U6b_q69x@*kY{1+|Ir#wshYDfSn_1*##Z^FqM^lkV-5lx1iHVI_KOlu$68 zIV!v+x-SuHcF0CE35TX4AfZi1&;1qqb`iecBb4DF&2IqOf$ikQIADU&8iLVE?mZjD z&@xrnX@PO5BIm_+`#>TflOQ19OxE5cVJTwVdihn5CmsVc%zlo2>h@tHHvScoH#~sJ zz)_7$TKx77)DIurfGwO_vR{*w8B16O&%pWkGk%x|f?lZ4MaYs9jRVTZ0XAw4D*)XG zIiOXLYyG150D{mBm% z@_o}@aoY9V=hjI&aZ3{V7DDI&wmUoKnvc{e6OxiUP>1C69_bwFeIKQ}DSC<^f$eFx zHS^2>WGle(uxF-e@Sk00n_45L8jC0qH=Dhs%*SGm(o1QBv~ER8VO~wF1gmj zOX`p!Eu+ZgmV=1bJYg+M|6Dxd>X@8HmN$m+I8&rZ!f2?gOqBiK6nwW6l{cspRFG4*Pn9R8Vw!G$B#Eo;7InKl;-KsL?! zBWB=BXUuC%zE*DFZ;K4_X9;BseHb+27z9%gB;{A>SqzHq97IWS+s=>=*@D5&Yn9wW zY{PX~t^nsOSB41B=0MYu42x&Sqy1N@Gs|aUlTU$SF^wDf%BY^ zw7F-`$wO9|5$#|Q-r0btuYZ9)1$6`40Tn89nRD6*%v+Edkatb!SpinmBZG|Qkt?*8 zD{KdR0mupFx`ts`uu+5Xu6xSOS|f<71i$!ta4%?aZ%E((mQwVhe;zA<*9CbERzOpz z7WQKqdX`!XmOW1}j5b04^1jn$vxI1bM}+Z&8ocoN=olpC1S^KdHDP^59J3%Xtg>Xd zFjqMvWa#SFufrtW9ZgAwH>?3|M^y3Lr{qZ#a0w_2)C{;hN67CCz`q|tuE}4b6TsGw zeIEdvFN+9Z{EG^YxHlv5WsfFURQdX~6j{z`kmh`NC|BY|X<2n)%n>(tt?;ZRS)WXF`IqalN64i{OM zVO^MepUzzcWmiXW{V3d0OL51i2VcDC|c- zo{D?R;}~1S90|i1?rIlz72G_7ygg^b41T&mZ*1vlg_PL?nak5yA@~`o2Fhm;M)wMj zKOTSr%^Y*nNM+^Mgw1QsEJ&g*6H}#(R?R*{>$$&0_UNeNjCr-ZR)wyY`~;2SSPYY&sfu|Q%{X2aS~ z`5}Y`$SP^2&YTSAVs5EMxAnEukat5ATj|$RUnT0iU}oCY59=1D0FAt{9zY$PRmu9%yi$wV=Uy>bR!WuH6W2**4T(HU8G; zQ(i}MW&1NXDk5uzNZEtP=v%kz+E8bKQva<-wRS%9(i0}+vOGfO*r{{R72Fo*9&R;BvG3Q_% zzn2$H-?J_n@uw3r;<)5ycNej?*KlT~G*F8aG#KkBOR9ruZ#W+$s6C-IqdqP!csLRj z!&VcD0(-lrFmK|%L;-?SOo*T3WF(R-xekldq40&Ggf&uprt))-CoSopYPg4aT05yx z{GQU(mwYvP&}1NpNEAxA(E5JKcrfeU{jzU+2B?`La1)@z$PG2^z5+wrhyDKTF24&7 zUm{!1Of-%OFOx6@NC?oy;LWs;VHbu){$c7hyFhIX=d;!q>*NRD!uA-{{*TS3{o>xE>}nLMp&u6`n% zDrR>Xw>b|Fs5(m+D*E+LSS+TdlPdogCce>rbgu@zuPjR@(c0K~c759v^dD2wk%lIU z^|-L$Ccn_Coyxxr)Uk*%ms?oOVUs9X5Mzw?(%`P{{C8$w2h8NC`H-FQ?ho0L3 zC5Vcevp4kL1xPm!MHu`ZkgWZr6jvBVt=G_VjS&KpB(A9mTa{DZ>_;c)-q_}Q7XkuYq*%-%n>@(3~)@v7ou7#>aaaF8LqanVG2s$Mgo)GV^ zix&)0l(-q}!rjX2ILt|!$l~&dIBt~(I^*rfn?hWK1o2R}=G*lkU505=z2Mopm5+a{ z4L#zSanhuBRQTjC#mBv6+H>F&Cm7jLDik|qzs1s6z9^j0T-v6Vq-ByHkw&!E_gN6u zq18h=0Q(O!Fgzi#hh@?N1v)TIBBGS<5fQZ#jf#B;oBQ}sl$LDBk*6j+k{KkbRs%nR zJe2l+C@aL~bPJ1O?=h=&Od6x`q;C8?{s|Dj=hOQ zzjD2cxf(T8YB@$6F}9UIk9`-Gm4G=}qQ9l6GBht8rFP7MQ<~7}=~(CHlF=hP5a{~F zO~yv8gZ;k3m0FxElYXJPS%49+yuQ?lol=)|6z!&=FGDzl<*v!XDc%cqCR#n|9cJu8 zTIz_cp2#E(koRtOK|Bt>oS%qoST$+!D(PfFiV5HeTa9b2{s4u3BfIB zPDNr`A%XqY286P!(1Jw`{5lNG%3wjBrb9mPSnkWqS2XAx8$a0+mkZf#fp>x)h?hFp z9eKa(mB+?t&ITmX8;AfZviiGs3ZV8Ct>X>f=sh$$4dbA-&h$A}2}Vnqd?K=Pq~jtY zW$`LJ*u?Knq%pkucp*&76~jd%S9E1nu*a?~L&qo)(s-i^@Wpl)|jkMZA(DQzH!>*cm(3yLRWdVT$wVitq-B z3+7R#qy`SJaSg&fjrjM!`t6|{MC70q*my97kOxK*wxMS7&AP3w7xT@Kq%r~VUZKDi zqd|Z8986?UjdxbJ=0_4+ZOL@PVDP(p0EnMdz*ChpQH>I&wp8qB@a8G5Mc4k-pW)CI3V0fgOz*Ib=4 z`-BAXGC+f6S)n7B&oORG1|LRoFk>@|-SDGv?9n%lK%!spI0Lc~$-?EQ`8Toyxqs3) z-~19|69&$7zjpL%_yP!S!%5>908h`b0z5O#cZJT%am=FEKqWCKZzUEP@avp~J;E9i z52*1pI_Tn=EmCt|B52I26Uy}d;rOD6@%ZQ zM_yXj*Mi`y5`DeiY(PxYFXoe9Iv{H;kPR6)i+y3-zzcr%A=4Y$`IUL2Gd>4WypYN? zDD_;G;dhnysN_RgZMLQlVfTa~J`~bhpEG1hqYskOOE>tmM*tvgZW*1T)NN$xr5DE@ z+Z(kDigt^q3|EPO%e1R$r_&?@Cg4OzJ7%_z@A05qQVB9S+SE>#(~jFzpGj1Pqoy)X z!?Dm!eTIxExd8{S=Fchlo^7OviLU6*BhtkF{3hwxXWS(vDq=5K_N|lfi}`|OIgHJP)+_!0If%CW=} zwD>Zq*Zcq=3kVbWo)@;hJ{tO?c)%VUJd?UCz!e~DGt{qb5;|{hztQ!e^+Co%2bN@0 ze{wA&K{*biY{E&Y$z)jxWVz~e)#9?_2B%6TkR7g@rIzuA9Ik?^t>oq_WMg zV|x>mYC09c1gFh1!%`QavqEZr$Oc&h1zCca2+)xen7CE+JleVsC#`wW(6L70XR`2H zGl}H+^TY5!RuSlpGj!;4Txcqy)In4IX);lY>bBvXOI_A+6c6wsV;4Tk2H=EgFybxo zS+1ZJJ;wMlRe&wfp9np10M+a@ge8r*VEsC1Jh&x;fW}+efQErkZbby5DvH^Vh*H)AS?rSOsh+1>jz&lJW%oL zjqkqtA>13F@4O+4f$qY%LE~@(inqaOb1*cm^RQPts^V+5?rI{uy9r_H8aAnWk>Pl4p9G#<6Dvx%`?$;e5(PC>&rEeFG0 znz^vJiKw~m6ogxHRL*!tmbrg8s}{h`ko5}a5iH#49mRy-00*WOH`2g6xH{_bugA{> zVBK;OvI5}X0x%$7^F5z8913lxn6cQakl?uYY6WWM4PXZt27tp?vWx0bZ=D4{-GHPh zGXo0yIR5IGB`cqsjB3YxeNl4@RY%%KMi2aAL@+j|Uk;(XQM3PrmP0xUo>!6FnmH>`B zydIft+}Yoce6__n%)&6rV>d>-Vow0Cg|x>xOCsY^VU; z5v!j28{6Th=!-6W8JB>afT6fC{jtVk|RGt|TZ zyKXVFq9SD!>3(T^eFf(C3$hL*`yTqsY?@5M2x&-NIca7TSmyUhQpH}CAJUuYKshD% z+{N!na0(fDP9X`V#ZnA~npD5p1i_TUCri6f zy^)eEagr>b05W~^%{zCY0C;?!F?`8hko74^PT&>oW0{67wP`y^^*UvRcdOG~v46Vz zb*kK62oC1rdlP3}!9&07&{coNuZovG&GXqin#dx=oHz+`zwVZoeMFzSi zuDK*Y_3_1zT1Dc9CLu+5W`nkY-uwnQx)Nc|F}*(-y2lyL8cVL?7i63HO66UC*CdO% zooOa!Ap5vpJO&P`)n-cX+CR%tuiN#MVEGi>7Ka8<0Z1hCV3Cg6X|RM7Wo2Y)?g;9e zV{JHb1nMmbL|0ksf@wlLW?p-}H@C&tI6R&1{`uDJDO9`}b(7!wg7Rl5zen22_Lz?6 zx}dQ=6cW$V0FVofTa$cQy~XeZ1oSN)z;F_n-iNm@A+9aR)R#v@^yRe@I2Npbb`v0kk?At5~?T~bQnMp zH}1QiDIi~3Pkoc0?ZF0&my2CD?TUtL>jE-INW>3Y26XH4Q3YrK@_>eDN7*zKvzd=- ziy~FUuYAdFHd(*TRZ=ZvK{UxGgAxv|kF?lDuR{LezXt5P2)KoaKFj(80(;WdiiW%( z#vl$^F?eaD3Yz|h<08^t|4}&pDIvo_%VHr#A`Faz38are`xG$(b3i&ch}$P1QNK|c zKYWHhQTW(91Twl6#3Wtn%N#%`z5|}9M(be!alvti8cRX%Ow(FetfIilHUtN2bY1~} z-K1Jo4e=K&He5t$4w5kh9ZEKHEB^^qdS}!MXU}|Q%F;VZ=akZ)ZA+9QX6B+Qqy6=T z4{sf#9cZfR{ckDHDURdKv#ztsr!50_IRhs@nTTUp;@%AQgE|<@g`Mc!z3%r` zRlmR7xL-%2jna5@9J&*CkgsX$;0#wT>L_DcmV1gHCFgnJFo~AA7^5iImGZt^^k))` zDQ4^0+KKQeQ{Ay^OJs9IvK~R`N|3zy{8mAy`Qy}Xa`7K+Ii zMj&dtIoQR#CEV!3@)aKeH(h;B4Z~;8e-eJb-m5G(wQ4VHB$X;`6Lp;{f3H?fmuO9u z+2&9b0C=Sh-FJkorh2E$5?2;lKtxnGu2~c;Z^&TLeNKE$y+4yYp5BGQ2!MP=s!@rm zfGeH5=btlrZ_}`JCD$%^S)%B9T_3lIX`<7HsdTh_a1lu_KTd3UR>;2g~Na@4y#D8Ki|E6v7U$XgF{>tBu zk+%BI_e%oD7weL-mqcM#x@9OwdF~#_2%Q2IP-rlx@#02uS{Thbj~4e-771-&X2cY; zvDHQQ;4!(u1HI5wdf};|KQ43Tfz4Ps{lRb*x0g|;g_ts;a_#!AHV+$Z+UB6~!-w>kt|c8xRe>w~P7#<`CKiOl(bjDks%f)i)^} zP@NOj(e*BZL{WIk9(y3y#UE9+t@AIHIZ8PxJ>tiY$02p|Yg=jstXx5B+oxH_vqzh{4psk>?^s2e$#2skjp~76FVfy-zzbCDze_!In zT^v~}AZ1up;|Qtad!~i0t784#6KuxD^lHoK^&v*+22z#kINbhcIdbg}owA`Vs&4Ly zkDm*%!f{C01}ywYL|bCOycohY&OtT}_cp=FzzBi7#cx+*p_-{5@Q*F8G;(*J4qJMLcO@}IcF-#|=95sLIpXbMNA1oDz0JZ(oQ*tL8cy~J@IBOfC$nC=r(eJygKK47a-xW`Q z6q#Zuq-DyVIK!to#+TIhkG%gDouiSudg*5_A-UQ0t7cF?yGUG1VR)K9*Dh+T>!3&@)L%D6ms<|VTfncC zt?B<>?6|O(bm4D&!ku)OueMJdc_F83`x>kcoV`n7{d7bIpTiw?pwXFy<7DHc)nHs(fxNDEHl7abWcQS6l2!{V~pIq5(pRUF^1L>-I z*JTuCN!P7NN9@v}^{I;h;^vH^lS5SZnw77`a-DfaI5KUQF=LO>9?j8~VhZbk-Wkwu zg(tV z6K9%82n}dREd|CESb6>Q5UA_!Vc}DVR<>%<2e1!$59U7GsVvByMCZjr&rd-*C~(6c z(d!X^>p!K}HT#kA3;a?u$VVxfNuMbPJ4#Asb4&x7=k9ctf(Q-W`8YJ-Tug#a+udNm z3|c?m2U1r8(qB|imBQ_KoW>MueoI*_J(8RZFk$>uS(kJQj5xshsmHx#@Q1ticw^PM z3148(Fs_r>w=Ygy{35>54*J+W)YEwGRr+C`A)@4D%);crm_BN>latG~&YU~`>;(Lux!yMMAt~Ib(ARhr@*1m{KEdxYOMpTbZ@Bt8AO$-Pw zQR2EI#B_hr4XNDGbo8^_#qKlkw*cAw78J0?ekIbO)9 zyYmb5OA3Ox{RnuEpkmVIYcEK`qTdgFn@|tkqEJWneJ`74!*9_h>H`*NMjs?NYjd)= z1rgNen;UAW?C!4>&G#_WF|Nb7$$vCzaLV`jjzRf8-bb())*2yNx)rf}${+7Kr)>h)!rja#8?5u+{J&tYiYMAe;?4;Fz=&&D^$#YTt5%O_D4= z2%-~vwpddGgelYLDG72j(eSRW+J2Qk^qW=6e6za%Zdr86W8`VD%u6xLSkA1bnlUjV zt%uKr-^S1etb4rl?gS+C0hmIO>c%0tLA09~7e)*<%v|oK3(8B_WcQ&P(Br4+F6x`C z&qCGxD!4Va)w!v?5CUiRr61Z!2J^^9>XY`;6XvJG%;UBP`WAgpI;S)aVJ^?nfMP@| z(qbUsO)=1Am+6(4JXO{;NgU$=yCoeMEGTzATr@J!Z8N$}5pEdIbdg+`fs`H7T=Fup zXr#MY$WCF(qg7T^WCtViO0TOQ`Ih!@$iwy_l->K*rSmvX3>8cg$EQeG$n}j`tfa(v zx#N$+TirRr6n&-l1}2=DtIALN4h}=yMC-C8caG_?{txuGkGO~VU}bX5aHF51q1YBfp{y>4|JhSw$*$a2DMCj&flkWp9ghcRd?tna1iWK+=N%F@oxXh-l{v zuXY3^3t5_Uhq^3N7R*HKnE4e}9p1(0v-5cNu&?6{5DBVsQp3UHf~n4WWJP7$qe9J3 zXr||?o3ntFI7d=W(haS4=%1-_4v3PKFe)vdH0t^tX=G`P`ts1EDQ68LxYCuwW^rPe z2Et&#koOCQVm7Ewc-p%4EI4P?cvmgh_=mPjr2?jbd0~$#q3xIKy-j#$zJyLv7as_S z_AF6972NCY_p_RSff_aTB2o2MT^xa}LD+IBx&#Z&Ulzuja@bHp7j|bkAPFq(kq*x3x_$S80X|Bfyhm1jsb4(t)f9gT}VaDB|?W|O(8!7 zq-FFRF*Wr(mybIQ}+(bRJ}6-ChURUL5uT z0wy7=h>eBT>+4VQiKx@1jF2KJ-c>izFF*`|ojSBYZdyxXI2ARWp9jowS;Wy09Bn;x zBD%j0&xS?Xf2*yr`{WcJwKk#w(zPVNk0}GE$cc`5aqvosiq96K3y8C%m$##46}$^% z?vVrv@sTnmoQeNlJRx5qk6C~m9H*~fE*Wo;&M~<$fiF}dcT`3fFO~TZN(z6CW9&zs4P1-E8i;5*;q^`jR}+@*(yo$5ZmHC#G~49)Y~UKsL>wz(i`%q~?@(voOa#nX>$ z2(Xd+n=dUK>?5=I*V(x3?q6Nc<1-1@9P z{2Yt(goWy-|3GQ;Z9sv1(!+_m?DJ?Wlx%kD{CNJ)v>aX3bN6ufxZx2MqQbpE?0l(#f;hf|Rb1+o2Hc z`=XmLEGnqIIsh{|G!C)K$;Ly&*#9X?9bGnqs4+!#0~i@=h&w3)V!@3sFp_xui>#Qw z+k3l;*9CU%>!;&{W-FJtReJv(gnn#G-|Bb4e&gkP@eZZ-BfIx(50iIG?Ml$`QQ!eW zAwuSGDG)gd^h3agZTIIv!Xynuxf>8Oi=TeAJeN$1OONYDo_gonqTEs&%)O$s$^~fU z`Yi}b*rR5*Ks|xY(NY*c%vIB9Yer=YN8OB;!s6KTv}^Q>rq?FMh_x_d5>xODQ#MSP z5H`9pR61_oQpjSk^Vn()N@D0^Ujhbk*lKHcF`|~5tCO|aQkM|0A(|ssAmmXpe~`st z3Z1*hKFuKK9)x)gLvJC>QkP(~;w6qH_dzntW{Vmfv>AT;X83@PUwB~U{-RwlFA!=5 zs*w6xPlikiIj&(_jOg#6Xh3mr-Jz_`RSv==5t_ zVx+AKKcAt25$fuP;(UPg9Kz+{~y!cY4lEtJ(cUMGkTn3#i)Zp^FLjK^} z2L{lVA7Gem$ns6$0@3IEBL0bcU6T6Hb^|3lxsk;RhzFDww^BS#8 z8qBAhmo`QdgW!risNjI`_>^p_K^RHI9+%l@NHPK@^iR$rNXxhT%VuBZ+z*lw9g=qI z-fbSvLo^CfA)W(rX8M}DN15LjAIEe* zS5&lli?oaAW+~G(f^+F39%R+i)THSX(F5Ib+d~pFGIP!Bcf511XF~UnCE*4d64L+F}(X$x)QnB#`-O-0bA_C|OStxUK?sTL&KWm6t~#&HiI;G}-<^3p_7UU{ybemj9@o)@@z^@XS=~^7+PXzOm)Y7=`4E zyvfyg0Jz}K@t7x0Ea=)3=lcCIlZt1PdLkMz#k>-`(9rz?QoC1k+1JI!oww?cTmLwP?#WAc6p5Z|5*Wnmv zd%&_5Q^UQh-P!cw1$iqSZNEuvRrvUaoNTJQ?&S|ZF=!t4^rwy3^cOj&^Wk9u=|Qeo z3g#N-TBg?Ry_~vwbDh!VU{wr2YiAUdDef`i3|uZXSi>ct&86)*yG~bT)0>MI2Gye+ z)&eC9id%@}g^Ze;_`z$Ty=CQsu_YZ0jH8g2E>Lar_yPt>!s*!}V z_B9=`5Svs|!?H76e?YizGt<+-()Lp}Pjco|eOuGt8Bdjm=IC+8cH039Hq6gaPg6Mo z^?`qc+_QU3VTK1ut9WWy$QJqttq!xAVTo!Itmc+Dc`nYhHCui#Ofw_c{j2FllF++-cR$z zB?EKv#6e(;jHg+>6x|;*wMn90#nksIhUsQPrb@0qQ6izQo%#p!C>SP- zC&wq?$>-0;s7}ytd>;7>UU{zT4Wipf?DeLnDR7#=i*Idgr`=jjwZ77YVxG+daQ#Uv z?og{}ovgERFea1l%TBN2Np_q<6|4+^p2`iA!@1HJIJ2G%&fL2(61!Gw>eKCwb?AGG zC05~qK55~A2LcX-&b%d$VFD|&W0uyJ*GMlSHowa&?W_)*!_cl3QAE>u6wmXWjI?Gx z>gSI3dMm#puryJuuAst|vuaS&tis$q3D}~{Z`y#BI~lDB>>4a%9!LvmNM#b5X^1On zSa}lKY4S-IxX8Iim5N?Lnqi!&)U-3Gg}gV@*O3aK9w$MpL8ZNhL?S*#fR4Fqhg2G%GVQdgnOcUa>Pp1^>HbA=hF;sG;QB``hvG+ZJca9C!R7SZIPN_RuRrW zy9i`NER4B-cTRpl*+{tPnK+=`ZM<}Ubbq|K>Zq-5v~84MAzD^iE~{GlTD!6zR32Ww z?o@u}eJ1KeZML6O;(3;F^c*6)dtZ^#QsUCKV~)5qyCPwd zccQ*rJEof^UeQwBdtGJ+@0`Z>UpLQqGahl;-RH$|uMi`4jSzLVd5^h^AI9>-mJoS# zekz7QdGv4>fNa1PbAewM0!9!5hCdQQ*9YjGz!TtKm%+2;9YpOF!rq0g1&7evNC+Yb z6JpxsA}Er^fxx=_QY@!RghVcfW-O4-kSfP#j3ns?Zx0Sug@UvN#lntc76I*K4ZIxk zj`4)X`+D&C0^S*ycR~NBZd^szyL0zBXfR?9gxT^)l@%ksh!o-m5H08-FYpA&Wdvx1 z<(7N!A$~ws2ZXy#4)XaxkRzRE7+Jsz;-fxCZ08aDqykW6G@aqIJ=Pr`$u}GjJWLdP zj23UPs6t$)DX8RLa4~1!L*QY`PIJijOs`_EoKIASGUMfjqLFDjY>daE-TgUnjJo2l zHty^%Gtb4+`{`-I9`vR3(+p=FfZ;dgnEwHY`5PSae|i-D;Zn!?`@?VTS$?Y(I%MF< zHrZ^fr0Ms}5FhVUqgdh4oHG!k5ZKlrDzfnp^lX&uC?^zxE?Vp=m?2V@c7k@~ZXL(V zqVc(O?Z=2?a^wAn613UL_}N_x_H%Y><=xxTZDYIi$=2horH0qLx@I8Ovhl@6VWZQl z>#_UK(hab=mDTly;s#3%os}*6{A-uyI6=+*cbf3_NH$#_YnwQ{l| z?ou@3e16qjGIHUn{hYkr5<_`8#uGt#Ip(=yvZ8(t6yp5XPB$9J5~~Vf{&e+&oIEBo zjB>IjH-k205R}SI1Xz&z5B~0Tga^$eWP-f9KC>$$1Ze3{s;G*Z^t!z2ue#5#Ix|Wc z$wd)l=@94fmyedIUp>Qtzt8Pjt|EUdaRyn{q;(e5J{AYcfpdE}>?k@7zTDgPI4X&( zg9ATQ=@Q_&Js|r_x{t5FD646&msD4^w8r8BmyU2wo_rLOLmkwd=JEiU zkX@{Iw^j;SI)Y`Q^07e{s~(Ii*tvWs<_}er`6yP|vZ{bq{*UT4N1{)q8lbMBK8&mM zN6Ug3xSF62A>Bv!@C}iVb`@^$`Ng&tNb^A~OephH9nTy<^V1E+wvXk@h_R8{&H3Kw z<2N!jMuTQP|B!BCVf?4z20cC7uRZ+J zXl6`VR*Fy={e6A3u8z+X%2FSrVa3+ZsnHQCuEmQf?`xN~di`*}Ls%2jywYJvQ4Dl-^q^t2evTLsI=#E<^N%{30iASm^t2nl zZVbvB+&v91Eiec3lT>_K?HyN41{1pD0nSJh=)`Vx?BwEMZnRmuJ;9+1uU+Ubw;dq1 z+P7OMzq2GynoMnOZrzLQ656k9YtwuBjQOVdwC17n4y86L+H;Hfyguw|25&J-_A0cF zC~wguG%n?oTpd}DSlX&9F>{4t7vr)QPAbuBkUhESpkZQYhfF#43~bgwL@kjb?&b)oKNGny^Z=n|QkZd47L z5%d#yL$pySOrS$GW$ZWhQncxs3<>j^96!*zIVlxg>5-5E{E>E5@{9|aMs?@P3m z4%j3k;B0)^D4_|Y4&={f)nsT_BqBn}5@BTk>v0D`>EDSHhS3Gdm7`txl$Agnzna=` z8ukz_@hjzAN*uovlINEaSr#SKA?z`u$FxLprVTNszbaRBbYp-HIzy ztnc>!Dek_*S$uTKp}r@L-lTNTPWuoDSFR56%>Sw-*qXCwTnY;ut#Usu)k#c zc?!U~XA0_RsqeM>*5$nKmGh}BS?S5wtyl%g=i!hff)`U!gvFPG4DbqtC`2>J zO{KHXeH;d8OAqxyU2DJV*>enX(WO%a5l$@0TbZtFe~YMF#TibqRU%aNswO`jj4FCD z8JTC}O-XH;htwl7toD>L0i|8=PXD=C2w~P1F(~Up3H<76nFZ8nhY$&%<*^Y2eS3q- z+v_{351jXew`M2KCeJ1&PSsT9;p=!PCJ3Y2cPx0LPoGYdWW4~o$qv&Gu|LTG9e9U& zgGzm3^^Wu=-Zao%z<*1oqy~iqUvp1FHx~;d?6vq7^Br$wu2yCbaE?B*Ecgx)$!u(5ze)*4sFkNv5 zukK7Sk{C0EJLlRCC+w4>yE`UK`$FptnT>4@-K@Z}k)m#{E1sQIM02oH=83!aQk+*W zDA7^cDaL^KM~T#tc*xVl)$T;IGOsF(mKxbA#X!r5pXVf&D)%5|C`+iso$e}w+plzt z?PXq??c1Kuvim5XuhTduA~2de*53C?&U=s?AM%K_4k{$xfo$!7lK)f8`QMpovHTJ) z{^xg`b|gJ)4<0zPJAma4Ts@w<4y`4T283@H zGeu|KYgpca*8RMM@A@mZV|mtDw32{|ZACLt+j?b-y8MYrl?8s0ZN6tE%XKI^F+E+bmge8EG+5N`3=JR%YBV#LjV*L?UHw*jk;rmzZkMK>+ z!0<5(f793gS66C!`VT$EkD;(IFynn}@|Q^d>r{V>%W9|R(3X~KZF^XS?KA0zxB)6AsiGJ;vd@H^DD=u7Yx)JEZ!20Lxr6H zR`j5OMncojYm@r=3D70gSQ;R7=vd6F#Lb`$?6YjeG-{Y2#A}&qAd0=I@?fM>lBr0g z?WdiVnQ+7k#J?t^ijj29uf7wbA4+kU?B9NCbH4a-(f)FN|K=X!4Zw$Z`{~nMXrlDk zr)H_9PpQlOcU|Kyyg#)8u7CEgha0rQy#r;DeLdIu!LzP>z4%$feIQ}qWy8-SMUYy$ zYcjd@MmM*$GK#Gh1meo}tY^`vzHFn3y?Whz5qWiLDhXZIOUG;;jFohLL1M?i-R7HO zju&jIX*$jt*3P$d5G>h)fCAu-3d}oK(J2^myL3SEQSmCEUY;K+10#l+;)XkoK$DTt zoMBZNDm{&Hi;Z#CJMSyWRY+6m8^g2EYc|mLT^a;Ygp=Tb+0agWw5uSNlRh`|JyuO9 zJOgC$-8gu9t@l0f7Lhn0A$7)LP<<_OGWZsLJdZKy5URl>PUr=M#`NP}(KgkYf*j*I zVKpd|-iVq0szZslOj6#|Hc?g8NKt?~Y7$9{FImFY<$*G6lB>JNkNJxci&?xi)%e)1frb{pJhe)`q&#FZYWqWmi znMZwv*>0R>vk)4(W$ZGXrkoGi@A)S`MV2OXTe9&6<>J>FBUuETJ>F)Q>9d7guYcd@ z5`LuAB7uue99-GudSYz}T9=KSuDw<4A64oLs|F3D=bt#lgIbM>@0 zlfT!PZO1wnrG14a_Js5zt~Ww)^}{ADymF0YnZ|v-*kP~ESPZRxkV_%sWt3Mz80|8l z5h0^do@Vkx&F|cz!uA05@*TNE zkw-#HmfDX|P!Lp!BAJ$OiURbwk18A@V;5f+g7PUT@|P*_cfjT5^OEwCpk-VN3q3Nx zHI`xg%#f(AS6&uACeMbF9#R?Sx|6@Nf#XG>f_oykwi5~YQy%~+FGp324q+z_sjJ)b{4v;D;9mXV#-otnU)Hq^6AE)I)4GQl;9a&7#LqR4k?Z!MiLdiXxMBCEuzLV)Q;tX5y|=GHveM7|uEHjy(yf>jE|}EO_H_t9%pHvC&h6Wt zaX#_-wyQmNEhN5!V&VW`zw5A%RO!0XzbN;_WXPLFiFSy{YVNgG;ZhY3po- z4;-Xh!f-Bj_ClG8VXeyN9}o5RVmNvq5;=bw?D~e$#@WsjJI7S**la>inV6HK5fb4& z6QHzF>+5D=?}dM5HXg4bk+y;j`&t)dh7hX6#wjY+$3;L`*Ujqg=Su-mA2Ryk^g<9n zZYS;!9(x8AKrknH?6Kz**pr!xgl`-^&WvD_AOdZb3^KfHOfXynPD)GIAhF-6V@q@( z<)RxG$PCy>vkMKPIwzczrK{0%7_oXx6}j4SmqyJ>DiJ|l8Co=w1Dvps?)Oyv7CroS zZP!tkQe0+rDdNP5bibq_T#N+^7ZM$qIJ?Gn9Hr6m*B%U>LcR%d1zbRAnHUKzBvIW7kg@?qD;fG)L`~tQ#QFvN>5*|Xe zypZ&98bS)Uq*=-r{w7=08vQGzTN6q*TKIhvNqsq^8yR{J3_6%JwDzWCL@BwrrB~x9 zIdb(1QtSxb9zrW;qhVxn*(L^PDl-<@^NGQ+qi`j5PFH#bf12;d9BI3m+$cEbHe&Sa zSa;o+r$Wz(vwbfFWz~_s&Lu0KvjI(aN9&_^F-`N{mj~~T07*%1Zt&I)VhAY~?jn5} zQ@&Bx681hj2c&NAG`V_ggO?7%sxCD9ZnZh7;s8|1*k$)D{kPLef_F&N*hFKdy3AR-g%G<__V0#Jf_$QBeDVs`Z8gsh0Y z+zT4W#z6_{A0NFo46Y=EuuO5()>&e9NUk*-8g6lsg-p6WUc>?@aomB6+{l+m5OgsV z_kG0igj{#Y6S%^S4`D^h!Y>iOgy(wLxruWb`HomjWs_J25ukxaGeLdw6^^;ynbjY) zuEKH&9D*HwByj)zsv*U1&Y!guI|*`Tms6g7eR*Gyc{ZYGiHzGLZB(X{p#B<}8yK6L zOTvWUXp)S)RxEBW2-bO3H759xjvJb{|CwX{Mvc}`ITtgKFYZJ?DZmD^+sjQS{yKQ{+0U#zhy-}NThLGyzfGI^YXPyUWn=s#2ute$JF<}HwNX8?pf)y&ZWKv{9 z2a_f?Yr!xiog8aw{iM%1jnjP?x;WNMVJHiS{i-jLH~Q@2$n^EvZ3pg$UTVE=NrH1 zeaJ27`NUHP&X|M1{&oW-Y)&h0-40h>4F^w2?9nC?KR?m472c|$j<};LTqyY*) zOPIcHUSZP=RYuiA%OpzZ_UOnQD-2;fd+&3=oN?YM% zju8QVbMh1fhmq4#z~S-~cxdM|S&iIMU`AtT? ze*BQ5vDtxNX2c(v4KtuqO<{F*dApOn`X%-fCaJ0h5rt;EVzk1tg`s0?BY?S;vXYa4 zZj2WB*2wfJTU^wN!lxY^Y=MAt#GoP4Z7*%(uEE=pz+e#3QGIPEvEigSQ{W>Pfm29& zpi=^F1(fj83x<*1fv%MzIc2f}fTeos$WS45bBdGD`@|*{Q&~II1{I7;gyw*iTPUQB z71fndC{%=%AN#WS#h>Hg$W8C46j0b_z?IE=2jh*$$)>2(G341K z#e0=Nlt&~j5ng?TOB3QnzBHHSP^e*&JE#iJ&NVKC!`pR6&VsK|>x$(NDHFgf=KmNJ z$0K5?+Q{(s?BFCsEYVOwI~u(dy(Tgu>w#P2R(bHEW!VM=?xp^O5j`xOeja{~_2WGg z$QIv=(nm?zntaN@Hxm?g1#GuL%F1$pJ=;IFxgZ2UjxscCJJ6o^~9{-?2 zLTotq;6fY6TUTPTgT-beIRGfN23w9^uF*6ZS@Vn$;yJoNzwBlsLbw6~F_NwxI*0C9 zHKIh8Lj4JSsyl+$Q0--n34&K&ZB)mpj{zL-e)B26!!uLhl>TUcKo+yA@cyWBB~UwH zH1~Itc-ngmLwP^j((_B1G1lGG#F`PM5opP=aEsiMsmZVBd+yTmEJI_eSGSRDJ_po1 zS@W`WLRjJ2Tt3|88qSy-KisOeQy0vpoJ%kyNtMkVIZ9D7sxVgKEy-Wwp^7jf;gEI7 z+B-9MXfZ-p??jMTW(xVaizie#7_}Wht%oNh#hut0i>G|Q;u|h?I39jyR^?q?XPtC> zznNHXw5@EshgN^4NEU^if7M%g9NGEo$?RDo%i->vbJIZC!VksRb_5MmG;5Y12UBQ( z5n!&8Ax#ixmQs(8<7F&0Eo?QV*q4!0oI&pp$Nrf+-rB+7WR+d4eX-4bm7(Ym_XSY< z=bh*Cq?(+DF+eJjmOo&rDpZ_2`S|n~lHQyqdpRvCNn6;{_430vw~k)kAPx0sT|k*i zh$mBT<)S1LHi1-r1ce+;xo}yzaC%vf8p0*|ra+2;7UZ>!N!++9DDN>52-L^wMz`t; z{s->>dbQAUrue+$!YFd_*gHtNLkDwkwICU3?~;7jz6c*V9Qq=45W(U;pjiustz#t^ z&;U!7L`v}e*o{&`RQpx3a{dI zL#&&u!fScUTm5Qmbw!D(tYg%NtnKjy_+=xbXC?(*xH~fn*ZL~GoR}HJ1tCR-`%6hD z8e86$@F=PY-Hm%H$>rb-kxI|D%q6#V?b?IGMZ9`08&40bbu|*qNYAA2&1QXCW4;e< zrL!6|{$qM&7(cMRjCiG=;O-?tVkA?_XcQ_HUd}9!S==K%kv2&0*?Gj~EER8w?nq&X zVG1);+VZy*l;@RFG7{#qq(^3$00c|&}$gF(8Iso0a@Q+NQ1IT2g;Ko;oL9B za>78*Bp0)=BymW6EBH<_e~V@O!)7!dMBc7Z)x_LO>>=Wu5GU7sWWRD&x5*E~2~eBZ z_;r!}EiLX;Eu37qm+VQxD^$~qG?K=*!fh2mdmn;`*Lpw9H@G5Loe}bj#LFh?_NWqx zW*Irs*BTJkL7|Bjz>ViZ6BKT)x0%Su^VQm%(k91?^hjna_3Jssjc;LxDsdj~<8ysC zT&c7;KP@+V5hvsXvvMt_r?)znJ-s z{~4e^a9OV$fM8F5;SCVGg>OxxFV2u^CEjRUt4MWjl<(_fkwUWzu4%D??QWqzfCfu5 zrk^6@It5ZlrUNyEV674GxH7BsO@w;S0kgbC<)VZ+^-jaHMh1(x1xagp7nHap<%qdS zUktB1b$s)Oih(w_f5$X&GjDrWg^QH%wB!{KZ-T0p2jI;Ukk=7VfS?m^7kCnJq-Jb5 zs>cbDWcz&XjW&AFDF(`5JVY zb-?wYqsq@FI8!hMg~UhSKW~lb)#L1N?ho}ejC_ZHPHK9pY220jo>n*=aUQd=;2;9n zo?DL5i|<|#6@3$*dl0dDGwa^L;pF0QntxFby;hsBR;9Pa=k}G7{%s$QXm7`4Uz+#s zS}@N933Ui*{YvwY^)Q$z2|Usj%QNq1^ONentRF3iaAY&9YwqOEYT=qOrWh`~6MIMH zoRZl$--)Az3Nam;GI;Ye%}W4=Bj9|pBsCFZ6?qkD?IE+=(0bWf(xcjGZ!*%^0vq*u z5tu?b;h`R#&;k{o#)mvmffrKy5>_1kkz?^`tGi8lj(zf&Qx+6)Qj`psegcsRcdm?+ zWChvl@N26y{QMp*0%D17l=ph$e4%W(`3`57gSLm;#_ezHkvZPkq%FVlVZdHeT^>rAS)$*Ay$r zCd%YzjabN27~%t{qQ^#&Yax+AlKa%Zse{$(3{CTi*p*Zyh*zKp2S_$j&L}8`i)0+I z#e+ik-_A09+R!UuL7ZY66YCnpBJMRWV*w3In@TEBVJ3rVx5%Yf zkM@jgPg?9FT8nkC+V;HOX*wyP(wr~TJL^clG~DM6;WW}%85bziwwuhTA$)6VeHPO@ zCHvM^bKMSOh_Jsqzhb2s*52(5^G)Y|&V*FKUmQrf?`6m~wy#h$2w~xA_F0L0t!ydx zDfiiaY(YcVj_KaLl4EJ#kVHS0=B@@>=|N0{Ong|P(5jidU z*;Fll!1z}S)hK?e z?q3$FJpR`3h2gPy*-M=P!tx(U*IgNSv^G>osBPR{pocl3tkaTQyjRvvfj{vJ=U6fw zAvRi<+4t0*T2{^U+_&wA3%=FT``?*z^cYhVEPa#OJ7}!x1I+mBnMSkf5uQ6EdS*yY z6*21m20^zb{m_-fe3^n#7fM^O6U{$Bp#-?k4TYd z*a8`?d^;h?GXF~g@&R3T267P^j+hT@j@TJs}uIeEc>U!reFwTJgVY=x5#vk}sKEQvWGw_S3DvD@ON*h~QSW_z)n>bqN z+x-Ty{JYT>J>9=pmeMmY|3+t^r~8X(DGS{{EVBOb{tFcOweI)zf7bsf{n7ddg!0#I z{#^V2r}X=={wV+bIDahvJnA3qe;?N$wLi|~k5T-y^>={h@6+|K7v$d&9ya>ly|#YU z{zQ1_Ke!oxcTi>flce#>Y>WQGN$dl~`sn>TxI_Q9x&95^`H$E5cTk6sR@&6w-qgzE zZ_wDUi}Mej20Pt9K^4obo; z6)o@OE9o+YjShve4hdE%Bvk4te!$%cpfOb?K2=0~OnjZ4@O(>tQba-e;vB$W&_Wnv zY$3iQw`{O15dDMOoA&_tMcxF2A$md?v715J*Kh7W_$P11-48}NO~zdx_AAfkf1XrU zzC9mzm*vJ)4E-SGC5|Uk^#CJLJct|9Kr9+?LJ+7NQ?DMyEsCx6124WwSk*-> z@&!D(dZ5)}rj>&W#qXU|<8W39e&iPf!oL?>Op*_Q@i`a`7oV7}F@^&X%_f=(SRqq=HNn0Y7P79UVMzHu*4A{Z?Y25io;dkPT{hoJ9>$CkpHN zF5w#N`&gu196L}+G*gnzAGg4^llDcHGZVRdQ1h?`8nD!Id^@HXt_khFm?O5MeMaHl z&2$jVUU;|z^KgTpt&K4zLk3hM3FU$>yu_8!nA7}D;E96{RIJfGU*SkwI??OGwFITz z{q(!JPMy)~x%o4)PQppG94DwuWo>k%G7GCXP~& zjtlf#qOHlkKsm=EJ$kYyLo;pxvI&+76*eK#M05D?(W~=C)UXUm9V^KwkdEb+_8RQb zBp#5KA%`B}+Ceyu2Y4B*`6QeveUTb8?jfkp<8bCB`z3lM?epn=%H6k4pE3Rcq)ZfmR>^=z&u2uunIG$E(IYaM8LTfkSE z01F2Afgod5F_dpGXCx(%HF5O_zJ2X}tramz;6TK|PTys1!l|!ZNl+IfkvU=zz2xh$ zl$<^f-T0T+y!`la11W?>d0UPCxoMVXYLDxEgPSWej*X|X^Sp3A?KEpSZQH_Pl(sWR ztLKZw#E?qHQ0{XQ)O$bb%c;G@*<66x!KPWyYL~cYjtq9BxK39l`I^>Y;hM5%49<_p zB8XLIS{_*Ox)u9B~9Y9)+8d} z_EpddHleSf_6<3jJGX9-m#t6^P*I0-&5gNpatd4EnW@0d)Uv+vfv{F7s%^XcfOFeJ zk)pa({{L8e2jEK9wrzA`+t$RkJ+U>hZCexDwrwX9+jcUsZR=#e*zfHB-|zRGQ*|m; zS>3%>cR&5~O77}Qx6!Ic=BYqhukPek!#>KRTAgKNkHBd`t0DP5$_QLnP9re)cHEAWZe%ySs1xYzO~1j}PcE`kQTms&~@SE&Psg`MB* zmP2+%z%pB-oK`A!{hVB~KT|NS!hk*mLexin31@o45Vf<0qG^brHDCb^Jdi0qi9vzs zkQnPrvhRgh?M_5=RqOK_v-5OMzgXPWZr>uJD^8#G@XQ?{=~OD*izO%WK=53CYXS%S*W#WUh4&6ojYN zZ=Mlwnu=Db6q z<^HqZTDW&lg80;qTZ6)}xk%JxwL zqD>9Zd%m@_~gG?#CNu{nj zu}cxyNt;KYpn;-16@3V~%X!*sygWKI4&os@S3IzXrF~k8)16 zaF7$a#)#;EC>h7-xRIywpvLGTdKF<6EaWW2DJ1u!Cv_2nLQtXyw{RJMjc?JU_MLCj z$3!r6lXY%h z`3Dn;94?Czh2KH}n-g(yun52rz1E|cXHrq$9CrB`1T|1uf{)G!pw{LVeZ!v6MNg$W z<$&I!%V&k&j}G}_oY+Iz^fS|q`=q;DS4J-qvYM3t)g`z$W{0iME|}j!pKZxT$wCf( zoFG*2D;T9&^}c{VafnbjaM*Gem6#3-AKN_?s~^M$t1aF3SOih`4hntz?_WObW4xOHKDJRPvpNgfw5SXxx5>*0uU-=D5uz;2 z)+OdGGVo*L1xZ(5ka~yj}8KU2gEb*?J3)>!M zKT!vda-U+1h(3uf3bJt!cvau;Z6~tjML`rw#*ZmX5W%<^sXyji)}Mnw%Ei>}AbbD= z=XLw^_HM4EP)blOpdmM8Aj~5Sr~&QBDp`?i zPpB~6H8CoOoIhcow7{t-gQv%swyv z7e^jF!OS{I*p~q#S^~0pWabX&{3z0ChuM>nRJk`#a2|vOJnRhVq_;?A0de3FT31w0 zh5*`54{**Z*f)KdycsCc=dj@l0TYkJYk{5@*arvBHrQYlx&3W1Ry$C8=yjzpP_}?C zphf7B@9xrGa2ZhJp+W!-Q|Jzr@D8=`Ei|0Rsvt7l+a;YLMYvaUM44`V3Q;b{2!Cu6 z;kpa{%f?9(Xfr`@bg7hLb*J09&<~Yq@A5<5@3(lA&rv9EgQ4&GLLW7`f7yIj_>WZ+ zS)>)Dsj!=c5qc&-0&if78yRK{7~i@e9j4@-8ZLYvVSoK`i-LClnF|ii&X>x(oz`(? zqjmeXrbd!6}v4x6g>}USNtTL z-tkT7&8i+@!wd`tAI$Ms4897wAFaF$kP^-X7`7NCk+Az%N1@K9!izC$dPmxDZEkOI zYa>bBuyaR!){p26)3ZT+&B=X)dAZ$Y_F{-wrgS`bjcXdTv1gCexd_E16Dz%6xbgge|jB`BE*nypuy z)=O065Kn@ta-g;@j5#81QKvMsQnvY*?hbN6dLYs~w+YgpUIuGhP{&k*lEu~%P_lTg{_wwk} z;cKnnk|@37jn*Nu1|1{W8d9BqnCM$i5k)?mD_O<_5)*{n68Bu_W$-Opq$|VeaW0+C z5ZkqtTL3T6s)M`h;5ySLbPrOB>iTxRShQXft3)}bJ%*xcSWe%YabdNte-0i)n5dsD zLwLq$RcM%@ttuxE$5uv)*=VF0@8{I?{IpfGUbEg9lTj*zE%UET73a3qbEjr^rdZ(> zGA1)j!K@IIdKO_8VP@fzP%;|7e(adURC+qk?CF%s-axG*AcnxHTLU-q5W;d>MH7ts zAl^!?<{gxu)*I2Ee(cA0nU6=G$_+Q`lW^B@5aytuyILqyII5J^-ERt65UdO56fGAO z#;I_9mD&mPU0Km|rt-%NT^bvo?Qle!Ij4F*yR?diJGDK-R~7JY=(#h5CQIfKHN+xk zV1?OBNS&D_y&d^-QKsjmQCJ~WCyfMxI4M^Fhs`E~-eHw3C@WW0t+?!j^^qR5zuXl( zh+}?sq$k*$J7TmXoy5BIukZvKs=k2n06 z4tQuVJ$V}$F_-6IcM1)jeY&WYmL@9Lu>5S@mJlSGB#KQ%J=;aJV^(c)6Y4Z?8_buLyM%vXg_57v&!QjG8EsPzp;F*DC zM1d|vBwU2;h_K6J0B7;^cby9$Po&)7d>rZ&$w>NUIam-mMvZ10vT8ACOJ$oNgZG)L zp7BtTg25bjLue!`p!w8H&{@{98%U*3FUc)7?=Wz(rmXv5ijZzm~yl+B{4UH1UiN{sr9f& zK~J14(r)FHKEPN5Ly!-)fsXMLFEaoQRG;g$961h@4Q6fm7`rJdDULXAuAyd_2?nps z&YPF5k+Pa54m6suSA?~e5LThYvpqcxt_bsxatFT}?xMT}#u@RK>e*XcNT)0v=P3PkfbMbwH# zSn5U~IZzb(<9TN>XRM2v4ICs_WrxVwDf;!I46b$Kbgn!dypVG%9K|K(dMGp+n@VUL zw;X*F_fko#6qnLw?!2|!v=-u~kqW1vX6Ey$@?a|S?*`u$zdIySQ<6}!P*x&R^oxHd zS1P5LPasH09hv$W%weu+psA?okWoQXNYh9&Ar^+r5E7g#t>1+d-h?tkH6pmRjC(U^ zsq!k|X0;xT#xNK$?(yzAm|hNHzcrGazVrGkBB5u+Iw{fHRX?Ko{uq%REi5d^XpaKh zPh|-{%qI3Q1bW)%HH)&UM#!B{5F{iV3Mxjxew<;0+##sWV(@6V^?C7oaHA&_u8R83 z!-Kc(QCMq0JL>?1dy}=-TR#?7-!@w4LAtVsInwzW{RnOiViBpvb!xCOWfmR*i6=`k zRWA_=BE-xx3Tyk(izqrolMt0aiSsC`^;sfyLy}js)6-0Yxy|`)Nh722igd<$i0#nE zldG36$+QcmNZg*S`lF&PLnNiWN-F6-YEZ)=<{>{DeAp|qnr`jt63)X8HKzSqs&Vw< z#pX-G%l$Bu)76Rd0rOND7NACF&iw#qY$1&|rgxgQu$i|p<{9*iC%&-ekEJTXwD4Wr zZvrTgec-E()C6FPm|}!xaa+4NPAQVp!EqO(m3#Ni(0zO`!kjeKd>=$A5+3Sf04{hlX@U< zy(V?aB`x%qew>58Qf!Ne^u?D7&1h4}$S;6J3Pm0qDdi(qRed@f#Gnc@J-D|EV#Ksq zfm*zAn$JIw2`~ifpwpo|Y-IGkgr3G02Q5o2Kj^$Kd|w`9wAwoqGHz}!q&qo1laim* z?5_J>uXfUZ9%i7lre;he-rlkYB)oxWzAi9(KgJ2Bm_z7klc4Tk&@aw{P12D-ey5;g z2XgQTQM*EN$^WwZMK}dtmL!sxox%a8tZj`p-)KFYKnc`QcBv_VTT2R_jQsjg5ttlh zE_Vnx2_Jbn_p%;7RXBs#dImf7g~a<(oy(Y{SAv&0m%8IzH&^XCHZx!5vNe#b+b-_f z+lqaW;>r#wq+f&5Z!v3Fk`+3m8c!y++!@%Y%;2Kbg@!Pv(@Dx zDpTVk{+I}?QSz$La{2x!rncyKfmfhkrfETHb&S)a%woZj7Pl&mBFY2wOzb6qn>+q0 z;#<+K+*h?>dd#nK0PdH3xj+n{+k_Z?!a~{P0OE;Lm=LSQy_|-lK1A~hZ7n8C+bi>B zi-~!U()Bj7ezycsr;m~Ah&WkRdlK@ zVr(!RF39U>FnT_ZR0?&rrvw89&`12Ho#KiW-y2HjkJ~n_k$kjK#(Xv?OqfD%%Qi5) z9H&3G;Uo5Zg-#b_>L$%u`FOHy>v3@Ni%E`F4_QXkjrc0ZRx9O&=tuc<(fREO%}P?p zwN#n%XDfZfDEWer-Axnv)^~4NwUFzI+#Zeh(AigkB9Hrz4NcucYo85|;MdjI@UH|T zmC>D%L^V|IxgN__?MQxzSk;+U{OTV~NzW69Tg(XREO zid&24u&=Udfdx&M->8+(DT{NV&C^7o7(B5UBBRnj$KvY2KYuaV!OOf>gtsEL4Frm( zg_=NekmZ@#wp}E}N@gx6)8fhU0dg1e*%|&5mj0Jd=5y%}R2uT1JjDM7P~$T){`a)r zq$U2)HZc~}`qCAxFM$Ml2z>rjZZAB%u{1wjh&&CbUPjqCH7Am6Rc251Q+kv&X1&l` ziPwt;5%pFEPUuL4Qk(=;vicZ*rLqr8TaUlKF%BZ5>2+SY+lMV1XIN*2R**?*woOU} zOE7Bb+l|?C-nSFmp1!{+E~pEw@>V4_9}nB7ca5Oy?77f^y4aLQ04zlQk<5D<4&W3y zjjjXy-9*0TQ!H58gRZlsf(b;0gQfo?Lf;&^8U*s#zhS$GADF9}7}| zD#DiV^dPoG)zgzy|8rMo7 z9H#Ybu5$X@oAR60^H00W|G-YkDaoj*DgTR|`X^)Y8#%@PfBApt>Hg`a{ZA?{J^NqC zsek%|{^bDrdm`sg8s|@v>fZ#u&ZGlzdZ(oBXlsVE{K1U%A{ z2^~JTUku$84#d}J|4RhSJ$J^KaCAN2Pjx%40Xht{uz_>pf^IzltO1I$bAn+EUFFF{ zY26sR8;cuy(v{0E+2t1wY6dp@iC6l;<4X05^`93D?VUfBsvyz7sN}%FQ(Tl$Pz1V& zRer5CP_kbeKv=(`|6;Dz;8A=#zI?JaTuK`ZS`Hm+@ylw@&>Z!DTI0?s^_AS;5AFn)ZUeAJIcg z5nEDeA}&^h45mARFc6+R2@X8QJo`i>pw@ES{LG*@Gtz9M9LpH7cRlc8G`(2g{A$sD zZqeE13tgaeyW4QRTS1-1tKpMS`|3}DqwPw3ZU2IE%gGl@cj~**MZN?CmXo@|c*&uP zr~^dKV+LI*QE&;e%tj#PhguRN%nls1MAHnIX0WN8EWCmz_GEB^j9y&6tNh5{D`Qr5 zX|a2s*8KD6rqWPKu2yvW&e;C6ffFQMu4 zB;Yj4$!?6V<}qlpzeJA4xzZ(7zRWi$sz0vB;N^G$LOyAQN58Q_`gb$2WiJ&7A3=fZ zgT%+aNxoy$*D35m6<1V;6yoY&cS%4d$?3^%)`$>)v^|?_zGokdM>kb(vMlH;bITQn zt2w*q_4X$4FOLh#Wg-Z5l2g2R;=Q#@L_tFBD}!=RZ;Vxns$^)Y^VgQ*b7NwMi1D=M z(t(lvxiriH&JG zk(*7sdvxtEUUqRkcEvP5AE$kl$btD&KxkK2$p&V+pWm7Z-fqLg0z6=3IMQk6AXx#s%I@3CyZOTF38E-=6)}LSVFQf*A0YC z&Iw*n_aFk~1nvU(LH~4<#xc>1`U7zI3u8nr-G@62)^cDcmea zp#OVCfD?W3k7YVE)GmQ$xI_VFVj(X{GpJK%8PKx3$BquI`FBKRx9!Kbr8Z?+uCoztj0z-p)w-z$x5R zcYXKaEgnpCd&R;!2ZwIjcJf&TwnAwZ+T537>1aV`UA`nBaPCe;q%bA&4o1)vB?cc3A|2i z^PPSIdTg8VKu1TI0B)=YV-~2_i6;-fXw^2gs|4@@QAO)}|0t1))4}ILfOBNOypIB~ z^gAT!oo+~%o(~d;G`vcD)eRw5WYu5~Yt4^jK(zf6Ht9_q8f;2B8Jb;JgfW!ErUF}U z`hDjpm3kQ?Mv{6;u+Q@Gt8I>zU+ZFi6DULD3w`0NN8%Q;9y!uf4dQsiw%)X>uXE5+E zOo9-T_DIh`UmA^DJ#^uB&BJ*0@)aX=0uEXl_@!&JYt!E#XmRU-qfLqd+EUf+nC z2G6G_!4Vt-{qsxjQN}|_y?`qlYT1K3gNWhXWel!i&?7592BQMuS>n2Y)mV#R>ES(m zpA1ssM5q07}79HT=?R_A8Bq-VU*^Je{~GJ*^@s8-qsM`$=n;V?&mdl#XiAnO^n8HiY{MZ)S)6&n#gvyPxX&N)?!Fo`b$vyr-J$ zml}&tA1Owk8qW_tvEh=oHvJcFDWZ*^t>7R14(oP5@G`-vY?g>cx7m55np6zyA~I`B zj9l~Swa!PcT4D>3Ng-ajNC%bA9erDfKsW}N@=qt0<1)@}D8;n07)7Q<4h){{&VZC5 z%zEPArGZYm%Rp!RU$O)rqqgqQWS&3+*d&vG2*~g7?M<^cf3u_YvUu&Q4Gb)uh=tfY zdA!w+7}Qc@a_)ZNRawr=@JdC=SXXT~cp5xeT}cE<@<6mQ9TW;n(%`Do_GtE+ogfZ- zz%-WX(IwuRB5Rsi9q^u^%DXu&Ks#huSYlgY2G zWw1*?%phzvLt7+|AOW698YT;llE=#X z;irvf1TrpB>h1j53pU3%)_gL~(0O1dRhTrJKXh7>vYcGD)OIaby`?`ljV&&Dgg7KY z{ob@tdZ|n_L#>JGWT@Lh9;M%C9zEkHHSU^`O<2_ro!7a(l~9Y z&r+q!Qdy-WM7F$$GR9=UMkf?N|E4A46gj5Id6MsW@a7fJ22V$wMKzhBM>G&hGQ=MCDI2Cael( zcWr*+564=_Pv<5pwXSW~!lgGoJUnoUB#P<@Vq#;qIR-2th57U|?$8ttC>V*GGIUdd zC^6r5?Uje|dTTLiqaH9WJ339V_On}`Z)LPHPz>&ToQLVZVCt$*W(6`Wp@Y-$a9A>3 z*4Iz}M9ngSI;ccW8dH$h*rTZ|(jm>SZFehTc2sm*IYdqxH~ShUsV$ziYm>h^Yz(y7 zT(ww~iAVL`*xF>^8w37G>LDYXJP>IRytj&MsJqiY2ABC#GTbJmF|6}6`!u^fe~!hi zFD^Wu>Ve7>;!%o(NE748$-ope9dFo;IFI&q~z=sdxQJ`9BlU()#vfwIh31A{^qFydX< zrA5vYT@X09__xpD zzr{}c*M~ZM2D-n3BhmD*J#^4Q59x%gCTeox1tU;Y(D=X__>MU7K1|}PKC3&;U0s({ z?R|-_nx|1g`j>SyG^Tx6<10^eUsDH&363&xd5UXi{05)&<*!WhDDFwIE~-RRv4$$V z8f7RdTP>+&_Bi}^_!R0zhXcV%CeRPlfrNA)UsfuB+Fu;3B;dohM$~+|)C?>5bt>u_ z@v9qE-QiVU%E19CYgw@Vcz5MT*UFII2Fi6L`x$Hc05LISUHUh|gM6N;3p3krS8>K; zn89CW5Pv%*zr|Gi`3V0%Kob&ziqdKdl>APHR;G5;QicYmj+Vb+6MrY1{b&5hZ`cIe z@7Tm=T*n`o9RI>57+C+AM)Du*;s3uS_TQ3ESpIR^|9)&{`aeefiF|y1j+y!2i75u>ugvU!06U-QCanMY{h6uqSB&I0CHJ2b{5Kd0Gt2KuWpuv@VLwyL{yypCQwvMa zfX~M836LvL_8U0cXlENw$qmEu1 zONiKZgl+ZW5s^`o^r<1Mtzt=3!uMJwONBFS-GZ-xwGgkmLaL?-1@bG!sA@%K_2QHb z!s@J+U$LG!H}ui5t#?IBI1XDkE;t|DGX|X}S{?*I5m2U(RheuyWzDfdw0Fs9qO#aL zU&Aewr4TW)GT3-A5-rt)Q#`K66p9RI(eZ1~T#uL0GUF{5A!9z~N@{jUH`vp93km0`B!y`CRI8am$4n*BTNUvHDmZl3RqGu^%9 z8*x}P8Z+2A3!gdNLdr3&XZ0PD1A$GEAH@PfLKjEKHy(pS$Fazq$+A2STjK7s7vVJ# z+i7h(=Qn&?)8Z%JzGYc{DtS&BCFbg_6E%SRcv-`hXX^%5BrFo)+@4H7s_W|ykr-uzko3)@iplRE*d10* zcQNzMr$my=mz4NNt(?Oo8P(0-5mR<$z2NKhgq*wFr(xf5On%vVopSCNr{UpnvU^2x zHIl=RS8cmTk5hDD~NeYZbby5bQ0R!|KV(%&ooyA#L)A2pO$*T z*Xe$YDXK6+Afir@mih~5(cUPUbSqhEYv;ouMb+ z{A9_`;Haq^F0F+oCC&c3uT`|G6{Vh%^U6x?*~i*t&)@Hx#h5?XncuN5I#tf{RL%q! z78Si&VHv6x%Z>q*I&+R!OSKdnDHr2HdB{Bj#P)+^rQ(luSlx|FN*ohYU*mYcCTF(byCMUt1)Tl zRb8~~TNQuh2}1IImzU|&&we#B4^rKvx@z`pY1}U@H4;g?iTRdG3t#5QhWcFw6R&cn zMYDCnwGkAOf=j4CE2_*$LV?S8GVhz3=G&ygr~#K=%;WZk$L<=C*V* zD753{6nQ!^?lH2UCA!&8PP+v$mqR>PGhB6cH#1Bk)8V|1&==o?3*239|NSKe=OPq> zC ziJMChGh$9lksUeYxWP8?%TgNYeevCRxy7Ze$<4$?jb3xGpz@|XvyL4z{XITW=t8sCo+B;Qewal?*ZbBtfS!>=}RxE(+2((LtEX$0)vw%4skgc5gHY@fx-x?@g=&9q> zda!#;WLJ6ZmmW;;6gSuyPmv3RXC>jSwjh1MVNYx?06;x2ffHW*p3E;v8wu|oa)0&a zM#5k=xLha77&KktN3im5dZ*h3@DtXl*Bl*s>^E!F`c80~Jq_m)+Fsrd2t6T4S7I{Uv)1+foW(%1aDl4sqqulpO{7TgyI7c2DNLG&8_E^%05!&t#>29PC)oomK251g8QlMITGKpr0pz zmo0{An@gwKv5qal)AlxkJ=B_m`cVwfAXb*ym*u)JsQuI{hhPg4gOk9#PYl3dNZ6_i z`F2PNp=|yq$!IcGu)1Wh7Uha;Fs!oQ*kL${5e#0~k+wXE*&P$ExG4BLK_POy_+y-O z(0tsgu+0YF^p5qLm34o0B8!L-%VA(81sIk z-_b;+q5S+S;+V7B9pj(2EdQ{brm$CVUV^749)!xzFe@mAA}ih zJ~gc4tI245$oXj6CVilM{FS??+xJVo+~7HwQIbEWt3XU2>^32|@^VdtXj~msQ&&ii zw!?_e+jBr>M5`I?s9rZZYJ8{+AA=_Yk|CwexQe_xp1t_MCf?J0fTTNQedqm1@*{Hi zQ7c)J+&5^P!2Eo7(L?M4Z9g7vU3tGTL{sh?1opas^m4^O;C6p<=dM{&U6iZFZ?$tZ zRvf2-$7)o%Ov(aMno2Nh-E$qI>Ot==*(R6SI43Z`yWb5Z3D_^R=l{Y z%v9lpCA@2h>k7-?PmE>o^@RWzU2ic!$04>h>h zb_v6Gw+V=nCbA`S*o19vIJ%%m2uBb{NXaO#3vsmYmBUe|?-Oq;rJ=8Mg24p2sPu|l z?AZMnh{0DZ(k+N5%D)P@`Z{tlr09riZ|!31VVE}xTym4`_w7@sv#Xs!JUyaSM4n+` zUM~g+IBJBamC@hC_Bmcl>q{pmxmU&SD?n6p7_k(Ef|8hYVO*}`2l-nl6<9VtK#AYpo(V2>G`mpWdxNUMi52x?m zFzjp^Jq6I9&0_@_TOgLm1uGVQu_)uLPVWF`E;Di7osY}3`=s)aEA1%2ma&a=Bc6DG zd}pG0Y$7r^t(Z~y!M*Bix5m@Q8nm(+0SG_=p zfow!{xiSkk%-`o}XYG4i#oV&*(rz(bZVADxlIp6Wyu8IIpdlEX;e|2lwNyB(fK28sI{G_NNI>;VXot{y> z+K6hujlG0%ZGJ7Yjls0Q~B2wD}~zPv=xJ z;Z-2b>8^3F$t}tOYqQ-QHQw~aDz7|8g(%%VEY4Gow^U;`Tf|Qe1{LJ7ec#B~S5rMn zv(uhEY~aChlMC2JpmVY%2WXwQT!cqKga-usS!qZ;^_mv*Roahbjg;!gTxG#}OIta} zLklP@IX`E3WUyy_FcIyE|D8Py_VXHaxI$a8+=Y>1;7g5Gha&BVvo_?S$n9z>{wWoO$;r8x>Ytx<$NQgXLWCYVbY|{gx4%swt&m=pUbCs zZt$70jL8E825`K5hzBlSpT@9m216U)&_Jl(v!I{jRyq+jAiZzIZn}<%%4C*BEG{6W zm#GwecOtWVlO=8W5RbD#MKu^WrT zV3f6$oF6am4&>w%fC|@S-b2?GS8KhG9|UzHT*`QKZuWdfFo0-?WmUF2wd*CM(`V}( zv%e3ZmySdr0P7c1u)^S@3o5m=fOVsn<`aSE6P*A99`*hF!S}No6kZg_9MC78=9r4Q zrgrF>P(q0clo^JnVr?^>FF?EA0#`9DEmfANB|{^kY$cUV6QGc(iYvbB|ifPl4&`X3+{ zGc)sNUjJW>;Gh2Rzo1`#cd`Gkz%RBx;uZds*8StK|J&E{Gq9iaKZ(Bd40L}{ZvQCKeK}QoGB$d}W0!R!OWJ@qraygSG54(uhdefE35k&5jH$ z1&31LkE;gB?VqkjW}pB*#IR&FT!1V9$DP1*I|J_q>9cN8vT<5zF`S(3m*a5C%6Z{# zz2S8;K4B8_NxvqFx~(Vq6be%9$#Xas#yx z8SQW)!2pMf^oaT+%`|}n8{#0$#6`Q(e$Fs<+lfik{z~ zEZ`VZod6xbYKbr$?=mSin zD`06MfyBB|=!wWONmw0D`kpj2wowB493KRA5@UOIsr9a%5B9$9ldEv64+2zIpDO@8 zQ=;*x7*Rd8A`musDL+bmJuvQGxRi9k0H5SuA1s&lmR|y7Yu9~tPPKa$GLK?dPmsBM zvz2uK_ol`m%DojO7vfZj{MMeP?@Wh;mNs$A#Z&AHC9(Av&rSl@9UifD#jxFRX0i3p zydVG@tF73k_n_D&B~fWM<9^b9TAOkhD@#=GdhQ)jB_!x?+0wCNg7b$_HlnH3cIXpd zk&;R>P2!sj9B$S2R?4nipb)hD$egs}QHBERrcew|c8lN} z6c}-Lwu5vXGBAsSdh#~|8D>OrTJ2H~O;7|*cDuQHl*F_!L9@UiU8(XjwV1>&b|2xZ zC=`DX#ei|0jNSG7jLCs2(xrrXR0r~d>!mbt`D9`2d!TxcoBTT}`uqcJU1#qs!e*p5F_d^0KWEzmpPl%YAWw~xU#PSc z6cy#cHY8jhVEUwU|vi5d~ z^Ty7F4c>KZ*x_q8+dyuznfW-19BT#xvZz?4!n; zofpzb8-H7#e^Y(_cNUGF<*zK7OzR&k+OHdwcfWl6tLRc1!=o<(U2yr43Wi-FGY*>< zrZb5?Thg@`(mbP$tq!M%L+-ba`M?V`AxzKYg#0po`f_JoY?3F6=o=j1%UX^uo`BsJ z_^o5CZzNA_I1`3S)C9MI1KBaT`##?)6cyB0!S!xPD}2@~C*EqvWzF_aH{!9!i5t^? zD1`TwFi}g{0Sw-{lRQQ%&*ha&xqnvGzXtcy|AU13lN)1Z{U_V>N7?&7fBxXB{>^Fqldt;c+P|5q zKVI2BTig7H%c8?)Vqp1enf{F({qth~Gg?}HdwoMIhd;Qc|Hf2(p53 zJUi%IBpl|Ym%MZYI(S1i%dQ+%6`^{=eYH$&{Ps?Vp6`m#ncQ=3pK z@N1*x0e4WfSh3+)#rgaq^M&W3?Hngg=o_U89y?RJu0DszF%oxF)%e4CvvCrQ$T?xH z*TuT^S>z#urp`92(^_Y5Jg?x};)VBPjrE~kzx%hXR=g#zNl)+A)hK$sLm5A~AK52o ztyl9;U6E_5VMk6s(HOfag24bFO;d_cR8;-f!r?ky7^46B~k|$L34~{ZT zJB7FHTD)YuGpKsT46T5X?pL(RRlo2Zh5)jA#tgfX(!0J$wjrm4ja777(=cGTZf3sh zQzCwL(!Fny=U|Y*GREzDC`>tgRUvN!Zb}ko)k%lMEfR=eRPF(XOF_VuC(`*bIXyJ* zY;UkGCO!F!g|X9`kc*c^4pJm9rr~C=$ap_W#y_tBQB8@a0{ID#Ysp~YbRd<|{R@{{ zd;~g>VEq2R137L6FApnPW-pIqi^a4x(f~97)&W3z-%4$RwVKjdm`JZ=@F(a^RmS8~ z(&ou(ovs=dASChT0nHX6q3qqY5mwM2Yac?s=vj(VkZP3+skCsX#HMupo?xmk=a-`l zIL+p5b%OJ;a4&-UYUFKuFtH(|f2aCNzjrvwGcPL0#BWXjqn)E}0wgtSS&zXmB>{!L zO47Qh_G6a~T1&7oa-f6{!i-Sn!dzulxr(yYWVV)}J&=G!bFr(i)g%_Qpd0Y}IiWKx z$sB1tpL1MLxd_@J=MvQcZSt#QT&MZ9P@J<(hr7@BG>^35Ecz#exK6|CQIe`1k4eA+ zsgCYlPAM;cv$@A4srBwBD=TgN{Sx|{!&i{FPOIxC$qtiij){llcXyZePb)MgW7PyL z?Zrd!QPF+H(Vb%1h(1Xc%jY4rSzJ@?`;*j|&vSDw6dhPZMuel^7}L$tf7)!HjLs#| zDDcW_d#1z|#mo$neM8CeffV2Fp*Ir>(GtQ7<0#2}P{J+{8i!Q+qRN+YDocPTsSYHV zw=h+#fG#?qpKx9&N0|i$0+e9&3WDV+mlbe3ss!R5z`0unr>J!6>~O2R|4SPKJKaK> zgo0AL(=F?+uB_g8Je9jcX`FJ!!!g1utBG+n=|M803M&M8Th3c2+k*G;>E`~^Ow%h( zWf6G_ZIP1peyHT3q!WY!Pm5LNOvCpOqpXGQY{R8h+X!fZRGr#Q)djVvL~ocSdyEo* zUukZvQ#!MypPXYq<+iVlas^`C<&RFZExpVx$S$``~H&0ty1~`v`8evcDE$^L>hoeR@bm z?-wK&1YWqugseVTx`Hf%n=kd#&#**UtdBMT=GG}N#!4G0rNIbyi zI?KY{VR!Ls+8hF{^>u@TzN%F%=0rfNV6IQan@OU{dyD1s=Zk$!%H;7s5_B87IhLIJ zrYniOFy|-xmHG4raJ?`AvrX^~(}PNfI60tX?H0KUAiq}>c9H1>6KtP}QmWP@Rw%CA zYNoounOshpXv02HLEPp&cCMcI%?|M4~UYy4cq83Z&@7S9Q#1cak0c@fPad( ze-L=&Z8Xhfo!|H0)NEY^2i>`wT`9Z>_bGXas~V$`iIW#>OuK!@Xe=Yo{(sne%jme0 zWnECr7Nf;zF*8}r%*@QpXfZQdEK9N&EM{hAMvKW}i(jd`eQx);ea|^>-mEoiX3Y&de|33!j29<)XTzcQxDNJ&2fnW1-%|q3XlbOalD#69jX#6joFy8Y#t2 zA5k6vkMYm|J|E@aU=~(rB~#}5r1-KZFIqzRv{+=XzO_3vXJA@l@p298rXP1=_2{cQ zaRd-$gS1cu?{aJFrr!zH&POof1F=&F?Gso+)|ohV1$+{PVIiLt(+7v4GGfLTxf(L# zjoh1|q5vj;#@tgAS7pm-=2yj^tDg%Z_==k=&9$Tv&q*MT>(Q7`@ADznYOM@qjB_fI z#UyDurk8Ixnhu4T>U<=IliH+PE`4&|wGq8hc@H1=Hms~5ai1kL$0_GOZ4|?C$t1zsO4t>7Z67%>g*{|)z z0Jj#iO`tFzZs{QD+I_lfQkI9a8yOE_w$mT}Y7o#ix9x`Va9|UH4iC>l`|V=*R+COv zR#@brr+y~=eHcdj`@uV_4*eYFcc8HEp)Qfi&GJ1o z&lx9GHvAUAg_N-6i*@F6){QBX3K{jwHEoqrlaRzD6@!^?+bo^u}&>lZHa+>Xw_<@ubq+=w)!YP3$7Ng6Ye0$ z))v_<8$15BQ-mvWzjH5OS)U!s5En@YGz;ZNyin*PA$kZ2`Rc@-m~=VtTw~NT;<_M! z%04kyPT?GdYAb=h_XEEsXh~FyT2UsHC_Y&_mQg39ONU&ckVg?>21S*Gx`wd%RASBI zE*p48yjfA?;3)GM`h;<(JZ^Uj0QOF`m8(4bA~zv`u`a6c=moy{g|?Al1d96tih2$z zRdTrfhe_mscNQ~(^_Wyc)k1z3FoU;ZcSaA$i&Ce}(lZwD3V17B4nf&NJ6NbFu#A|W z8X~PU3_IzlI-;FYs-r?DdeeNyKo*>2$d?r0fddh9CL(JQl6suDez@RB*mFkOHS-3g z1GB!$2EwR&f%T#c&l*cx_BPHbjx1PB>0$#{17}XHnR;h-wx#0+V%OcP0)raIn`D1I z&PY;zf!O8 zNlR2)v7x78!zj@leQq!faSvlF^0;ThY*Zm?LRB54ztetRfUm9xDPZc;ky zRtE6FB&v)y1lCA*GJza@8MDY03UrW{52Vo9^LJg+e@xxf6rryqTY%~hD#n1oc%Z$< zt2Yn6qJ`dg5y$NU`Lg~&roWF)!^_STXx-4;uT`ik6EPkEc{L9w1A`p+3uup`!G3hO zJJ&>y8JFpHx+KUt(q@vFsRT$y{x))!QA=gHVZthNy1dyuc4~LuhLmAvl^+GPBRAaj zTq9axvGQFj3B!bb_(*3axzq5@fCS1$=A@><@tME#-ND1RnsV_r=;*{FM2fjXi-r?3 zIhR_Q&WsbYkDiRy`8|v(Y9MV@srM?mwZrV?$fb1>i54!Gwd>F^qseO@V^4-5ubz0J zR!Xv=z3sE%cb3bH&4#NNmZV*;e|oC+TY_j#yV}t1ks&ulYZxW9LCutfXhLYrJKwo| z8oGV)w`Nk^sP{UyF&|k)eMW%D8ZWMLH zr8+bG@L|{sGB}Glw2*B-Hi`Wi~^YnN*_8hP#*Ap8-a1Qy|fb34>xABe(E{_J@ozPxILu4T- zDi|UG|c8ky=Q&->#eR%dO0!BSJ~QPn+8B~$rK>Cy`a1G%gBt)>1LJXb%A+@Tck zSXp9LVnRj35kA&Qy$6wkGBYQb1)J?lz6SfUCrh)ISeo))sR)BW5LlvE0VM?TiU4O{ zlH?YNFQtkxU{1~9LrbJ=(wX^^cu5?vj&Y~RC*P4L)%L?5ye5m*S0y8B&@&U|?y_g?% z5BIpamq^_Mnn@-~UPEbKB9;r0i49A1kbblWCWwM(p!{I%V`eAO3-2n6A4wGoW#Ad3 zG#f;Xci&_@$1Y7I#&@0V6ZS^{N4@SL@r)@`c(k2eAKH2SRJgn+W$fT_6rghV!ES<^1JUYas}d?a}7db`1nq3wIdyT7#S-gJae>RO(vS_%1}y5jdBo>hka zWti^S`}Xyz>CDu4WlQ&2E3j!5k4~}V#n}5|E?XZE> zacy8eL#bS=wD24LV!bopA=_PN< ziJTtPX>?PRa$xJI8^P#J<&N!`y%=h%QtbP8ELQ0Wrbz|g zimjuD!!Qe>qnaL^N=}Q1z{hkZi1|IOZTNIDv=l2b|Z_`i4i=FZn0C1R}7l`YvF|6TtuzMUHWGY` zB?RUF5$i8ztncFfuR%h_f8xmgD@e%r7mDnE1_^&_`_In(&FSO+C6EyC*8dC=0;1;s z0uuh+hy9NZAKBj6o&Nz6{$}<+014Sy>3^L!GB7g!rKl2xyzjDH`>%pAk($WGiTZT1 zq3_~3WY&7F_g05b(sxq^!WIWt`3YxS~^U+!I%MMuaW7 z7r!>#$)C&m1w;UqH4=vvd)pg(&z#-iC@x)&ad7G2^^jDK+AKfgZs;kfnyMn#cZ3{{ zE|0k`Sid$MZb&TN*I+!KC!{1FzPFwR@#bTk+fV-*&r=|wA7e4Ph{koxlI#{8rNg?T z`$Q#Uo;RuVUT6ma^HELq+iHk;IIFEZN0{oyp$rOa@zGC(S-pi#C_*6Z_tXquWH1c| zdYx7)ODR~K?Rg7izD9hz3ZB(pM-uyTU1c#TWd%&fg38#8DNQNjBt{a0ODj2j1uoW0Z{RLP2=riSuqlcjWd45tAZy{PlvAZGn65VOnA*QCIz+VQyyB6$Y(|t(ndC z$nSAF4%tx4ECzH51-KT4H{y)TQ7I^;v5q?XlL(x?Ws$%`L}Z0fa`3PnfL~s7f87F3 z!y|}t{{o>6azBrA@k!4S7=%%o0EJvbEOf6msqr8rqfIP_w^%Uya89OPHF8sR z6t?M{K%Gg=Uh@EA@<86VeNU~Aen3!NoXrhB@} zv^xc|Eq27qusKH?ynT-`2!Tx~<0G1t*mu7{w%)$wZbic!t^E7cY;*0CyAynT1{zP@ zMNOWcDyjABd6ftIpYD=>8q#BLj~QH6pKDX>92Ea}#YOf3@k;)gbsOT))PvP-G~T)N zVX}EvUghL$#-{?Rs&1!4lrVUIochH7LVM3*ZculYfOiLg8)`gfY-Dqvwg-Ou8NX9e z>(_eO<9LOys*hd!x3{0S`TyTdjRXv=e~FmTmi;aJZuJW0m4E^cy!c1~9Rd-AHv*tjz75jsJAR{hQo6 zK#A+Oo7jKhhm7=p4$JwiAooA;!~eT->)8HM+54Z7$T#^7W&BrY<(m#O``Zop7asZc z|97nN4axlD>dnfZGVA~~6@JUK`$roFj=$iSzjelcu!8t2e#yxCCpIELp^3k(DgKse z#}0rm|2u@q_)kd;(3>wq%vKhqS5A3H%%*Wz8~IT6dT(K3@0Mi4HnuZHVbA13egfK zZjGmq(Op}NTR4_#o`A`nrk2(lCbyg_gUn?<)5-j<7Y`XwHC#WvKMT6r-a_v6_2a%S zkDX3JS~$m@*Imb5##OfC%fkIR2n5j2Ekv~{lbhydk+NV zbh`m9w|J8~YauCTkggk**$eGaDZLftkk>!=pj(ALHajo4cz+B|dEjZmUoSo1$hq~z zTdvq>*dw3;Ayfmu>jp}wC%^+5!~ZUY&L4cC8*Au;J@Rf4bKt%V2XZ34oesNS_bvuI zA!K0dZ2Q|Lu;o>VWq;S#Xc4ZE0237(zs zHd~V8M@dr*RMmy&S?$$=BQOKYDK{AX22j z^cfDSyfWepE4Ddk>waKFKdvNX=*nzzd9($>nHut6Ip?G-v;0Ak`jR4ezFCKaBT@D9 zsBaBSRjb4BpaDor4r-F$_qc@f*7o&aQji6Ix!u}qqhb!<-OkJnA2tG--W_EmViF4f zIz@0B1llYJ)rp_2EU-mhL8XVM3VmLX&dQH>5z6mU9vp?e0%era5G2F}=_i@WRW9%f zhN0|PiS4I%BXJ4|?XErBSQ9+#)7PGKVXw8G`0KKDA-3DGOkB)c60`&`ew4L0301wX=6%ul)R*33qMTqx8p(qLp*v7e{7% zj&uZNNP@wgx?_*1H*1=2?|?gJnCnL5Zzc9dpo801=6Jh(=sa3h z5czlNvxPuzqVN3u+b__J=dmf7ObGZ}>B806u+%4jlJxjUr!sg#_fdS>-U0prXW#je zu4gQ)4PZb&aL4%7&XF8#jO^&lszAi#EHk(P-AF4k|HLz*{E_aLPtFHrWhOWoRe%s7 zEZbh1#lEEswosiK{@Kqdiz?I7E{Y_$lVMPmW<`#!rP^+!M~;kU=1Q+DdXZKFcEKVSAeua+xlep#>Ba=A6)aZm{U8l{3PPvF;}RQzr6c;^NxJX4!Ef)H*l_}pGa&AA=!ix zkGV(&X_pFyoxuipQN$$322bQiU~}~(oN`Tta-nlMG9*=J7mQsM)Vo{B(647U6r23G z;hP$wlL$h3Na%Ps6gRUXkQn|QotK#CIwxpPfra+yDQj1}mv$hy2Ncw!Jse=%oyU`3 z_$Yvv8|R={4VR+&mPv;3Nwg7U6kjxmX2UNR1nA43>NM^@OyWW0CLf|1`DBrTHXTw+ zkn%)+!568LN5n=W)>aP&Jupct1Yj~) zYAj`SzyQ-C8Lo}Smg8yak_Vg6+k&|3#<|hK-~@x903OKE;R^t{Y0c?uK0uOb8+~<> zq1lVSv-gLtdXrN!@&xgt9^KY3(qFl|qBi<nDY19(;IiQrww*68p8&E*KicaGm4 z88OB(XH>E*n=L$8om4$mqo{aSXYb`MiW1P&N$2{Kl>35i?wpcc#$=Z{rk-|%jdRtA z

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 z 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 lK<~1Vu<^8Tm9z!#yT0IE+VA(vE-(SKdloS Date: Wed, 7 Oct 2026 15:19:10 +0800 Subject: [PATCH 02/11] fix: CPU bf16 embedding + UTF-8 I/O + resume checkpoint; add query client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - embed_cpu/api: use bfloat16 on CPU (fits 16GB RAM); .float() before numpy cast - embed_cpu: persist/resume embed checkpoint so interrupted runs continue - pipelines/pdf: explicit UTF-8 encoding for text/json I/O - CLAUDE.md: expand layout/commands/architecture docs - add local knowledge-base query client (query.py, pixelrag_client.py, *.bat, 使用说明) - .gitignore: exclude papers/, knowledge_index/, e2e_test/index/ --- .gitignore | 5 + CLAUDE.md | 83 +++- ...G\345\256\242\346\210\267\347\253\257.bat" | 3 + embed/src/pixelrag_embed/embed_cpu.py | 68 +++- index/src/pixelrag_index/pipelines.py | 6 +- pixelrag_client.py | 377 ++++++++++++++++++ query.bat | 5 + query.py | 132 ++++++ render/src/pixelrag_render/backends/pdf.py | 4 +- serve/src/pixelrag_serve/api.py | 4 +- start_serve.bat | 24 ++ ...7\347\224\250\350\257\264\346\230\216.txt" | 30 ++ 12 files changed, 723 insertions(+), 18 deletions(-) create mode 100644 "PixelRAG\345\256\242\346\210\267\347\253\257.bat" create mode 100644 pixelrag_client.py create mode 100644 query.bat create mode 100644 query.py create mode 100644 start_serve.bat create mode 100644 "\347\237\245\350\257\206\345\272\223\344\275\277\347\224\250\350\257\264\346\230\216.txt" diff --git a/.gitignore b/.gitignore index cdf3130..83bc0f0 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,8 @@ node_modules/ tmp/ demos/demo_tiles/ assets/*.mp4 + +# Research knowledge base (local only — papers are large & copyrighted, indexes are generated) +papers/ +knowledge_index/ +e2e_test/index/ diff --git a/CLAUDE.md b/CLAUDE.md index 87aeed0..53ec322 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,7 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + # PixelRAG Visual RAG: render documents (web pages, PDFs) as screenshot tiles and retrieve over the images @@ -6,16 +10,87 @@ for how it runs in production. ## Layout - `render/` — `pixelshot` capture (Playwright/CDP, PDF) -- `embed/`, `index/` — tiles → vectors → FAISS index -- `serve/` — FAISS search API (`pixelrag serve`) +- `embed/` — tiles → chunks → vectors → index (the `chunk`, `embed`, `build-index` stages) +- `index/` — `pixelrag index` orchestrator + pluggable document sources +- `serve/` — FAISS/Qdrant search API (`pixelrag serve`) - `web/` — Next.js frontend (on Vercel) + `agent-server.mjs` (the chat agent backend) - `train/` — **separate uv project** (LoRA finetune); install from inside `train/`, not the root +- `eval/` — **separate uv project** reproducing paper Table 1 (reader + grader over HTTP) - `deploy/` — systemd units, CD workflow, blue-green scripts +## Commands + +Python (uv only — `uv add`, never `uv pip install`; work in `.venv`; commit `uv.lock`): + +```bash +# Install dev deps and run the full Python suite (render tests need Chrome too) +uv sync --extra dev +uv run pixelshot install-chrome # one-time; CI also runs `npx -y playwright install-deps chromium` +uv run pytest tests/ -v +uv run pytest tests/test_cli.py::test_ -v # single test + +# Lint / format (ruff; config in pyproject.toml excludes train/, demos/, docs/, skill/) +uvx ruff check . +uvx ruff format --check . + +# Pipeline: build an index from a pixelrag.yaml config, then serve it +pixelrag index build -c pixelrag.yaml +pixelrag serve --index-dir ./index --port 30001 +``` + +Frontend (`cd web`): + +```bash +npm ci +npm test # node --test lib/*.test.mjs +npm run build +npm run typecheck # tsc --noEmit +``` + +## Architecture + +**One distribution, many packages.** `pixelrag` is the single PyPI package (`pyproject.toml` +bundles `src/pixelrag` plus `render/embed/index/serve` under `src/`). Two entry points: the +standalone `pixelshot` (`pixelrag_render.render:main`) and the `pixelrag` umbrella +(`pixelrag.cli:main`). Heavy ML stages are opt-in extras (`embed`, `index`, `serve`, `qdrant`, `pdf`). + +**CLI dispatch is lazy.** `src/pixelrag/cli.py` maps `pixelrag ` to a stage module and +imports it on demand, printing an install hint if the extra is missing. Stage 0 (render) is the +separate `pixelshot` command, kept torch-free in core. + +**`pixelrag index` orchestrates by subprocess.** `index/src/pixelrag_index/pipelines.py:build` +walks `source → ingest (render tiles) → chunk → embed → build-index`, shelling out to +`python -m pixelrag_embed.chunk / .embed_cpu / .embed / .index` rather than importing them. +Device routing happens here: `device: cpu|mps|auto` → `embed_cpu.py`, `cuda` → `embed.py` +(vLLM/sglang). + +**Tile data model.** Each document gets a numeric position index. Tiles live in +`/tiles/.png.tiles/` containing `tile_XXXX.jpg`, a `tiles.json` manifest +(`article_id`, `source`, `page_height`, `tiles`, `complete`), and — after chunking — +`chunks.json` + `chunk_XXXX_YY.png` (≤1024px strips). `embed` reads `article_id` from the +manifest, never from the directory name. `/articles.json` maps position index → +title/url/department (department = first subdirectory under a local source root, used for +filtered search). Manifests are written atomically (`*.tmp` + `os.replace`) and `_needs_render` +uses the `source` field to detect stale tile dirs when the source set changes between runs. + +**Config.** `pixelrag.yaml` is deep-merged over `DEFAULT_CONFIG` in +`index/src/pixelrag_index/config.py` (`ingest`, `embed` sections merge key-wise). Sources are +pluggable via the `SOURCES` registry in `index/src/pixelrag_index/sources/__init__.py` +(`local`, `web`, `kiwix`, `pdf`); each yields `Document(id, url, path, metadata)`. + +**Serve.** `serve/src/pixelrag_serve/api.py` is a FastAPI app serving FAISS (default) or Qdrant +backends, with text and image queries. It also holds `render_ondemand.py` (render retrieved +pages at query time from a kiwix ZIM) and `zim_server*.py`. `web/agent-server.mjs` calls +`/search`; its `/health` is liveness and `/ready` is readiness (see `deploy/README.md`). + +**Training and eval are separate uv projects.** `train/` LoRA-fine-tunes +`Qwen/Qwen3-VL-Embedding-2B` (`train_contrastors.py` is the primary script); it has its own +`train/CLAUDE.md` and `train/README.md` — read those before touching it. `eval/` is a pure HTTP +client (reader + grader) that reproduces paper numbers; also self-contained. + ## Conventions -- Python: `uv` only (`uv add`, never `uv pip install`); work in `.venv`; commit `uv.lock`. -- One distribution (`pixelrag`) with extras; the CLIs are `pixelshot` and `pixelrag `. - `main` is branch-protected — land changes via PRs. +- Releasing is automated: `uv version X.Y.Z`, tag `vX.Y.Z`, GitHub Release → `.github/workflows/release.yml` publishes `pixelrag` to PyPI. ## Operational context (host-specific, kept out of this public repo) Deploy/runtime details that are specific to the live host are **not** committed here. They are diff --git "a/PixelRAG\345\256\242\346\210\267\347\253\257.bat" "b/PixelRAG\345\256\242\346\210\267\347\253\257.bat" new file mode 100644 index 0000000..ff5b3dc --- /dev/null +++ "b/PixelRAG\345\256\242\346\210\267\347\253\257.bat" @@ -0,0 +1,3 @@ +@echo off +cd /d "%~dp0" +start "" ".venv\Scripts\pythonw.exe" pixelrag_client.py diff --git a/embed/src/pixelrag_embed/embed_cpu.py b/embed/src/pixelrag_embed/embed_cpu.py index 08b4c33..ca8a0c9 100644 --- a/embed/src/pixelrag_embed/embed_cpu.py +++ b/embed/src/pixelrag_embed/embed_cpu.py @@ -142,18 +142,33 @@ def scan_chunks(shard_dir: str) -> list[dict]: return items +def _save_checkpoint(path: Path, embeddings: np.ndarray, done: int) -> None: + """Atomically persist a resume checkpoint holding the first `done` rows.""" + tmp = path.parent / (path.name + ".tmp.npz") + np.savez(tmp, embeddings=embeddings[:done], done=np.int64(done)) + os.replace(tmp, path) + + def embed_items( items: list[dict], model_name: str, device: str = "cpu", instruction: str = "", -) -> np.ndarray: - """Embed image items using transformers on the given device.""" + checkpoint_dir: str | None = None, + checkpoint_every: int = 10, +) -> tuple[np.ndarray, Path | None]: + """Embed image items using transformers on the given device. + + When ``checkpoint_dir`` is set, progress is written to + ``/embed_checkpoint.npz`` every ``checkpoint_every`` + chunks and resumed from there on restart, so an interrupted CPU embed + continues instead of restarting. Returns ``(embeddings, checkpoint_path)``. + """ import torch from transformers import AutoProcessor, Qwen3VLForConditionalGeneration device = _resolve_device(device) - dtype = torch.float32 if device == "cpu" else torch.float16 + dtype = torch.bfloat16 if device == "cpu" else torch.float16 logger.info("Loading model %s on %s (%s)...", model_name, device, dtype) processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) @@ -170,9 +185,36 @@ def embed_items( dim = model.config.text_config.hidden_size embeddings = np.zeros((len(items), dim), dtype=np.float16) + checkpoint_path = None + start = 0 + if checkpoint_dir: + checkpoint_path = Path(checkpoint_dir) / "embed_checkpoint.npz" + if checkpoint_path.exists(): + try: + with np.load(checkpoint_path) as ck: + partial = ck["embeddings"] + done = int(ck["done"]) + if partial.shape == (done, dim) and 0 < done <= len(items): + embeddings[:done] = partial + start = done + logger.info( + "Resuming from checkpoint: %d/%d chunks already embedded", + done, + len(items), + ) + else: + logger.warning( + "Ignoring unusable checkpoint (shape=%s, done=%d)", + partial.shape, + done, + ) + except (OSError, ValueError, KeyError): + logger.warning("Ignoring unreadable checkpoint at %s", checkpoint_path) + prefix = f"Instruct: {instruction}\n" if instruction else "" - for i, item in enumerate(tqdm(items, desc="Embedding")): + for i in tqdm(range(start, len(items)), desc="Embedding", initial=start): + item = items[i] img = Image.open(item["path"]).convert("RGB") img = _clamp_width(img) @@ -202,7 +244,10 @@ def embed_items( last_idx = seq_lens - 1 pooled = last_hidden[0, last_idx[0]] pooled = pooled / pooled.norm() - embeddings[i] = pooled.cpu().numpy().astype(np.float16) + embeddings[i] = pooled.cpu().float().numpy().astype(np.float16) + + if checkpoint_path and (i + 1) % checkpoint_every == 0: + _save_checkpoint(checkpoint_path, embeddings, i + 1) # tqdm covers interactive runs but never reaches log files; when # stderr is not a TTY (nohup/CI/redirects) emit a periodic record so @@ -210,7 +255,7 @@ def embed_items( if (i + 1) % 100 == 0 and not sys.stderr.isatty(): logger.info("Embedded %d/%d", i + 1, len(items)) - return embeddings + return embeddings, checkpoint_path def main(): @@ -244,8 +289,12 @@ def main(): items = items[: args.limit] else: logger.info("Found %d chunks to embed", len(items)) - embeddings = embed_items( - items, args.model, device=args.device, instruction=args.instruction + embeddings, checkpoint_path = embed_items( + items, + args.model, + device=args.device, + instruction=args.instruction, + checkpoint_dir=args.output_dir, ) output_path = Path(args.output_dir) / "shard_000.npz" @@ -259,6 +308,9 @@ def main(): tile_heights=np.array([it["height"] for it in items], dtype=np.int32), ) + if checkpoint_path and checkpoint_path.exists(): + checkpoint_path.unlink() + logger.info("Saved %d embeddings to %s", len(items), output_path) diff --git a/index/src/pixelrag_index/pipelines.py b/index/src/pixelrag_index/pipelines.py index a7118b4..70002e1 100644 --- a/index/src/pixelrag_index/pipelines.py +++ b/index/src/pixelrag_index/pipelines.py @@ -213,7 +213,7 @@ def _repl(m: re.Match) -> str: if not _needs_render(tiles_dir, idx, doc): continue src_path = Path(doc.path) - content = src_path.read_text(errors="replace") + content = src_path.read_text(encoding="utf-8", errors="replace") ext = (doc.metadata or {}).get("extension", src_path.suffix.lower()) if ext == ".md": body = md_lib.markdown( @@ -224,7 +224,7 @@ def _repl(m: re.Match) -> str: body = _resolve_relative_paths(body, str(src_path.parent)) html_content = f"{_HTML_TEMPLATE}{body}" html_path = Path(tmp_dir) / f"{idx}.html" - html_path.write_text(html_content) + html_path.write_text(html_content, encoding="utf-8") text_urls.append(f"file://{html_path.resolve()}") text_stems.append(str(idx)) @@ -349,7 +349,7 @@ def _repl(m: re.Match) -> str: article_entries.append( {"title": title, "url": url, "department": _department_of(a, source_root)} ) - with open(articles_path, "w") as f: + with open(articles_path, "w", encoding="utf-8") as f: json.dump(article_entries, f) logger.info( " Saved %d article mappings to %s", len(article_entries), articles_path diff --git a/pixelrag_client.py b/pixelrag_client.py new file mode 100644 index 0000000..41addba --- /dev/null +++ b/pixelrag_client.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""PixelRAG 知识库桌面客户端(DeepSeek 风格深色聊天界面)。 + +打开即自动检测/启动检索服务,支持中文或英文查询,返回最相关的论文页面 +(标题 + 相似度 + 页面截图),点击标题打开论文 PDF,点击截图看大图。 + +依赖:仅标准库 tkinter + 已安装的 Pillow,无需额外安装。 +""" +import base64 +import io +import json +import os +import subprocess +import sys +import threading +import time +import urllib.request + +import tkinter as tk +from PIL import Image, ImageTk + +ROOT = os.path.dirname(os.path.abspath(__file__)) +HOST = "localhost" +PORT = 30001 +N_DOCS = 5 +HEALTH_URL = f"http://{HOST}:{PORT}/health" +SEARCH_URL = f"http://{HOST}:{PORT}/search" + +# ---- 配色(参考 DeepSeek 深色主题)---- +BG = "#1f1f1f" # 主背景 +BAR_BG = "#141414" # 顶栏/底栏背景 +CARD_BG = "#2a2a2a" # 结果卡片背景 +USER_BG = "#4d6bfe" # 用户气泡(蓝) +USER_FG = "#ffffff" +TEXT_FG = "#e6e6e6" # 主文字 +MUTED_FG = "#9e9e9e" # 次要文字 +ACCENT = "#7a9bff" # 可点击标题 +BORDER = "#3a3a3a" +GREEN = "#4ade80" # 服务运行中 +YELLOW = "#facc15" # 启动中 +RED = "#f87171" # 失败 + +FONT = "Microsoft YaHei UI" + + +# --------------------------------------------------------------------------- +# 服务与检索 +# --------------------------------------------------------------------------- + +def check_health() -> bool: + try: + urllib.request.urlopen(HEALTH_URL, timeout=3) + return True + except Exception: + return False + + +def _serve_cmd() -> list[str]: + exe = os.path.join(ROOT, ".venv", "Scripts", "pixelrag.exe") + return [ + exe, "serve", + "--index-dir", os.path.join(ROOT, "knowledge_index"), + "--tiles-dir", os.path.join(ROOT, "knowledge_index", "tiles"), + "--articles-json", os.path.join(ROOT, "knowledge_index", "articles.json"), + "--device", "cpu", + "--port", str(PORT), + ] + + +def start_serve_process() -> subprocess.Popen: + env = dict(os.environ) + env["HF_HUB_OFFLINE"] = "1" + env["TRANSFORMERS_OFFLINE"] = "1" + flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) + return subprocess.Popen( + _serve_cmd(), env=env, creationflags=flags, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + + +def search(text: str, n_docs: int = N_DOCS) -> dict: + payload = json.dumps( + {"queries": [{"text": text}], "n_docs": n_docs, "include_images": True} + ).encode("utf-8") + req = urllib.request.Request( + SEARCH_URL, data=payload, + headers={"Content-Type": "application/json"}, method="POST", + ) + with urllib.request.urlopen(req, timeout=900) as r: + return json.loads(r.read().decode("utf-8")) + + +def title_of(url: str) -> str: + name = os.path.basename((url or "").replace("\\", "/")) + if name.lower().endswith(".pdf"): + name = name[:-4] + return name + + +def decode_b64_image(b64: str) -> Image.Image: + return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB") + + +# --------------------------------------------------------------------------- +# 界面 +# --------------------------------------------------------------------------- + +class App: + def __init__(self, root: tk.Tk): + self.root = root + self.root.title("PixelRAG 知识库检索") + self.root.geometry("880x760") + self.root.minsize(640, 560) + self.root.configure(bg=BG) + + self._photos: list[ImageTk.PhotoImage] = [] # 防止图片被 GC + self._serve_ready = check_health() + self._busy = False + + self._build_ui() + self._add_note("欢迎使用 PixelRAG 知识库检索。\n输入中文或英文问题,回车查询。") + self._set_status() + + if not self._serve_ready: + threading.Thread(target=self._ensure_serve, daemon=True).start() + + # ---------- UI 构建 ---------- + + def _build_ui(self): + # 顶栏状态 + self.status = tk.Label( + self.root, text="", bg=BAR_BG, fg=MUTED_FG, anchor="w", + padx=16, pady=7, font=(FONT, 10), + ) + self.status.pack(side="top", fill="x") + + # 消息区(Canvas + Frame + 滚动条) + body = tk.Frame(self.root, bg=BG) + body.pack(side="top", fill="both", expand=True) + + self.canvas = tk.Canvas(body, bg=BG, highlightthickness=0) + self.scroll = tk.Scrollbar( + body, orient="vertical", command=self.canvas.yview, + bg=CARD_BG, troughcolor=BG, activebackground=BORDER, + borderwidth=0, width=12, + ) + self.canvas.configure(yscrollcommand=self.scroll.set) + self.scroll.pack(side="right", fill="y") + self.canvas.pack(side="left", fill="both", expand=True) + + self.frame = tk.Frame(self.canvas, bg=BG) + self._win = self.canvas.create_window((0, 0), window=self.frame, anchor="nw") + self.frame.bind( + "", + lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", + lambda e: self.canvas.itemconfigure(self._win, width=e.width), + ) + + # 底部输入栏 + bar = tk.Frame(self.root, bg=BAR_BG) + bar.pack(side="bottom", fill="x") + + self.input = tk.Text( + bar, height=3, bg=CARD_BG, fg=TEXT_FG, insertbackground=TEXT_FG, + relief="flat", font=(FONT, 12), padx=12, pady=10, wrap="word", + ) + self.input.pack(side="left", fill="x", expand=True, padx=(16, 8), pady=12) + self.input.bind("", self._on_enter) + self.input.insert("1.0", "") + self.input.focus_set() + + self.send_btn = tk.Button( + bar, text="发送", command=self._on_send, + bg=USER_BG, fg=USER_FG, activebackground="#5a7cff", + activeforeground="#ffffff", relief="flat", font=(FONT, 11, "bold"), + padx=22, pady=8, cursor="hand2", + ) + self.send_btn.pack(side="right", padx=(0, 16), pady=12) + + # ---------- 状态 ---------- + + def _set_status(self, state: str | None = None): + if state == "starting": + self.status.configure(text="● 正在启动检索服务...", fg=YELLOW) + elif state == "failed": + self.status.configure(text="● 服务启动失败(见控制台)", fg=RED) + elif self._serve_ready: + self.status.configure(text="● 服务运行中 · 端口 30001", fg=GREEN) + else: + self.status.configure(text="● 服务启动中...", fg=YELLOW) + + # ---------- 消息渲染 ---------- + + def _add_user(self, text: str): + row = tk.Frame(self.frame, bg=BG) + row.pack(fill="x", padx=16, pady=(14, 2)) + lbl = tk.Label( + row, text=text, bg=USER_BG, fg=USER_FG, justify="left", + wraplength=520, font=(FONT, 12), padx=14, pady=9, + ) + lbl.pack(side="right", anchor="e") + self._scroll_bottom() + + def _add_note(self, text: str): + lbl = tk.Label( + self.frame, text=text, bg=BG, fg=MUTED_FG, justify="center", + font=(FONT, 11), padx=16, pady=10, + ) + lbl.pack(fill="x", padx=16) + self._scroll_bottom() + + def _add_error(self, text: str): + lbl = tk.Label( + self.frame, text=text, bg="#3a1d1d", fg=RED, justify="left", + wraplength=700, font=(FONT, 11), padx=14, pady=10, + ) + lbl.pack(fill="x", padx=16, pady=8) + self._scroll_bottom() + + def _add_result_card(self, hit: dict, idx: int): + card = tk.Frame( + self.frame, bg=CARD_BG, highlightthickness=1, + highlightbackground=BORDER, + ) + card.pack(fill="x", padx=16, pady=(0, 14)) + + title = title_of(hit.get("url", "")) + ti = hit.get("tile_index", 0) + score = hit.get("score", 0.0) + + # 标题(点击打开 PDF) + title_lbl = tk.Label( + card, text=f"[{idx}] {title}", bg=CARD_BG, fg=ACCENT, + font=(FONT, 11, "bold"), justify="left", anchor="w", + wraplength=760, cursor="hand2", padx=14, + ) + title_lbl.pack(fill="x", pady=(12, 2)) + title_lbl.bind( + "", + lambda e, u=hit.get("url", ""): self._open_pdf(u), + ) + + # 元信息 + meta = tk.Label( + card, text=f"页面 tile_{ti:04d} · 相似度 {score:.3f} · 点击标题打开 PDF", + bg=CARD_BG, fg=MUTED_FG, font=(FONT, 9), anchor="w", padx=14, + ) + meta.pack(fill="x", pady=(0, 8)) + + # 页面截图缩略图(点击看大图) + b64 = hit.get("image_base64") + if b64: + try: + img = decode_b64_image(b64) + img.thumbnail((320, 460), Image.LANCZOS) + photo = ImageTk.PhotoImage(img) + self._photos.append(photo) + img_lbl = tk.Label(card, image=photo, bg=CARD_BG, cursor="hand2") + img_lbl.image = photo + img_lbl.pack(padx=14, pady=(0, 14)) + img_lbl.bind( + "", + lambda e, b=b64, t=title: self._open_image(b, t), + ) + except Exception: + pass + + self._scroll_bottom() + + def _scroll_bottom(self): + self.root.after_idle(lambda: self.canvas.yview_moveto(1.0)) + + # ---------- 交互 ---------- + + def _on_enter(self, event): + self._on_send() + return "break" # 阻止 Enter 插入换行 + + def _on_send(self): + if self._busy: + return + text = self.input.get("1.0", "end").strip() + if not text: + return + self.input.delete("1.0", "end") + self._add_user(text) + + if not self._serve_ready: + self._add_note("检索服务尚未就绪,正在启动,请稍候几秒再试...") + threading.Thread(target=self._ensure_serve, daemon=True).start() + return + + self._busy = True + self._add_note("⏳ 正在检索,约需 1 分钟...") + threading.Thread(target=self._do_search, args=(text,), daemon=True).start() + + def _do_search(self, text: str): + try: + resp = search(text) + hits = resp["results"][0]["hits"] + self.root.after(0, lambda: self._show_results(hits)) + except Exception as e: + self.root.after(0, lambda: self._show_error(e)) + finally: + self.root.after(0, self._clear_busy) + + def _show_results(self, hits: list): + if not hits: + self._add_note("没有找到相关结果,换个说法试试。") + return + for i, h in enumerate(hits, 1): + self._add_result_card(h, i) + + def _show_error(self, e: Exception): + self._add_error( + f"查询失败:{e}\n请确认检索服务已启动、索引文件完整。" + ) + + def _clear_busy(self): + self._busy = False + + # ---------- 打开图片 / PDF ---------- + + def _open_image(self, b64: str, title: str): + try: + img = decode_b64_image(b64) + except Exception: + return + win = tk.Toplevel(self.root) + win.title(title or "页面截图") + win.configure(bg="#000000") + max_w = int(win.winfo_screenwidth() * 0.85) + max_h = int(win.winfo_screenheight() * 0.85) + img.thumbnail((max_w, max_h), Image.LANCZOS) + photo = ImageTk.PhotoImage(img) + self._photos.append(photo) + lbl = tk.Label(win, image=photo, bg="#000000") + lbl.image = photo + lbl.pack() + + def _open_pdf(self, url: str): + if url and os.path.exists(url): + os.startfile(url) + else: + self._add_note("找不到该论文 PDF 文件。") + + # ---------- 服务 ---------- + + def _ensure_serve(self): + if check_health(): + self._serve_ready = True + self.root.after(0, self._set_status) + return + self.root.after(0, self._set_status, "starting") + start_serve_process() + deadline = time.time() + 180 + while time.time() < deadline: + time.sleep(2) + if check_health(): + self._serve_ready = True + self.root.after(0, self._set_status) + return + self.root.after(0, self._set_status, "failed") + + +def main(): + root = tk.Tk() + App(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/query.bat b/query.bat new file mode 100644 index 0000000..b553eda --- /dev/null +++ b/query.bat @@ -0,0 +1,5 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +.venv\Scripts\python.exe query.py +pause diff --git a/query.py b/query.py new file mode 100644 index 0000000..fcd925c --- /dev/null +++ b/query.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""PixelRAG 知识库中文查询脚本(交互式)。 + +用法:先双击 start_serve.bat 启动服务,再双击 query.bat(或运行本脚本)。 +输入中文问题,返回最相关的论文页面;输入编号可直接打开该页截图, +输入 p+编号 打开对应论文的 PDF。 +""" +import json +import os +import sys +import urllib.request + +HOST = "localhost" +PORT = 30001 +N_DOCS = 5 + +_ROOT = os.path.dirname(os.path.abspath(__file__)) +TILES_DIR = os.path.join(_ROOT, "knowledge_index", "tiles") + + +def _safe(s: str) -> str: + """把控制台无法编码的字符替换掉,避免 UnicodeEncodeError(如 ∕、₃)。""" + enc = getattr(sys.stdout, "encoding", None) or "utf-8" + try: + s.encode(enc) + return s + except UnicodeEncodeError: + return s.encode(enc, errors="replace").decode(enc) + + +def search(text: str, n_docs: int = N_DOCS) -> dict: + payload = json.dumps( + {"queries": [{"text": text}], "n_docs": n_docs} + ).encode("utf-8") + req = urllib.request.Request( + f"http://{HOST}:{PORT}/search", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=900) as r: + return json.loads(r.read().decode("utf-8")) + + +def title_of(url: str) -> str: + name = os.path.basename(url.replace("\\", "/")) + if name.lower().endswith(".pdf"): + name = name[:-4] + return name + + +def page_image_path(article_id: int, tile_index: int) -> str: + d = os.path.join(TILES_DIR, f"{article_id}.png.tiles") + for ext in ("jpg", "jpeg", "png"): + p = os.path.join(d, f"tile_{tile_index:04d}.{ext}") + if os.path.exists(p): + return p + return "" + + +def main() -> None: + print("=" * 62) + print("PixelRAG 知识库检索") + print("输入中文问题后回车查询;直接回车退出。") + print("=" * 62) + + while True: + try: + text = input("\n查询 > ").strip() + except (EOFError, KeyboardInterrupt): + print("\n再见。") + break + if not text: + print("再见。") + break + + print(f"正在检索「{_safe(text)}」...(约 1 分钟)") + try: + resp = search(text) + except Exception as e: + print(f"查询失败:{e}") + print("请确认服务已启动(双击 start_serve.bat)。") + continue + + hits = resp["results"][0]["hits"] + if not hits: + print("没有找到相关结果,换个说法试试。") + continue + + print(f"\n最相关的 {len(hits)} 个页面:") + for i, h in enumerate(hits, 1): + t = title_of(h.get("url", "")) + score = h.get("score", 0) + ti = h.get("tile_index", 0) + print(f" [{i}] {_safe(t)} (tile_{ti:04d}, 相似度 {score:.3f})") + + print("\n输入编号打开该页截图;输入 p+编号 打开论文 PDF;直接回车继续:") + try: + choice = input("> ").strip().lower() + except (EOFError, KeyboardInterrupt): + break + if not choice: + continue + + open_pdf = choice.startswith("p") + num = choice[1:] if open_pdf else choice + try: + idx = int(num) - 1 + h = hits[idx] + except (ValueError, IndexError): + print("编号无效。") + continue + + if open_pdf: + pdf = h.get("url", "") + if pdf and os.path.exists(pdf): + os.startfile(pdf) + print(f"已打开论文:{_safe(title_of(pdf))}") + else: + print("找不到 PDF 文件。") + else: + img = page_image_path(h["article_id"], h["tile_index"]) + if img: + os.startfile(img) + print(f"已打开页面截图:{_safe(os.path.basename(img))}") + else: + print("找不到该页截图,可能未渲染。") + + +if __name__ == "__main__": + main() diff --git a/render/src/pixelrag_render/backends/pdf.py b/render/src/pixelrag_render/backends/pdf.py index 7241105..44f1530 100644 --- a/render/src/pixelrag_render/backends/pdf.py +++ b/render/src/pixelrag_render/backends/pdf.py @@ -123,7 +123,7 @@ def render_pdf( # complete — see #139 on what an unconditional flag costs downstream. "complete": pages is None, } - with open(tile_dir / "tiles.json", "w") as f: + with open(tile_dir / "tiles.json", "w", encoding="utf-8") as f: json.dump(manifest, f) # Write chunks.json so the chunker skips this directory — @@ -137,7 +137,7 @@ def render_pdf( "num_chunks": len(chunks_info), "chunks": chunks_info, } - with open(tile_dir / "chunks.json", "w") as f: + with open(tile_dir / "chunks.json", "w", encoding="utf-8") as f: json.dump(chunks_manifest, f) logger.info("PDF rendered: %d pages → %s", len(saved_tiles), tile_dir) diff --git a/serve/src/pixelrag_serve/api.py b/serve/src/pixelrag_serve/api.py index 01c4716..59c914b 100644 --- a/serve/src/pixelrag_serve/api.py +++ b/serve/src/pixelrag_serve/api.py @@ -711,7 +711,9 @@ def load(args): import torch device = args.device - dtype = torch.float32 if device == "cpu" else torch.bfloat16 + # 本机 16GB 内存:CPU 也用 bfloat16(4GB),与 embed 阶段一致。float32(8GB) + # 会挤爆内存并把模型权重换出到 pagefile,导致单次查询被拖到几十分钟。 + dtype = torch.bfloat16 # Load summary (index loading itself lives in the backend: FaissBackend # honors PIXELRAG_INDEX_MMAP for memory-mapped multi-100G indexes). diff --git a/start_serve.bat b/start_serve.bat new file mode 100644 index 0000000..73efe88 --- /dev/null +++ b/start_serve.bat @@ -0,0 +1,24 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" + +rem 若服务已在运行则不再重复启动 +curl -s --max-time 2 http://localhost:30001/health >nul 2>&1 +if %errorlevel%==0 ( + echo 服务已在运行(端口 30001),无需重复启动。 + echo 直接双击 query.bat 开始查询即可。 + pause + exit /b 0 +) + +echo 正在启动 PixelRAG 检索服务,加载模型约需 1 分钟... +echo 看到 "Uvicorn running on http://0.0.0.0:30001" 即表示启动成功。 +echo. +echo 提示:请保持本窗口开启,关闭本窗口即停止服务。 +echo. + +set HF_HUB_OFFLINE=1 +set TRANSFORMERS_OFFLINE=1 +.venv\Scripts\pixelrag.exe serve --index-dir knowledge_index --tiles-dir knowledge_index/tiles --articles-json knowledge_index/articles.json --device cpu --port 30001 + +pause diff --git "a/\347\237\245\350\257\206\345\272\223\344\275\277\347\224\250\350\257\264\346\230\216.txt" "b/\347\237\245\350\257\206\345\272\223\344\275\277\347\224\250\350\257\264\346\230\216.txt" new file mode 100644 index 0000000..3879e7e --- /dev/null +++ "b/\347\237\245\350\257\206\345\272\223\344\275\277\347\224\250\350\257\264\346\230\216.txt" @@ -0,0 +1,30 @@ +======================================== + 你的知识库目录 —— 把论文 PDF 放这里 +======================================== + +【怎么添加论文】 + 用文件资源管理器,把你下载的 PDF 论文 + 直接复制 / 粘贴到这个文件夹里即可。 + 支持 .pdf / .html / .png / .jpg / .md / .txt 等格式。 + +【怎么构建索引】 + 在项目根目录 D:\ccproject\PixelRAG 下运行: + + .venv\Scripts\pixelrag.exe index build + + 首次构建会自动加载 Qwen3-VL-Embedding-2B 模型(已缓存,不需再下载)。 + +【怎么启动搜索】 + 构建完成后运行: + + .venv\Scripts\pixelrag.exe serve --index-dir D:/ccproject/PixelRAG/knowledge_index --port 30001 + + 然后查询: + + curl -X POST http://localhost:30001/search ^ + -H "Content-Type: application/json" ^ + -d "{\"queries\":[{\"text\":\"你的问题\"}],\"n_docs\":5}" + +【小提示】 + - 攒一批论文(10~20 篇)再 build,效率更高(嵌入阶段会重新处理全部文档)。 + - 新文件建议用日期/序号前缀命名,避免插入中间导致旧文档重渲染。 From 735ae55e47969200ee106c2575f048d016d71c5e Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 14:55:39 +0800 Subject: [PATCH 03/11] feat: add VLM generation layer to desktop client Retrieve Top-K page screenshots, send them as multi-image input to DeepSeek-V4.1-Flash (deepseek-v4-flash) via the anthropic SDK, stream the answer into the chat UI, and show citation cards below. Falls back to citation-only display when the VLM is unavailable. --- pixelrag_client.py | 188 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 11 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index 41addba..f4d56b1 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """PixelRAG 知识库桌面客户端(DeepSeek 风格深色聊天界面)。 打开即自动检测/启动检索服务,支持中文或英文查询,返回最相关的论文页面 @@ -12,12 +11,12 @@ import json import os import subprocess -import sys import threading import time +import tkinter as tk import urllib.request -import tkinter as tk +import anthropic from PIL import Image, ImageTk ROOT = os.path.dirname(os.path.abspath(__file__)) @@ -27,6 +26,13 @@ HEALTH_URL = f"http://{HOST}:{PORT}/health" SEARCH_URL = f"http://{HOST}:{PORT}/search" +# ---- VLM 生成配置(DeepSeek-V4.1-Flash,原生视觉)---- +VLM_MODEL = "deepseek-flash" # DeepSeek-V4.1-Flash 官方主名;旧名 deepseek-v4-flash 已下线 +VLM_BASE_URL = "https://api.deepseek.com/anthropic" +VLM_MAX_IMAGES = 6 # 最多发几张截图 +VLM_MAX_SIDE = 1568 # 截图长边像素上限(超出即压缩) +VLM_MAX_TOKENS = 2048 + # ---- 配色(参考 DeepSeek 深色主题)---- BG = "#1f1f1f" # 主背景 BAR_BG = "#141414" # 顶栏/底栏背景 @@ -102,6 +108,92 @@ def decode_b64_image(b64: str) -> Image.Image: return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB") +SYSTEM_PROMPT = ( + "你是一个科研文献视觉问答助手。用户会提供若干张论文页面的截图,并附上一个问题。" + "请只根据这些截图中的可见内容回答,严格遵守:\n" + "1. 只能依据截图内容回答,严禁编造截图里没有的数据或事实。\n" + "2. 若截图信息不足以回答,直接说\"根据现有资料无法回答\",不要猜测。\n" + "3. 回答要具体、简洁,直接给出结论。\n" + "4. 每个关键事实后用 [1][2] 等编号标注,编号对应第几张截图(从 1 开始)。\n" + "5. 若答案来自某张图表,说明依据的是第几张图的哪个部分。\n" +) + + +def _load_vlm_config() -> dict: + """读取 VLM 配置:优先环境变量 PIXELRAG_VLM_*,其次 ANTHROPIC_* 环境变量,最后回退 ~/.claude/settings.json。""" + cfg = { + "api_key": os.environ.get("PIXELRAG_VLM_API_KEY"), + "base_url": os.environ.get("PIXELRAG_VLM_BASE_URL"), + "model": os.environ.get("PIXELRAG_VLM_MODEL"), + } + if not cfg["api_key"]: + cfg["api_key"] = os.environ.get("ANTHROPIC_AUTH_TOKEN") + cfg["base_url"] = os.environ.get("ANTHROPIC_BASE_URL") or VLM_BASE_URL + if not cfg["api_key"]: + try: + p = os.path.join(os.path.expanduser("~"), ".claude", "settings.json") + env = json.load(open(p, encoding="utf-8")).get("env", {}) + cfg["api_key"] = env.get("ANTHROPIC_AUTH_TOKEN") + cfg["base_url"] = env.get("ANTHROPIC_BASE_URL") or VLM_BASE_URL + except Exception: + pass + cfg["base_url"] = cfg["base_url"] or VLM_BASE_URL + cfg["model"] = cfg["model"] or VLM_MODEL + return cfg + + +def prepare_images(hits: list, k: int = VLM_MAX_IMAGES) -> list: + """取 Top-K 相关截图:按相关度降序、按 (article_id, tile_index) 去重、压缩为长边 ≤ VLM_MAX_SIDE 的 JPEG。 + + 返回 [(b64_jpeg, hit), ...],顺序即引用编号 [1][2]...。 + """ + seen = set() + out = [] + for h in sorted(hits, key=lambda x: float(x.get("score", 0.0)), reverse=True): + key = (h.get("article_id"), h.get("tile_index")) + if key in seen: + continue + seen.add(key) + b64 = h.get("image_base64") + if not b64: + continue + try: + img = decode_b64_image(b64) + img.thumbnail((VLM_MAX_SIDE, VLM_MAX_SIDE), Image.LANCZOS) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=80) + b64_out = base64.b64encode(buf.getvalue()).decode("ascii") + except Exception: + continue + out.append((b64_out, h)) + if len(out) >= k: + break + return out + + +def vlm_stream(images, question: str, cfg: dict): + """把多张截图按相关度顺序 + 问题发给 VLM,流式 yield 答案文本(过滤 thinking)。""" + client = anthropic.Anthropic(api_key=cfg["api_key"], base_url=cfg["base_url"]) + content = [] + for i, (b64, _h) in enumerate(images, 1): + content.append({ + "type": "image", + "source": {"type": "base64", "media_type": "image/jpeg", "data": b64}, + }) + content.append({"type": "text", "text": f"[第{i}张截图]"}) + content.append({"type": "text", "text": question}) + + with client.messages.stream( + model=cfg["model"], + max_tokens=VLM_MAX_TOKENS, + system=SYSTEM_PROMPT, + messages=[{"role": "user", "content": content}], + ) as stream: + for ev in stream: + if getattr(ev, "type", None) == "text": + yield ev.text + + # --------------------------------------------------------------------------- # 界面 # --------------------------------------------------------------------------- @@ -109,7 +201,7 @@ def decode_b64_image(b64: str) -> Image.Image: class App: def __init__(self, root: tk.Tk): self.root = root - self.root.title("PixelRAG 知识库检索") + self.root.title("PixelRAG 视觉问答") self.root.geometry("880x760") self.root.minsize(640, 560) self.root.configure(bg=BG) @@ -119,7 +211,7 @@ def __init__(self, root: tk.Tk): self._busy = False self._build_ui() - self._add_note("欢迎使用 PixelRAG 知识库检索。\n输入中文或英文问题,回车查询。") + self._add_note("欢迎使用 PixelRAG 视觉问答。\n输入问题,回车后检索相关论文页面截图,并由视觉模型生成答案(附引用)。") self._set_status() if not self._serve_ready: @@ -228,7 +320,8 @@ def _add_result_card(self, hit: dict, idx: int): ) card.pack(fill="x", padx=16, pady=(0, 14)) - title = title_of(hit.get("url", "")) + url = hit.get("url", "") + title = title_of(url) ti = hit.get("tile_index", 0) score = hit.get("score", 0.0) @@ -241,7 +334,7 @@ def _add_result_card(self, hit: dict, idx: int): title_lbl.pack(fill="x", pady=(12, 2)) title_lbl.bind( "", - lambda e, u=hit.get("url", ""): self._open_pdf(u), + lambda e, u=url: self._open_pdf(u), ) # 元信息 @@ -295,18 +388,22 @@ def _on_send(self): return self._busy = True - self._add_note("⏳ 正在检索,约需 1 分钟...") + self._add_note("⏳ 正在检索截图并生成答案,请稍候...") threading.Thread(target=self._do_search, args=(text,), daemon=True).start() def _do_search(self, text: str): try: resp = search(text) hits = resp["results"][0]["hits"] - self.root.after(0, lambda: self._show_results(hits)) except Exception as e: - self.root.after(0, lambda: self._show_error(e)) - finally: + self.root.after(0, lambda err=e: self._show_error(err)) + self.root.after(0, self._clear_busy) + return + if not hits: + self.root.after(0, lambda: self._add_note("没有找到相关结果,换个说法试试。")) self.root.after(0, self._clear_busy) + return + self._generate(text, hits) # 仍在后台线程,继续生成答案 def _show_results(self, hits: list): if not hits: @@ -315,6 +412,75 @@ def _show_results(self, hits: list): for i, h in enumerate(hits, 1): self._add_result_card(h, i) + def _generate(self, text: str, hits: list): + """准备截图 → 调 VLM 流式生成 → 前端流式显示答案 + 引用卡片。运行于后台线程。""" + images = prepare_images(hits) + if not images: + self.root.after(0, lambda: self._add_error( + "检索到了结果,但截图加载失败。以下为检索到的相关页面:" + )) + self.root.after(0, lambda: self._show_results(hits)) + self.root.after(0, self._clear_busy) + return + + cfg = _load_vlm_config() + if not cfg["api_key"]: + self.root.after(0, lambda: self._add_error( + "未找到 VLM API key。请设置环境变量 PIXELRAG_VLM_API_KEY,\n" + "或确保 ~/.claude/settings.json 里有 ANTHROPIC_AUTH_TOKEN。\n" + "以下为检索到的相关页面:" + )) + self.root.after(0, lambda: self._show_results(hits)) + self.root.after(0, self._clear_busy) + return + + # 在 UI 线程创建助手气泡,拿到可更新的 Label + self._answer_ready = threading.Event() + self.root.after(0, self._begin_answer) + if not self._answer_ready.wait(timeout=5): + self.root.after(0, self._clear_busy) + return + + full = "" + try: + for chunk in vlm_stream(images, text, cfg): + full += chunk + self.root.after(0, lambda t=full: self._set_answer(t)) + except Exception as e: + self.root.after(0, lambda err=e: self._add_error(f"生成失败:{err}")) + if full: + self.root.after(0, lambda: self._set_answer(full + "\n\n[生成中断]")) + finally: + self.root.after(0, lambda: self._finish_answer(images)) + + def _begin_answer(self): + row = tk.Frame(self.frame, bg=BG) + row.pack(fill="x", padx=16, pady=(14, 2)) + lbl = tk.Label( + row, text="", bg=CARD_BG, fg=TEXT_FG, justify="left", anchor="w", + wraplength=680, font=(FONT, 12), padx=14, pady=10, + ) + lbl.pack(side="left", fill="x", expand=True) + self._answer_label = lbl + self._answer_ready.set() + self._scroll_bottom() + + def _set_answer(self, text: str): + lbl = getattr(self, "_answer_label", None) + if lbl is not None: + lbl.configure(text=text) + self._scroll_bottom() + + def _finish_answer(self, images): + lbl = getattr(self, "_answer_label", None) + if lbl is not None and not lbl.cget("text"): + lbl.configure(text="(VLM 未返回内容,请重试。)") + if images: + self._add_note("引用来源:") + for i, (_b64, hit) in enumerate(images, 1): + self._add_result_card(hit, i) + self._clear_busy() + def _show_error(self, e: Exception): self._add_error( f"查询失败:{e}\n请确认检索服务已启动、索引文件完整。" From a8e76308c93130effa171fb69752c6abb6afae8f Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 16:51:04 +0800 Subject: [PATCH 04/11] feat(ui): design-token theme, font scale, and query history for the desktop client - split styling into pixelrag_theme (tokens) / pixelrag_widgets (PIL render + interaction primitives) / pixelrag_client (business + assembly) - single FONT_SCALE knob drives both tk and Pillow text so they stay in sync - query history: JSON store under ~/.pixelrag, left-hand panel, up/down recall - VLM_MAX_TOKENS 2048 -> 8192: thinking tokens count against the cap, which is why deepseek-flash used to return an empty answer --- pixelrag_client.py | 1513 +++++++++++++++++++++++++++++++++++++------ pixelrag_history.py | 416 ++++++++++++ pixelrag_theme.py | 187 ++++++ pixelrag_widgets.py | 504 ++++++++++++++ 4 files changed, 2418 insertions(+), 202 deletions(-) create mode 100644 pixelrag_history.py create mode 100644 pixelrag_theme.py create mode 100644 pixelrag_widgets.py diff --git a/pixelrag_client.py b/pixelrag_client.py index f4d56b1..f826ea8 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -1,15 +1,21 @@ #!/usr/bin/env python3 -"""PixelRAG 知识库桌面客户端(DeepSeek 风格深色聊天界面)。 +"""PixelRAG 知识库桌面客户端 —— 「Notion 纸感」浅色聊天界面。 -打开即自动检测/启动检索服务,支持中文或英文查询,返回最相关的论文页面 -(标题 + 相似度 + 页面截图),点击标题打开论文 PDF,点击截图看大图。 +打开即自动检测/启动检索服务,输入问题后检索最相关的论文页面截图,交给视觉模型 +流式生成答案,正文下方附引用卡片(文件名 / 页码 / 缩略图 / 点击打开 PDF)。 +顶栏可切换深色主题。 -依赖:仅标准库 tkinter + 已安装的 Pillow,无需额外安装。 +界面样式全部取自 ``pixelrag_theme`` 的设计令牌;tkinter 缺失的圆角、hover 等能力 +由 ``pixelrag_widgets`` 用 Pillow 离屏渲染补齐。检索与生成逻辑与旧版一致。 + +依赖:标准库 tkinter + 已安装的 Pillow、anthropic。 """ + import base64 import io import json import os +import re import subprocess import threading import time @@ -19,41 +25,39 @@ import anthropic from PIL import Image, ImageTk +import pixelrag_history as H +import pixelrag_theme as T +import pixelrag_widgets as W + ROOT = os.path.dirname(os.path.abspath(__file__)) HOST = "localhost" PORT = 30001 N_DOCS = 5 HEALTH_URL = f"http://{HOST}:{PORT}/health" SEARCH_URL = f"http://{HOST}:{PORT}/search" +ARTICLES_JSON = os.path.join(ROOT, "knowledge_index", "articles.json") # ---- VLM 生成配置(DeepSeek-V4.1-Flash,原生视觉)---- -VLM_MODEL = "deepseek-flash" # DeepSeek-V4.1-Flash 官方主名;旧名 deepseek-v4-flash 已下线 +VLM_MODEL = ( + "deepseek-flash" # DeepSeek-V4.1-Flash 官方主名;旧名 deepseek-v4-flash 已下线 +) VLM_BASE_URL = "https://api.deepseek.com/anthropic" -VLM_MAX_IMAGES = 6 # 最多发几张截图 -VLM_MAX_SIDE = 1568 # 截图长边像素上限(超出即压缩) -VLM_MAX_TOKENS = 2048 - -# ---- 配色(参考 DeepSeek 深色主题)---- -BG = "#1f1f1f" # 主背景 -BAR_BG = "#141414" # 顶栏/底栏背景 -CARD_BG = "#2a2a2a" # 结果卡片背景 -USER_BG = "#4d6bfe" # 用户气泡(蓝) -USER_FG = "#ffffff" -TEXT_FG = "#e6e6e6" # 主文字 -MUTED_FG = "#9e9e9e" # 次要文字 -ACCENT = "#7a9bff" # 可点击标题 -BORDER = "#3a3a3a" -GREEN = "#4ade80" # 服务运行中 -YELLOW = "#facc15" # 启动中 -RED = "#f87171" # 失败 - -FONT = "Microsoft YaHei UI" +VLM_MAX_IMAGES = 6 # 最多发几张截图 +VLM_MAX_SIDE = 1568 # 截图长边像素上限(超出即压缩) +# 输出上限。注意:deepseek-flash 是带思考的模型,思考 token 也计入这里 —— +# 图多、问题复杂时思考轻松吃掉 2000+,上限太小会导致"一个字都没写就被切断" +# (界面表现就是"模型没有返回任何内容")。实测 8192 能稳定作答。 +VLM_MAX_TOKENS = 8192 + +# 窗口逻辑尺寸(会按屏幕 DPI 缩放) +WIN_W, WIN_H = 900, 780 # --------------------------------------------------------------------------- -# 服务与检索 +# 服务与检索(与旧版一致,未改动) # --------------------------------------------------------------------------- + def check_health() -> bool: try: urllib.request.urlopen(HEALTH_URL, timeout=3) @@ -65,12 +69,18 @@ def check_health() -> bool: def _serve_cmd() -> list[str]: exe = os.path.join(ROOT, ".venv", "Scripts", "pixelrag.exe") return [ - exe, "serve", - "--index-dir", os.path.join(ROOT, "knowledge_index"), - "--tiles-dir", os.path.join(ROOT, "knowledge_index", "tiles"), - "--articles-json", os.path.join(ROOT, "knowledge_index", "articles.json"), - "--device", "cpu", - "--port", str(PORT), + exe, + "serve", + "--index-dir", + os.path.join(ROOT, "knowledge_index"), + "--tiles-dir", + os.path.join(ROOT, "knowledge_index", "tiles"), + "--articles-json", + os.path.join(ROOT, "knowledge_index", "articles.json"), + "--device", + "cpu", + "--port", + str(PORT), ] @@ -80,8 +90,11 @@ def start_serve_process() -> subprocess.Popen: env["TRANSFORMERS_OFFLINE"] = "1" flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) return subprocess.Popen( - _serve_cmd(), env=env, creationflags=flags, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + _serve_cmd(), + env=env, + creationflags=flags, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) @@ -90,8 +103,10 @@ def search(text: str, n_docs: int = N_DOCS) -> dict: {"queries": [{"text": text}], "n_docs": n_docs, "include_images": True} ).encode("utf-8") req = urllib.request.Request( - SEARCH_URL, data=payload, - headers={"Content-Type": "application/json"}, method="POST", + SEARCH_URL, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", ) with urllib.request.urlopen(req, timeout=900) as r: return json.loads(r.read().decode("utf-8")) @@ -108,11 +123,30 @@ def decode_b64_image(b64: str) -> Image.Image: return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB") +def _load_titles() -> dict[int, str]: + """articles.json 的下标即 article_id,用它拿到比文件名更干净的标题。""" + try: + with open(ARTICLES_JSON, encoding="utf-8") as f: + arts = json.load(f) + return {i: (a.get("title") or "") for i, a in enumerate(arts)} + except Exception: + return {} + + +def screen_scale() -> float: + try: + import ctypes + + return ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100.0 + except Exception: + return 1.0 + + SYSTEM_PROMPT = ( "你是一个科研文献视觉问答助手。用户会提供若干张论文页面的截图,并附上一个问题。" "请只根据这些截图中的可见内容回答,严格遵守:\n" "1. 只能依据截图内容回答,严禁编造截图里没有的数据或事实。\n" - "2. 若截图信息不足以回答,直接说\"根据现有资料无法回答\",不要猜测。\n" + '2. 若截图信息不足以回答,直接说"根据现有资料无法回答",不要猜测。\n' "3. 回答要具体、简洁,直接给出结论。\n" "4. 每个关键事实后用 [1][2] 等编号标注,编号对应第几张截图(从 1 开始)。\n" "5. 若答案来自某张图表,说明依据的是第几张图的哪个部分。\n" @@ -172,17 +206,24 @@ def prepare_images(hits: list, k: int = VLM_MAX_IMAGES) -> list: def vlm_stream(images, question: str, cfg: dict): - """把多张截图按相关度顺序 + 问题发给 VLM,流式 yield 答案文本(过滤 thinking)。""" + """把多张截图按相关度顺序 + 问题发给 VLM,流式 yield 答案文本(过滤 thinking)。 + + 两个容易被误判成"网络坏了"的情况在这里说明白:模型把输出额度全花在思考上、 + 一个字都没写出来时,抛一条能看懂的错误,而不是静默返回空串。 + """ client = anthropic.Anthropic(api_key=cfg["api_key"], base_url=cfg["base_url"]) content = [] for i, (b64, _h) in enumerate(images, 1): - content.append({ - "type": "image", - "source": {"type": "base64", "media_type": "image/jpeg", "data": b64}, - }) + content.append( + { + "type": "image", + "source": {"type": "base64", "media_type": "image/jpeg", "data": b64}, + } + ) content.append({"type": "text", "text": f"[第{i}张截图]"}) content.append({"type": "text", "text": question}) + got_text = False with client.messages.stream( model=cfg["model"], max_tokens=VLM_MAX_TOKENS, @@ -191,187 +232,904 @@ def vlm_stream(images, question: str, cfg: dict): ) as stream: for ev in stream: if getattr(ev, "type", None) == "text": + got_text = True yield ev.text + stop_reason = getattr(stream.get_final_message(), "stop_reason", None) + + if stop_reason == "max_tokens": + if not got_text: + raise RuntimeError( + f"模型的思考占满了输出上限(max_tokens={VLM_MAX_TOKENS})," + "还没来得及作答就被切断了。请再试一次,或把 pixelrag_client.py 里的 " + "VLM_MAX_TOKENS 调得更大。" + ) + yield "\n\n(回答达到长度上限,可能不完整)" + + +_CODE_RE = re.compile(r"```[ \t]*[\w+-]*\n(.*?)```", re.DOTALL) + + +def _split_code_blocks(text: str) -> list[tuple[str, str]]: + """把答案切成 ('text'|'code', 片段) 序列,用于分别排版代码块。""" + out: list[tuple[str, str]] = [] + pos = 0 + for m in _CODE_RE.finditer(text): + if m.start() > pos: + out.append(("text", text[pos : m.start()])) + out.append(("code", m.group(1).rstrip("\n"))) + pos = m.end() + if pos < len(text): + out.append(("text", text[pos:])) + return out or [("text", text)] # --------------------------------------------------------------------------- # 界面 # --------------------------------------------------------------------------- + class App: def __init__(self, root: tk.Tk): self.root = root - self.root.title("PixelRAG 视觉问答") - self.root.geometry("880x760") - self.root.minsize(640, 560) - self.root.configure(bg=BG) + root.title("PixelRAG 视觉问答") + root.configure(bg=T.c("bg")) + root.minsize(int(660 * T.UI_SCALE), int(480 * T.UI_SCALE)) + # 消息数据模型:主题切换 / 窗口缩放时据此重放渲染 + self.messages: list[dict] = [] self._photos: list[ImageTk.PhotoImage] = [] # 防止图片被 GC - self._serve_ready = check_health() + self._answer_text: tk.Text | None = None + self._answer_msg: dict | None = None + self._paint_job = None + self._loading_job = None + self._loading_active = False + self._loading_base = "正在检索相关页面" + self._resize_job = None + self._send_hover = False + self._stop_hover = False self._busy = False + # 停止生成:主线程置位,后台消费循环轮询。generator 另存一份是为了能 + # 显式 close(),让底层的 HTTP 流立刻断开而不是等 GC。 + self._stop_event = threading.Event() + self._gen = None + self._stopped = False + self._last_question: str | None = None + # 查询历史:落盘存储 + 面板 + 输入框上下键召回 + self.history = H.History() + self._history_panel: H.HistoryPanel | None = None + self._hist_entry: dict | None = None # 本次提问对应的历史条目,答完回填 + self._hist_pos: int | None = None # None = 不在召回态;0 = 最新一条 + self._hist_draft = "" # 召回前用户已经敲了一半的内容 + self._serve_ready = check_health() + self._titles = _load_titles() + self._content_w = T.CONTENT_MAX_WIDTH + self._canvas_w = 0 + self._apply_window_geometry() self._build_ui() - self._add_note("欢迎使用 PixelRAG 视觉问答。\n输入问题,回车后检索相关论文页面截图,并由视觉模型生成答案(附引用)。") + self._add_welcome() self._set_status() if not self._serve_ready: threading.Thread(target=self._ensure_serve, daemon=True).start() + def _apply_window_geometry(self): + sw, sh = self.root.winfo_screenwidth(), self.root.winfo_screenheight() + w = min(int(WIN_W * T.UI_SCALE), sw - int(80 * T.UI_SCALE)) + h = min(int(WIN_H * T.UI_SCALE), sh - int(120 * T.UI_SCALE)) + x = max(0, (sw - w) // 2) + y = max(0, (sh - h) // 3) + self.root.geometry(f"{w}x{h}+{x}+{y}") + # ---------- UI 构建 ---------- def _build_ui(self): - # 顶栏状态 + self._build_topbar() + self._build_bottom_bar() + self._build_body() + + def _build_topbar(self): + self.topbar = tk.Frame(self.root, bg=T.c("surface")) + self.topbar.pack(side="top", fill="x") + inner = tk.Frame(self.topbar, bg=T.c("surface")) + inner.pack(fill="x", padx=T.SPACE["lg"], pady=T.SPACE["sm"]) + + self.brand = tk.Label( + inner, + text="PixelRAG", + bg=T.c("surface"), + fg=T.c("text"), + font=T.font("title"), + ) + self.brand.pack(side="left") + self.brand_sub = tk.Label( + inner, + text="视觉问答", + bg=T.c("surface"), + fg=T.c("text_faint"), + font=T.font("caption"), + ) + self.brand_sub.pack( + side="left", padx=(T.SPACE["sm"], 0), pady=(T.SPACE["xs"], 0) + ) + + # 主题切换 + self.theme_btn = tk.Label( + inner, + text="", + bg=T.c("surface"), + fg=T.c("text_muted"), + font=T.font("caption"), + cursor="hand2", + padx=T.SPACE["sm"], + pady=T.SPACE["xs"], + ) + self.theme_btn.pack(side="right") + self.theme_btn.bind("", lambda _e: self._toggle_theme()) + W.make_focusable(self.theme_btn, T.c("surface"), self._toggle_theme) + W.bind_hover( + self.theme_btn, + on_enter=lambda: self.theme_btn.configure( + fg=T.c("accent"), bg=T.c("accent_soft") + ), + on_leave=lambda: self.theme_btn.configure( + fg=T.c("text_muted"), bg=T.c("surface") + ), + ) + self._sync_theme_btn() + + # 查询历史 + self.history_btn = tk.Label( + inner, + text="历史", + bg=T.c("surface"), + fg=T.c("text_muted"), + font=T.font("caption"), + cursor="hand2", + padx=T.SPACE["sm"], + pady=T.SPACE["xs"], + ) + self.history_btn.pack(side="right", padx=(0, T.SPACE["xs"])) + self.history_btn.bind("", lambda _e: self._on_history()) + W.make_focusable(self.history_btn, T.c("surface"), self._on_history) + W.bind_hover( + self.history_btn, + on_enter=lambda: self.history_btn.configure( + fg=T.c("accent"), bg=T.c("accent_soft") + ), + on_leave=lambda: self.history_btn.configure( + fg=T.c("text_muted"), bg=T.c("surface") + ), + ) + self.status = tk.Label( - self.root, text="", bg=BAR_BG, fg=MUTED_FG, anchor="w", - padx=16, pady=7, font=(FONT, 10), + inner, + text="", + bg=T.c("surface"), + fg=T.c("text_muted"), + font=T.font("caption"), + ) + self.status.pack(side="right", padx=(0, T.SPACE["md"])) + + self.topbar_line = tk.Frame(self.root, bg=T.c("border"), height=1) + self.topbar_line.pack(side="top", fill="x") + + def _sync_theme_btn(self): + text = "切换深色" if T.mode() == "light" else "切换浅色" + self.theme_btn.configure(text=text) + + def _build_bottom_bar(self): + self.bottom = tk.Frame(self.root, bg=T.c("bg")) + self.bottom.pack(side="bottom", fill="x") + tk.Frame(self.bottom, bg=T.c("border"), height=1).pack(side="top", fill="x") + + row = tk.Frame(self.bottom, bg=T.c("bg")) + row.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["md"], T.SPACE["sm"])) + + # 发送按钮(圆角图片按钮,支持 hover / disabled 两态) + self.send_btn = tk.Label(row, bg=T.c("bg"), bd=0, cursor="hand2") + self.send_btn.pack(side="right", padx=(T.SPACE["md"], 0), anchor="s") + self.send_btn.bind("", lambda _e: self._on_send()) + W.bind_hover( + self.send_btn, on_enter=self._send_hover_on, on_leave=self._send_hover_off + ) + W.make_focusable(self.send_btn, T.c("bg"), self._on_send) + + # 停止按钮:只在生成期间出现,平时不 pack(不占位) + self.stop_btn = tk.Label(row, bg=T.c("bg"), bd=0, cursor="hand2") + self.stop_btn.bind("", lambda _e: self._on_stop()) + W.bind_hover( + self.stop_btn, on_enter=self._stop_hover_on, on_leave=self._stop_hover_off + ) + W.make_focusable(self.stop_btn, T.c("bg"), self._on_stop) + self._stop_shown = False + + # 输入区:底线式,聚焦时底线变强调色 + box = tk.Frame(row, bg=T.c("bg")) + box.pack(side="left", fill="x", expand=True) + self.input = tk.Text( + box, + height=2, + bg=T.c("input_bg"), + fg=T.c("text"), + insertbackground=T.c("accent"), + relief="flat", + bd=0, + highlightthickness=0, + font=T.font("body_lg"), + wrap="word", + padx=0, + pady=0, + spacing1=2, + spacing3=2, + ) + self.input.pack(fill="x") + self.input.bind("", self._on_enter) + self.input.bind("", lambda _e: self._clear_input()) + self.input.bind("", self._select_all_input) + # ↑/↓ 召回历史:只在光标处于首行(↑)/已在召回态(↓)时接管,否则让 tk 正常移光标 + self.input.bind("", self._on_input_up) + self.input.bind("", self._on_input_down) + W.attach_text_menu(self.input, self.root, editable=True) + self.input.bind( + "", lambda _e: self.input_line.configure(bg=T.c("accent")) + ) + self.input.bind( + "", lambda _e: self.input_line.configure(bg=T.c("input_line")) ) - self.status.pack(side="top", fill="x") + self.input_line = tk.Frame(box, bg=T.c("input_line"), height=1) + self.input_line.pack(fill="x", pady=(T.SPACE["sm"], 0)) + + hint = tk.Label( + self.bottom, + text="Enter 发送 · Shift+Enter 换行 · ↑ 历史 · Esc 清空", + bg=T.c("bg"), + fg=T.c("text_faint"), + font=T.font("micro"), + ) + hint.pack(side="top", pady=(0, T.SPACE["sm"])) - # 消息区(Canvas + Frame + 滚动条) - body = tk.Frame(self.root, bg=BG) + def _build_body(self): + body = tk.Frame(self.root, bg=T.c("bg")) body.pack(side="top", fill="both", expand=True) - self.canvas = tk.Canvas(body, bg=BG, highlightthickness=0) + self.canvas = tk.Canvas(body, bg=T.c("bg"), highlightthickness=0, bd=0) self.scroll = tk.Scrollbar( - body, orient="vertical", command=self.canvas.yview, - bg=CARD_BG, troughcolor=BG, activebackground=BORDER, - borderwidth=0, width=12, + body, + orient="vertical", + command=self.canvas.yview, + bg=T.c("scrollbar"), + troughcolor=T.c("bg"), + activebackground=T.c("border_strong"), + borderwidth=0, + width=10, + relief="flat", + elementborderwidth=0, ) self.canvas.configure(yscrollcommand=self.scroll.set) self.scroll.pack(side="right", fill="y") self.canvas.pack(side="left", fill="both", expand=True) - self.frame = tk.Frame(self.canvas, bg=BG) + self.frame = tk.Frame(self.canvas, bg=T.c("bg")) self._win = self.canvas.create_window((0, 0), window=self.frame, anchor="nw") - self.frame.bind( - "", - lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", - lambda e: self.canvas.itemconfigure(self._win, width=e.width), - ) - - # 底部输入栏 - bar = tk.Frame(self.root, bg=BAR_BG) - bar.pack(side="bottom", fill="x") - - self.input = tk.Text( - bar, height=3, bg=CARD_BG, fg=TEXT_FG, insertbackground=TEXT_FG, - relief="flat", font=(FONT, 12), padx=12, pady=10, wrap="word", + self.frame.bind("", self._on_frame_configure) + self.canvas.bind("", self._on_canvas_configure) + self.canvas.bind_all("", self._on_wheel) + + def _on_frame_configure(self, _e=None): + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def _on_canvas_configure(self, event): + self.canvas.itemconfigure(self._win, width=event.width) + new_w = max(280, min(T.CONTENT_MAX_WIDTH, event.width - 2 * T.SPACE["xl"])) + # 画布宽变了也要重排(即使 _content_w 已到上限):正文的左右内缩量是按 + # 画布宽算的,不重排的话首次拿到真实宽度后行宽会一直偏宽。 + moved = abs(event.width - self._canvas_w) > 8 + self._canvas_w = event.width + if abs(new_w - self._content_w) > 8 or moved: + self._content_w = new_w + self._schedule_reflow() + + def _schedule_reflow(self): + """窗口尺寸变化后防抖重排(圆角图与折行宽度都需要重算)。""" + if self._resize_job: + try: + self.root.after_cancel(self._resize_job) + except Exception: + pass + self._resize_job = self.root.after( + T.MOTION["resize_debounce"], self._render_all ) - self.input.pack(side="left", fill="x", expand=True, padx=(16, 8), pady=12) - self.input.bind("", self._on_enter) - self.input.insert("1.0", "") - self.input.focus_set() - self.send_btn = tk.Button( - bar, text="发送", command=self._on_send, - bg=USER_BG, fg=USER_FG, activebackground="#5a7cff", - activeforeground="#ffffff", relief="flat", font=(FONT, 11, "bold"), - padx=22, pady=8, cursor="hand2", - ) - self.send_btn.pack(side="right", padx=(0, 16), pady=12) + def _on_wheel(self, event): + try: + self.canvas.yview_scroll(int(-event.delta / 120) * 3, "units") + except Exception: + pass # ---------- 状态 ---------- def _set_status(self, state: str | None = None): if state == "starting": - self.status.configure(text="● 正在启动检索服务...", fg=YELLOW) + self.status.configure(text="● 正在启动检索服务", fg=T.c("warning")) elif state == "failed": - self.status.configure(text="● 服务启动失败(见控制台)", fg=RED) + self.status.configure(text="● 服务启动失败(见控制台)", fg=T.c("danger")) elif self._serve_ready: - self.status.configure(text="● 服务运行中 · 端口 30001", fg=GREEN) + self.status.configure(text="● 服务运行中 · 端口 30001", fg=T.c("success")) else: - self.status.configure(text="● 服务启动中...", fg=YELLOW) - - # ---------- 消息渲染 ---------- + self.status.configure(text="● 服务启动中", fg=T.c("warning")) + + # ---------- 消息模型与渲染 ---------- + + def _append(self, msg: dict) -> dict: + stick = self._at_bottom() # 先取状态:追加后必然"不在底部" + self.messages.append(msg) + self._render_message(msg) + self._scroll_bottom(stick) + return msg + + def _add_welcome(self): + self._append( + { + "kind": "welcome", + "text": "输入问题,我会检索知识库里的论文页面截图,并基于截图内容作答。\n" + "答案下方给出引用来源,点击卡片可打开对应的 PDF。", + } + ) def _add_user(self, text: str): - row = tk.Frame(self.frame, bg=BG) - row.pack(fill="x", padx=16, pady=(14, 2)) - lbl = tk.Label( - row, text=text, bg=USER_BG, fg=USER_FG, justify="left", - wraplength=520, font=(FONT, 12), padx=14, pady=9, - ) - lbl.pack(side="right", anchor="e") - self._scroll_bottom() + self._append({"kind": "user", "text": text}) def _add_note(self, text: str): - lbl = tk.Label( - self.frame, text=text, bg=BG, fg=MUTED_FG, justify="center", - font=(FONT, 11), padx=16, pady=10, + self._append({"kind": "note", "text": text}) + + def _add_error(self, text: str, retry: bool = False): + """``retry=True`` 时在这条错误下挂一个「重新生成」按钮。""" + self._append({"kind": "error", "text": text, "retry": retry}) + + def _render_all(self, view_state: tuple[bool, float] | None = None): + """清空并重放全部消息(主题切换 / 窗口尺寸变化时调用)。 + + ``view_state`` 由调用方在重建界面前取得;不传则读当前画布。 + """ + self._resize_job = None + stick, top = view_state if view_state is not None else self._view_state() + was_loading = self._loading_active + self._cancel_loading_job() + self._loading_active = False + + for w in self.frame.winfo_children(): + w.destroy() + self._photos.clear() + self._answer_text = None + + for msg in self.messages: + self._render_message(msg) + if was_loading: + self._start_loading(self._loading_base) + + self.canvas.update_idletasks() # 让 bbox 反映新内容,否则滚动范围是旧的 + self._on_frame_configure() + try: + self.canvas.yview_moveto(1.0 if stick else top) + except Exception: + pass + if stick: + self._scroll_bottom(True) # 几何稳定后再吸一次,避免滚动范围随后变大 + + def _render_message(self, msg: dict): + kind = msg.get("kind") + if kind == "user": + self._render_user(msg) + elif kind == "welcome": + self._render_welcome(msg) + elif kind == "note": + self._render_note(msg) + elif kind == "error": + self._render_error(msg) + elif kind == "section": + self._render_section(msg["text"]) + elif kind == "answer": + self._render_answer(msg) + elif kind == "citations": + self._render_citations(msg) + + # ---- 各类消息 ---- + + def _render_welcome(self, msg: dict): + wrap = tk.Frame(self.frame, bg=T.c("bg")) + wrap.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["2xl"], T.SPACE["lg"])) + tk.Label( + wrap, + text=msg["text"], + bg=T.c("bg"), + fg=T.c("text_muted"), + font=T.font("body"), + justify="center", + wraplength=self._content_w - 2 * T.SPACE["md"], + ).pack(anchor="center") + + def _render_user(self, msg: dict): + row = tk.Frame(self.frame, bg=T.c("bg")) + row.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["xs"])) + img = W.render_label_box( + msg["text"], + "body", + T.c("user_text"), + T.c("bg"), + box_fill=T.c("user_bubble"), + radius=T.RADIUS["md"], + pad_x=T.SPACE["md"], + pad_y=T.SPACE["sm"], + max_w=int(self._content_w * 0.78), + ) + photo = W.photo_of(img) + self._photos.append(photo) + lbl = tk.Label(row, image=photo, bg=T.c("bg"), bd=0) + lbl.image = photo + lbl.pack(side="right") + + def _render_note(self, msg: dict): + tk.Label( + self.frame, + text=msg["text"], + bg=T.c("bg"), + fg=T.c("text_muted"), + font=T.font("body"), + justify="left", + anchor="w", + wraplength=self._content_w, + ).pack(fill="x", padx=T.SPACE["xl"], pady=T.SPACE["xs"]) + + def _render_error(self, msg: dict): + row = tk.Frame(self.frame, bg=T.c("bg")) + row.pack(fill="x", padx=T.SPACE["xl"], pady=T.SPACE["sm"]) + img = W.render_label_box( + msg["text"], + "body", + T.c("danger"), + T.c("bg"), + box_fill=T.c("danger_bg"), + radius=T.RADIUS["md"], + pad_x=T.SPACE["md"], + pad_y=T.SPACE["sm"], + max_w=self._content_w, ) - lbl.pack(fill="x", padx=16) - self._scroll_bottom() + photo = W.photo_of(img) + self._photos.append(photo) + lbl = tk.Label(row, image=photo, bg=T.c("bg"), bd=0) + lbl.image = photo + lbl.pack(anchor="w") # 竖排:气泡在上,重试按钮在下 + if msg.get("retry"): + self._render_retry_button(row) + + def _render_retry_button(self, parent): + """次要按钮:描边胶囊,悬停变主色。""" + btn = tk.Label(parent, bg=T.c("bg"), bd=0, cursor="hand2") + btn.pack(anchor="w", pady=(T.SPACE["sm"], 0)) + + def paint(hover: bool): + img = W.render_label_box( + "重新生成", + "button", + T.c("accent") if hover else T.c("text"), + T.c("bg"), + box_fill=T.c("card_hover") if hover else T.c("card"), + outline=T.c("accent") if hover else T.c("border_strong"), + radius=T.RADIUS["sm"], + pad_x=T.SPACE["md"], + pad_y=T.SPACE["sm"], + align="center", + ) + photo = W.photo_of(img) + self._photos.append(photo) + try: + btn.configure(image=photo) + btn.image = photo + except Exception: + pass - def _add_error(self, text: str): - lbl = tk.Label( - self.frame, text=text, bg="#3a1d1d", fg=RED, justify="left", - wraplength=700, font=(FONT, 11), padx=14, pady=10, + paint(False) + btn.bind("", lambda _e: self._on_regenerate()) + W.bind_hover(btn, on_enter=lambda: paint(True), on_leave=lambda: paint(False)) + W.make_focusable(btn, T.c("bg"), self._on_regenerate) + + def _render_section(self, text: str): + wrap = tk.Frame(self.frame, bg=T.c("bg")) + wrap.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["sm"])) + tk.Label( + wrap, + text=text, + bg=T.c("bg"), + fg=T.c("text_muted"), + font=T.font("h2"), + ).pack(side="left") + tk.Frame(wrap, bg=T.c("border"), height=1).pack( + side="left", + fill="x", + expand=True, + padx=(T.SPACE["sm"], 0), + pady=(T.SPACE["sm"], 0), ) - lbl.pack(fill="x", padx=16, pady=8) - self._scroll_bottom() - def _add_result_card(self, hit: dict, idx: int): - card = tk.Frame( - self.frame, bg=CARD_BG, highlightthickness=1, - highlightbackground=BORDER, + def _render_answer(self, msg: dict): + container = tk.Frame(self.frame, bg=T.c("bg")) + container.pack( + fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["xs"], T.SPACE["sm"]) ) - card.pack(fill="x", padx=16, pady=(0, 14)) + msg["_widget"] = container + + # 正文用只读 Text 而不是 Label —— Label 不能选中,复制不出来。 + # Text 没有 wraplength,只能靠左右内缩把行宽压回 _content_w, + # 否则窗口一宽,长行就会横贯整个画布(和引用卡片对不齐)。 + avail = max(1, self.canvas.winfo_width() - 2 * T.SPACE["xl"]) + inset = max(0, (avail - self._content_w) // 2) + txt = W.selectable_text(container, T.font("body"), T.c("bg"), T.c("text")) + txt.pack(fill="x", padx=(inset, inset)) + W.attach_text_menu(txt, self.root) + txt.tag_configure( + "code", + font=T.font_mono(9), + background=T.c("code_bg"), + lmargin1=T.SPACE["md"], + lmargin2=T.SPACE["md"], + rmargin=T.SPACE["md"], + spacing1=T.SPACE["sm"], + spacing3=T.SPACE["sm"], + ) + self._answer_text = txt - url = hit.get("url", "") - title = title_of(url) - ti = hit.get("tile_index", 0) - score = hit.get("score", 0.0) + text = msg.get("text", "") + if msg.get("streaming"): + self._set_answer_into(txt, (text + " ▌") if text else "…", settle=True) + self._answer_msg = msg + return - # 标题(点击打开 PDF) - title_lbl = tk.Label( - card, text=f"[{idx}] {title}", bg=CARD_BG, fg=ACCENT, - font=(FONT, 11, "bold"), justify="left", anchor="w", - wraplength=760, cursor="hand2", padx=14, + self._fill_answer(txt, text) + self._render_copy_button(container, text) + + def _set_answer_into(self, txt: tk.Text, s: str, settle: bool = False): + """整体重写正文。内容短、节流到 60ms 一次,所以不做增量插入。""" + txt.delete("1.0", "end") + txt.insert("1.0", s) + W.fit_height(txt, settle=settle) + + def _fill_answer(self, txt: tk.Text, text: str): + """定稿排版:代码段套 code 标签(等宽 + 淡底 + 内缩)。""" + for seg_kind, seg_text in _split_code_blocks(text): + if seg_kind == "code": + txt.insert("end", seg_text + "\n", "code") + elif seg_text: + txt.insert("end", seg_text) + W.fit_height(txt, settle=True) + + def _render_copy_button(self, parent, text: str): + bar = tk.Frame(parent, bg=T.c("bg")) + bar.pack(fill="x", pady=(T.SPACE["sm"], 0)) + btn = tk.Label( + bar, + text="复制答案", + bg=T.c("bg"), + fg=T.c("text_faint"), + font=T.font("micro"), + cursor="hand2", + padx=T.SPACE["xs"], ) - title_lbl.pack(fill="x", pady=(12, 2)) - title_lbl.bind( - "", - lambda e, u=url: self._open_pdf(u), + btn.pack(side="left") + + def copy(_e=None): + self.root.clipboard_clear() + self.root.clipboard_append(text) + btn.configure(text="已复制", fg=T.c("success")) + self.root.after( + 1400, lambda: btn.configure(text="复制答案", fg=T.c("text_faint")) + ) + + btn.bind("", copy) + W.bind_hover( + btn, + on_enter=lambda: btn.configure(fg=T.c("accent")), + on_leave=lambda: btn.configure(fg=T.c("text_faint")), ) - - # 元信息 - meta = tk.Label( - card, text=f"页面 tile_{ti:04d} · 相似度 {score:.3f} · 点击标题打开 PDF", - bg=CARD_BG, fg=MUTED_FG, font=(FONT, 9), anchor="w", padx=14, + W.make_focusable(btn, T.c("bg"), copy) + + def _render_citations(self, msg: dict): + self._render_section("引用来源") + for i, (b64, hit) in enumerate(msg["tiles"], 1): + self._render_citation_card(b64, hit, i) + + def _render_citation_card(self, b64: str, hit: dict, idx: int): + """白底描边卡片:缩略图 + 标题 + 页码/相似度。点缩略图看大图,点其余开 PDF。""" + pad = T.SPACE["md"] + w = max(240, self._content_w) + thumb_box = (int(74 * T.UI_SCALE), int(96 * T.UI_SCALE)) + + card = tk.Canvas( + self.frame, + width=w, + height=int(104 * T.UI_SCALE), # 占位高度,量完信息块后修正 + bg=T.c("bg"), + highlightthickness=0, + bd=0, + cursor="hand2", ) - meta.pack(fill="x", pady=(0, 8)) + card.pack(fill="x", padx=T.SPACE["xl"], pady=(0, T.SPACE["sm"])) - # 页面截图缩略图(点击看大图) - b64 = hit.get("image_base64") + title = self._titles.get(hit.get("article_id")) or title_of(hit.get("url", "")) + x = pad + thumb_lbl = None + thumb_h = 0 if b64: try: img = decode_b64_image(b64) - img.thumbnail((320, 460), Image.LANCZOS) - photo = ImageTk.PhotoImage(img) - self._photos.append(photo) - img_lbl = tk.Label(card, image=photo, bg=CARD_BG, cursor="hand2") - img_lbl.image = photo - img_lbl.pack(padx=14, pady=(0, 14)) - img_lbl.bind( - "", - lambda e, b=b64, t=title: self._open_image(b, t), + img.thumbnail(thumb_box, Image.LANCZOS) + thumb = W.photo_of(img) + self._photos.append(thumb) + thumb_lbl = tk.Label( + card, image=thumb, bg=T.c("card"), bd=0, cursor="hand2" ) + thumb_lbl.image = thumb + x += thumb.width() + pad + thumb_h = thumb.height() + except Exception: + thumb_lbl = None + + tw = max(120, w - x - pad) + + # 信息块:交给 Frame + pack 自己排垂直布局,避免手算 y 导致换行标题被盖住 + info = tk.Frame(card, bg=T.c("card")) + title_lbl = tk.Label( + info, + text=f"[{idx}] {title}", + bg=T.c("card"), + fg=T.c("accent"), + font=T.font("label"), + justify="left", + anchor="w", + wraplength=tw, + cursor="hand2", + ) + title_lbl.pack(anchor="w", fill="x") + + page = int(hit.get("tile_index", 0)) + 1 + meta = f"第 {page} 页" + if int(hit.get("chunk_index", 0) or 0) > 0: + meta += f" · 第 {int(hit['chunk_index']) + 1} 段" + meta += f" · 相似度 {float(hit.get('score', 0.0)):.3f}" + meta_lbl = tk.Label( + info, + text=meta, + bg=T.c("card"), + fg=T.c("text_muted"), + font=T.font("caption"), + justify="left", + anchor="w", + ) + meta_lbl.pack(anchor="w", fill="x", pady=(T.SPACE["xs"], 0)) + + hint_lbl = tk.Label( + info, + text="点击打开 PDF", + bg=T.c("card"), + fg=T.c("text_faint"), + font=T.font("micro"), + justify="left", + anchor="w", + cursor="hand2", + ) + hint_lbl.pack(anchor="w", fill="x", pady=(T.SPACE["sm"], 0)) + + info.update_idletasks() + h = max(int(80 * T.UI_SCALE), thumb_h, info.winfo_reqheight()) + 2 * pad + card.configure(height=h) + + # 三种外观:常态 / 悬停 / 键盘聚焦(聚焦用主色描边,与 hover 区分开) + normal = W.rounded_photo( + w, h, T.RADIUS["md"], T.c("card"), T.c("bg"), outline=T.c("border") + ) + hover = W.rounded_photo( + w, + h, + T.RADIUS["md"], + T.c("card_hover"), + T.c("bg"), + outline=T.c("border_strong"), + ) + focused = W.rounded_photo( + w, + h, + T.RADIUS["md"], + T.c("card_hover"), + T.c("bg"), + outline=T.c("focus"), + outline_w=2, + ) + self._photos.extend([normal, hover, focused]) + bg_id = card.create_image(0, 0, anchor="nw", image=normal) + card.tag_lower(bg_id) # 背景图是后建的,必须沉底,否则会盖住信息块 + + card.create_window(x, pad, anchor="nw", window=info, width=tw) + if thumb_lbl is not None: + card.create_window( + pad, max(pad, (h - thumb_h) // 2), anchor="nw", window=thumb_lbl + ) + + def repaint(color: str): + def walk(widget): + for child in widget.winfo_children(): + try: + child.configure(bg=color) + except Exception: + pass + walk(child) + + walk(card) + + def open_pdf(_e=None): + self._open_hit(hit, title) + + # 悬停与聚焦会同时发生,用一份状态决定最终外观(聚焦优先) + state = {"hover": False, "focus": False} + + def paint(_e=None): + if state["focus"]: + card.itemconfigure(bg_id, image=focused) + repaint(T.c("card_hover")) + elif state["hover"]: + card.itemconfigure(bg_id, image=hover) + repaint(T.c("card_hover")) + else: + card.itemconfigure(bg_id, image=normal) + repaint(T.c("card")) + + def set_state(key, value): + def apply(_e=None): + state[key] = value + paint() + + return apply + + W.bind_hover(card, set_state("hover", True), set_state("hover", False)) + card.bind("", set_state("focus", True), add="+") + card.bind("", set_state("focus", False), add="+") + + # 键盘可达:Tab 聚焦,Enter / Space 打开 PDF + card.configure(takefocus=1) + for key in ("", "", ""): + card.bind(key, open_pdf, add="+") + + for widget in (card, info, title_lbl, meta_lbl, hint_lbl): + widget.bind("", open_pdf, add="+") + if thumb_lbl is not None: + thumb_lbl.bind( + "", lambda _e: self._open_image(b64, title), add="+" + ) + + # ---- 加载态 ---- + + def _start_loading(self, text: str = "正在检索相关页面"): + if self._loading_active: + return + self._loading_active = True + self._loading_base = text + self._loading_frame = tk.Frame(self.frame, bg=T.c("bg")) + self._loading_frame.pack( + fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["sm"]) + ) + self._loading_label = tk.Label( + self._loading_frame, + text=text, + bg=T.c("bg"), + fg=T.c("text_muted"), + font=T.font("body"), + anchor="w", + justify="left", + ) + self._loading_label.pack(fill="x") + + skel = tk.Frame(self._loading_frame, bg=T.c("bg")) + skel.pack(fill="x", pady=(T.SPACE["sm"], 0)) + for frac in (0.92, 0.68): + bar = W.rounded_photo( + max(40, int(self._content_w * frac)), + int(12 * T.UI_SCALE), + T.RADIUS["sm"], + T.c("skeleton"), + T.c("bg"), + ) + self._photos.append(bar) + lbl = tk.Label(skel, image=bar, bg=T.c("bg"), bd=0) + lbl.image = bar + lbl.pack(anchor="w", pady=(0, T.SPACE["xs"])) + + self._dot = 0 + self._tick_loading() + self._scroll_bottom(True) + + def _tick_loading(self): + if not self._loading_active: + return + self._dot = (self._dot + 1) % 4 + stick = self._at_bottom() + try: + self._loading_label.configure(text=self._loading_base + "·" * self._dot) + except Exception: + return + self._loading_job = self.root.after(420, self._tick_loading) + self._scroll_bottom(stick) + + def _set_loading_text(self, text: str): + self._loading_base = text + if self._loading_active: + try: + self._loading_label.configure(text=text) + except Exception: + pass + + def _cancel_loading_job(self): + if self._loading_job: + try: + self.root.after_cancel(self._loading_job) + except Exception: + pass + self._loading_job = None + + def _stop_loading(self): + self._cancel_loading_job() + self._loading_active = False + frame = getattr(self, "_loading_frame", None) + if frame is not None: + try: + frame.destroy() except Exception: pass + self._loading_frame = None - self._scroll_bottom() + # ---------- 滚动 ---------- + + def _at_bottom(self) -> bool: + """当前视图是否停在底部。必须在改动内容【之前】调用才有意义。""" + try: + return self.canvas.yview()[1] >= 0.995 + except Exception: + return True + + def _view_state(self) -> tuple[bool, float]: + """(是否停在底部, 顶部比例)。重建界面前取一次,重建后据此还原。""" + try: + top, bottom = self.canvas.yview() + return bottom >= 0.995, top + except Exception: + return True, 0.0 + + def _scroll_bottom(self, stick: bool = True): + """stick=True 时把视图吸到底部;False 表示用户正在上翻,不去抢。""" + if not stick: + return + + def go(): + try: + # 顺序很关键:先让几何算完并刷新滚动范围,再移动。 + # 否则 moveto(1.0) 会按【旧】范围钳制,结果停在中途。 + self.canvas.update_idletasks() + h = max(self.frame.winfo_reqheight(), self.canvas.winfo_height()) + self.canvas.configure(scrollregion=(0, 0, self.canvas.winfo_width(), h)) + self.canvas.yview_moveto(1.0) + except Exception: + pass - def _scroll_bottom(self): - self.root.after_idle(lambda: self.canvas.yview_moveto(1.0)) + self.root.after_idle(go) # ---------- 交互 ---------- def _on_enter(self, event): + if event.state & 0x0001: # Shift+Enter → 换行 + return None self._on_send() - return "break" # 阻止 Enter 插入换行 + return "break" + + def _clear_input(self): + self.input.delete("1.0", "end") def _on_send(self): if self._busy: @@ -383,58 +1141,272 @@ def _on_send(self): self._add_user(text) if not self._serve_ready: - self._add_note("检索服务尚未就绪,正在启动,请稍候几秒再试...") + self._add_note("检索服务尚未就绪,正在启动,请稍候几秒再试…") threading.Thread(target=self._ensure_serve, daemon=True).start() return + self._run_query(text) + + def _run_query(self, text: str): + """检索 + 生成。重试时复用同一个问题,不再追加用户气泡。""" + self._last_question = text + self._stopped = False + self._stop_event.clear() + # 记进历史("重新生成"同问同答,会在存储层合并成一条) + self._hist_entry = self.history.remember(text) + self._hist_pos = None + self._hist_draft = "" self._busy = True - self._add_note("⏳ 正在检索截图并生成答案,请稍候...") + self._update_send_button() + self._show_stop(True) + self._start_loading("正在检索相关页面") threading.Thread(target=self._do_search, args=(text,), daemon=True).start() + def _on_stop(self): + """停止生成:置取消信号,后台循环下一轮就退出,已收到的文字保留。""" + if not self._busy: + return + self._stopped = True + self._stop_event.set() + if self._loading_active: + self._set_loading_text("正在停止…") + self._update_stop_button() + + def _on_regenerate(self): + """重新生成:用上一次的问题重跑。答案为空或生成失败时给出这个入口。""" + if self._busy or not self._last_question: + return + self._run_query(self._last_question) + + def _select_all_input(self, _e=None): + self.input.tag_add("sel", "1.0", "end-1c") + self.input.mark_set("insert", "1.0") + return "break" + + # ---------- 查询历史 ---------- + + def _on_history(self): + if self._history_panel is not None: + self._history_panel.win.lift() + return + self._history_panel = H.HistoryPanel( + self.root, + self.history, + on_pick=self._reask, + on_close=self._forget_history_panel, + ) + + def _forget_history_panel(self): + self._history_panel = None + + def _reask(self, question: str): + """从历史里点一条:填进输入框并直接提问。""" + self._replace_input(question) + self._on_send() + + def _replace_input(self, text: str): + self.input.delete("1.0", "end") + self.input.insert("1.0", text) + self.input.mark_set("insert", "end-1c") + self.input.focus_set() + + def _on_input_up(self, _e=None): + """↑:光标在首行时召回上一条历史问题。""" + on_first_line = self.input.index("insert").split(".")[0] == "1" + if self._hist_pos is not None or on_first_line: + return self._recall_history(+1) + return None # 交给 tk:光标上移一行 + + def _on_input_down(self, _e=None): + """↓:只有在召回态才接管(往回翻到更新的一条)。""" + if self._hist_pos is None: + return None + return self._recall_history(-1) + + def _recall_history(self, delta: int): + """``delta=+1`` 往更早翻,``-1`` 往更新翻;翻过头就恢复召回前的草稿。""" + items = self.history.questions() + if not items: + return None + if self._hist_pos is None: + self._hist_draft = self.input.get("1.0", "end-1c") + self._hist_pos = 0 + else: + self._hist_pos += delta + if self._hist_pos < 0: # 回到最新之后 = 回到草稿 + self._hist_pos = None + self._replace_input(self._hist_draft) + return "break" + self._hist_pos = min(self._hist_pos, len(items) - 1) + self._replace_input(items[self._hist_pos]) + return "break" + + def _send_hover_on(self): + self._send_hover = True + self._update_send_button() + + def _send_hover_off(self): + self._send_hover = False + self._update_send_button() + + def _stop_hover_on(self): + self._stop_hover = True + self._update_stop_button() + + def _stop_hover_off(self): + self._stop_hover = False + self._update_stop_button() + + def _show_stop(self, show: bool): + """打包顺序:send 先占最右,stop 紧跟其后排到它左边,box 再吃掉剩余宽度。""" + if show == self._stop_shown: + return + self._stop_shown = show + if show: + self._update_stop_button() + self.stop_btn.pack( + side="right", + after=self.send_btn, + padx=(0, T.SPACE["sm"]), + anchor="s", + ) + else: + self.stop_btn.pack_forget() + + def _update_stop_button(self): + if not self._stop_shown: + return + fill = T.c("border_strong") if self._stop_hover else T.c("card") + img = W.render_label_box( + "停止生成", + "button", + T.c("text"), + T.c("bg"), + box_fill=fill, + outline=T.c("border_strong"), + radius=T.RADIUS["sm"], + pad_x=T.SPACE["lg"], + pad_y=T.SPACE["sm"], + align="center", + ) + photo = W.photo_of(img) + self._stop_photo = photo + try: + self.stop_btn.configure(image=photo, cursor="hand2") + self.stop_btn.image = photo + except Exception: + pass + + def _update_send_button(self): + if self._busy: + fill, fg, label, cursor = ( + T.c("border"), + T.c("text_faint"), + "生成中", + "arrow", + ) + elif self._send_hover: + fill, fg, label, cursor = ( + T.c("accent_hover"), + T.c("on_accent"), + "发送", + "hand2", + ) + else: + fill, fg, label, cursor = T.c("accent"), T.c("on_accent"), "发送", "hand2" + img = W.render_label_box( + label, + "button", + fg, + T.c("bg"), + box_fill=fill, + radius=T.RADIUS["sm"], + pad_x=T.SPACE["lg"], + pad_y=T.SPACE["sm"], + align="center", + ) + photo = W.photo_of(img) + self._send_photo = photo + try: + self.send_btn.configure(image=photo, cursor=cursor) + self.send_btn.image = photo + except Exception: + pass + + # ---------- 检索 → 生成 ---------- + def _do_search(self, text: str): try: resp = search(text) hits = resp["results"][0]["hits"] except Exception as e: + self.root.after(0, self._stop_loading) self.root.after(0, lambda err=e: self._show_error(err)) self.root.after(0, self._clear_busy) return if not hits: - self.root.after(0, lambda: self._add_note("没有找到相关结果,换个说法试试。")) + self.root.after(0, self._stop_loading) + self.root.after( + 0, lambda: self._add_note("没有找到相关结果,换个说法试试。") + ) self.root.after(0, self._clear_busy) return self._generate(text, hits) # 仍在后台线程,继续生成答案 def _show_results(self, hits: list): - if not hits: - self._add_note("没有找到相关结果,换个说法试试。") - return - for i, h in enumerate(hits, 1): - self._add_result_card(h, i) + """VLM 不可用时的降级:只展示检索到的引用卡片。""" + seen, tiles = set(), [] + for h in sorted(hits, key=lambda x: float(x.get("score", 0.0)), reverse=True): + key = (h.get("article_id"), h.get("tile_index"), h.get("chunk_index")) + if key in seen: + continue + seen.add(key) + b64 = h.get("image_base64") + if b64: + tiles.append((b64, h)) + if tiles: + self._append({"kind": "citations", "tiles": tiles}) + else: + self._add_note("没有可显示的截图。") def _generate(self, text: str, hits: list): """准备截图 → 调 VLM 流式生成 → 前端流式显示答案 + 引用卡片。运行于后台线程。""" + if self._stop_event.is_set(): + # 检索期间用户就点了停止,别再开生成 + self.root.after(0, self._stop_loading) + self.root.after(0, lambda: self._add_note("已停止。")) + self.root.after(0, self._clear_busy) + return + images = prepare_images(hits) if not images: - self.root.after(0, lambda: self._add_error( - "检索到了结果,但截图加载失败。以下为检索到的相关页面:" - )) + self.root.after(0, self._stop_loading) + self.root.after( + 0, + lambda: self._add_error( + "检索到了结果,但截图加载失败。以下为检索到的相关页面:" + ), + ) self.root.after(0, lambda: self._show_results(hits)) self.root.after(0, self._clear_busy) return cfg = _load_vlm_config() if not cfg["api_key"]: - self.root.after(0, lambda: self._add_error( - "未找到 VLM API key。请设置环境变量 PIXELRAG_VLM_API_KEY,\n" - "或确保 ~/.claude/settings.json 里有 ANTHROPIC_AUTH_TOKEN。\n" - "以下为检索到的相关页面:" - )) + self.root.after(0, self._stop_loading) + self.root.after( + 0, + lambda: self._add_error( + "未找到 VLM API key。请设置环境变量 PIXELRAG_VLM_API_KEY,\n" + "或确保 ~/.claude/settings.json 里有 ANTHROPIC_AUTH_TOKEN。\n" + "以下为检索到的相关页面:" + ), + ) self.root.after(0, lambda: self._show_results(hits)) self.root.after(0, self._clear_busy) return - # 在 UI 线程创建助手气泡,拿到可更新的 Label + self.root.after(0, lambda: self._set_loading_text("正在生成答案")) self._answer_ready = threading.Event() self.root.after(0, self._begin_answer) if not self._answer_ready.wait(timeout=5): @@ -442,55 +1414,187 @@ def _generate(self, text: str, hits: list): return full = "" + stopped = False + failed = False try: - for chunk in vlm_stream(images, text, cfg): + self._gen = vlm_stream(images, text, cfg) + for chunk in self._gen: + if self._stop_event.is_set(): + stopped = True + break full += chunk self.root.after(0, lambda t=full: self._set_answer(t)) except Exception as e: - self.root.after(0, lambda err=e: self._add_error(f"生成失败:{err}")) - if full: - self.root.after(0, lambda: self._set_answer(full + "\n\n[生成中断]")) + if self._stop_event.is_set(): + stopped = True # 主动中止引发的异常不算失败,别再弹一次错 + else: + failed = True # 已经报过错了,_finish_answer 别再补一条"没返回内容" + err = str(e) + self.root.after( + 0, lambda m=err: self._add_error(f"生成失败:{m}", retry=True) + ) + if full: + self.root.after(0, lambda t=full: self._set_answer(t)) finally: - self.root.after(0, lambda: self._finish_answer(images)) + # 显式 close 掉生成器,底层的 HTTP 流立刻断开,不用等 GC + gen, self._gen = self._gen, None + if gen is not None: + try: + gen.close() + except Exception: + pass + self.root.after(0, lambda: self._finish_answer(images, stopped, failed)) def _begin_answer(self): - row = tk.Frame(self.frame, bg=BG) - row.pack(fill="x", padx=16, pady=(14, 2)) - lbl = tk.Label( - row, text="", bg=CARD_BG, fg=TEXT_FG, justify="left", anchor="w", - wraplength=680, font=(FONT, 12), padx=14, pady=10, - ) - lbl.pack(side="left", fill="x", expand=True) - self._answer_label = lbl + self._stop_loading() + msg = {"kind": "answer", "text": "", "streaming": True} + self.messages.append(msg) + self._render_message(msg) + self._answer_msg = msg + self._scroll_bottom(True) self._answer_ready.set() - self._scroll_bottom() def _set_answer(self, text: str): - lbl = getattr(self, "_answer_label", None) - if lbl is not None: - lbl.configure(text=text) - self._scroll_bottom() - - def _finish_answer(self, images): - lbl = getattr(self, "_answer_label", None) - if lbl is not None and not lbl.cget("text"): - lbl.configure(text="(VLM 未返回内容,请重试。)") + msg = self._answer_msg + if msg is None: + return + msg["text"] = text + if self._paint_job is None: + self._paint_job = self.root.after( + T.MOTION["stream_throttle"], self._paint_answer + ) + + def _paint_answer(self): + self._paint_job = None + msg, txt = self._answer_msg, self._answer_text + if msg is None or txt is None: + return + text = msg.get("text", "") + stick = self._at_bottom() + try: + self._set_answer_into(txt, (text + " ▌") if text else "…") + except tk.TclError: + return # 主题切换把控件重建了,这一帧直接丢掉 + self._scroll_bottom(stick) + + def _finish_answer(self, images, stopped: bool = False, failed: bool = False): + if self._paint_job: + try: + self.root.after_cancel(self._paint_job) + except Exception: + pass + self._paint_job = None + + msg = self._answer_msg + self._answer_msg = None + self._answer_text = None + + if msg is not None: + msg["streaming"] = False + text = msg.get("text", "") + if stopped: + # 保留已经吐出来的部分,补一句话说明是用户主动停的 + msg["text"] = ( + (text.rstrip() + "\n\n(已停止生成)") + if text.strip() + else "(已停止生成)" + ) + self._redraw_answer(msg) + elif not text.strip(): + # 一个字都没有:留个空气泡没意义,换成可重试的错误。 + # failed 时上面已经报过具体原因了,这里不再补一条重复的。 + self._drop_message(msg) + if not failed: + self._add_error("模型没有返回任何内容。", retry=True) + else: + self._redraw_answer(msg) + + # 回填历史:命中页数 + 答案(方便以后直接复制,不用重跑一遍) + self.history.update( + self._hist_entry, + hits=len(images), + answer=(msg or {}).get("text", ""), + ) + if images: - self._add_note("引用来源:") - for i, (_b64, hit) in enumerate(images, 1): - self._add_result_card(hit, i) + self._append({"kind": "citations", "tiles": list(images)}) + self._scroll_bottom(True) self._clear_busy() + def _redraw_answer(self, msg: dict): + """定稿重绘:分段代码块 + 复制按钮。""" + widget = msg.get("_widget") + if widget is not None: + try: + widget.destroy() + except Exception: + pass + self._render_message(msg) + + def _drop_message(self, msg: dict): + """把一条消息从数据模型和界面上一起撤掉。""" + try: + self.messages.remove(msg) + except ValueError: + pass + widget = msg.get("_widget") + if widget is not None: + try: + widget.destroy() + except Exception: + pass + def _show_error(self, e: Exception): self._add_error( - f"查询失败:{e}\n请确认检索服务已启动、索引文件完整。" + f"查询失败:{e}\n请确认检索服务已启动、索引文件完整。", retry=True ) def _clear_busy(self): self._busy = False + self._stop_hover = False + self._show_stop(False) + self._update_send_button() + + # ---------- 主题 ---------- + + def _toggle_theme(self): + T.set_mode(T.other_mode()) + W._clear_cache() + self._apply_theme() + + def _apply_theme(self): + """换主题 = 用令牌重放全部界面(tkinter 不支持热改样式)。""" + draft = self.input.get("1.0", "end-1c") + state = self._view_state() # 必须在销毁旧界面之前取 + # 历史面板是独立窗口,颜色也是构建时取的 —— 关掉,重建完再打开 + reopen_history = self._history_panel is not None + if self._history_panel is not None: + self._history_panel.close() + self._history_panel = None + + for w in self.root.winfo_children(): + w.destroy() + self._photos.clear() + self._answer_text = None + + self.root.configure(bg=T.c("bg")) + self._build_ui() + self.input.insert("1.0", draft) + self._render_all(view_state=state) + self._set_status() + self._update_send_button() + if reopen_history: + self._on_history() # ---------- 打开图片 / PDF ---------- + def _open_hit(self, hit: dict, title: str): + url = hit.get("url", "") + if url and os.path.exists(url): + os.startfile(url) + else: + self._add_note(f"找不到 PDF 文件:{title}") + def _open_image(self, b64: str, title: str): try: img = decode_b64_image(b64) @@ -498,21 +1602,19 @@ def _open_image(self, b64: str, title: str): return win = tk.Toplevel(self.root) win.title(title or "页面截图") - win.configure(bg="#000000") + win.configure(bg=T.c("bg")) max_w = int(win.winfo_screenwidth() * 0.85) max_h = int(win.winfo_screenheight() * 0.85) img.thumbnail((max_w, max_h), Image.LANCZOS) - photo = ImageTk.PhotoImage(img) + photo = W.photo_of(img) self._photos.append(photo) - lbl = tk.Label(win, image=photo, bg="#000000") + lbl = tk.Label(win, image=photo, bg=T.c("bg"), bd=0) lbl.image = photo lbl.pack() - - def _open_pdf(self, url: str): - if url and os.path.exists(url): - os.startfile(url) - else: - self._add_note("找不到该论文 PDF 文件。") + # 焦点必须落在预览窗内,Escape 才会送到这里(否则被主窗吃掉) + win.bind("", lambda _e: win.destroy()) + win.transient(self.root) + win.focus_set() # ---------- 服务 ---------- @@ -534,6 +1636,13 @@ def _ensure_serve(self): def main(): + try: + import ctypes + + ctypes.windll.shcore.SetProcessDpiAwareness(1) # 高分屏下不再被位图拉伸 + except Exception: + pass + T.set_ui_scale(screen_scale()) root = tk.Tk() App(root) root.mainloop() diff --git a/pixelrag_history.py b/pixelrag_history.py new file mode 100644 index 0000000..5cb178f --- /dev/null +++ b/pixelrag_history.py @@ -0,0 +1,416 @@ +"""查询历史:落盘存储 + 查看面板 + 输入框上下键召回。 + +历史写在用户目录下的 ``~/.pixelrag/history.json``(不放仓库里,免得污染项目), +一条记录 = 时间 / 问题 / 命中页数 / 答案。存储只做增删查,不涉及任何检索逻辑。 +""" + +from __future__ import annotations + +import json +import os +import time +import tkinter as tk +from collections.abc import Callable +from datetime import datetime, timezone + +import pixelrag_theme as T +import pixelrag_widgets as W + +HISTORY_PATH = os.path.join(os.path.expanduser("~"), ".pixelrag", "history.json") +MAX_ITEMS = 200 # 只留最近这么多条 +REUSE_WINDOW = 300 # 秒;同一个问题在这段时间内重复提交(含"重新生成")算同一条 + + +# --------------------------------------------------------------------------- +# 存储 +# --------------------------------------------------------------------------- + + +class History: + """查询历史。读写都容错 —— 历史存不下来不应该影响问答。""" + + def __init__(self, path: str | None = None): + # 取调用时的模块级常量(而不是把它当默认参数固化),改 HISTORY_PATH 能生效 + self.path = path or HISTORY_PATH + self._items: list[dict] = self._load() + + def _load(self) -> list[dict]: + try: + with open(self.path, encoding="utf-8") as f: + data = json.load(f) + except Exception: + return [] + if not isinstance(data, list): + return [] + return [d for d in data if isinstance(d, dict) and d.get("question")] + + def items(self) -> list[dict]: + """新的在前(面板按这个顺序显示)。""" + return list(reversed(self._items)) + + def questions(self) -> list[str]: + """问题列表,新的在前(输入框 ↑/↓ 用)。""" + return [d["question"] for d in self.items()] + + def remember(self, question: str) -> dict: + """记一条提问。刚问过的同一个问题(比如点"重新生成")复用原条目。""" + now = time.time() + if self._items: + last = self._items[-1] + if ( + last.get("question") == question + and now - float(last.get("ts") or 0) < REUSE_WINDOW + ): + last["time"] = _iso(now) + last["ts"] = now + self._save() + return last + item = { + "question": question, + "time": _iso(now), + "ts": now, + "hits": None, + "answer": "", + } + self._items.append(item) + if len(self._items) > MAX_ITEMS: + del self._items[:-MAX_ITEMS] + self._save() + return item + + def update( + self, item: dict | None, hits: int | None = None, answer: str | None = None + ): + """问答结束后回填命中页数与答案。""" + if not item: + return + if hits is not None: + item["hits"] = int(hits) + if answer is not None: + item["answer"] = answer + self._save() + + def clear(self) -> None: + self._items = [] + self._save() + + def _save(self) -> None: + try: + os.makedirs(os.path.dirname(self.path), exist_ok=True) + tmp = self.path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(self._items, f, ensure_ascii=False, indent=1) + os.replace(tmp, self.path) # 原子替换,写一半断电也不会留半截文件 + except Exception: + pass + + +def _iso(ts: float) -> str: + """带时区偏移的 ISO 时间戳(换时区/夏令时也不会看错)。""" + return ( + datetime.fromtimestamp(ts, tz=timezone.utc) + .astimezone() + .isoformat(timespec="seconds") + ) + + +def _pretty(iso: str) -> str: + """显示成本地时间。统一用 ``MM-DD HH:MM``:每行等宽,问题那列才对得齐。""" + try: + dt = datetime.fromisoformat(iso).astimezone() + except Exception: + return "" + return dt.strftime("%m-%d %H:%M") + + +# --------------------------------------------------------------------------- +# 面板 +# --------------------------------------------------------------------------- + + +class HistoryPanel: + """历史列表面板(独立窗口)。 + + ``on_pick(question)``:点某条问题 / 点「重问」时回调,由调用方决定怎么重问。 + """ + + def __init__( + self, + root: tk.Tk, + history: History, + on_pick: Callable[[str], None], + on_clear: Callable[[], None] | None = None, + on_close: Callable[[], None] | None = None, + ): + self.root = root + self.history = history + self.on_pick = on_pick + self.on_clear = on_clear + self.on_close = on_close + self._closed = False + + win = tk.Toplevel(root) + self.win = win + win.title("查询历史") + win.configure(bg=T.c("bg")) + win.transient(root) + w, h = int(620 * T.UI_SCALE), int(560 * T.UI_SCALE) + win.geometry(f"{w}x{h}+{root.winfo_rootx() + 80}+{root.winfo_rooty() + 60}") + win.minsize(int(420 * T.UI_SCALE), int(260 * T.UI_SCALE)) + win.bind("", lambda _e: self.close()) + win.bind("", self._on_wheel) + win.protocol("WM_DELETE_WINDOW", self.close) + self._build() + self._render_rows() + win.focus_set() + + # ---------- 骨架 ---------- + + def _build(self): + head = tk.Frame(self.win, bg=T.c("surface")) + head.pack(side="top", fill="x") + inner = tk.Frame(head, bg=T.c("surface")) + inner.pack(fill="x", padx=T.SPACE["lg"], pady=T.SPACE["sm"]) + + tk.Label( + inner, text="查询历史", bg=T.c("surface"), fg=T.c("text"), font=T.font("h2") + ).pack(side="left") + self.count_lbl = tk.Label( + inner, + text="", + bg=T.c("surface"), + fg=T.c("text_faint"), + font=T.font("caption"), + ) + self.count_lbl.pack( + side="left", padx=(T.SPACE["sm"], 0), pady=(T.SPACE["xs"], 0) + ) + + self.clear_btn = tk.Label( + inner, + text="清空历史", + bg=T.c("surface"), + fg=T.c("text_muted"), + font=T.font("caption"), + cursor="hand2", + padx=T.SPACE["sm"], + pady=T.SPACE["xs"], + ) + self.clear_btn.pack(side="right") + self.clear_btn.bind("", lambda _e: self._clear()) + W.bind_hover( + self.clear_btn, + on_enter=lambda: self.clear_btn.configure(fg=T.c("danger")), + on_leave=lambda: self.clear_btn.configure(fg=T.c("text_muted")), + ) + W.make_focusable(self.clear_btn, T.c("surface"), self._clear) + tk.Frame(self.win, bg=T.c("border"), height=1).pack(side="top", fill="x") + + body = tk.Frame(self.win, bg=T.c("bg")) + body.pack(side="top", fill="both", expand=True) + self.canvas = tk.Canvas(body, bg=T.c("bg"), highlightthickness=0, bd=0) + self.scroll = tk.Scrollbar( + body, + orient="vertical", + command=self.canvas.yview, + bg=T.c("scrollbar"), + troughcolor=T.c("bg"), + activebackground=T.c("border_strong"), + borderwidth=0, + ) + self.scroll.pack(side="right", fill="y") + self.canvas.pack(side="left", fill="both", expand=True) + self.canvas.configure(yscrollcommand=self.scroll.set) + + self.body = tk.Frame(self.canvas, bg=T.c("bg")) + self._win = self.canvas.create_window((0, 0), window=self.body, anchor="nw") + self.body.bind( + "", + lambda _e: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", lambda e: self.canvas.itemconfigure(self._win, width=e.width) + ) + + # 面板变宽时问题重新折行(只绑一次,一次改所有行) + self._q_labels: list[tk.Label] = [] + self.win.bind("", self._on_resize) + + tip = tk.Label( + self.win, + text="点问题即可重新提问 · ↑ / ↓ 在输入框里快速召回", + bg=T.c("bg"), + fg=T.c("text_faint"), + font=T.font("micro"), + ) + tip.pack(side="bottom", pady=(T.SPACE["xs"], T.SPACE["sm"])) + + def _on_wheel(self, event): + try: + self.canvas.yview_scroll(int(-event.delta / 120) * 3, "units") + except Exception: + pass + + def _on_resize(self, event): + wrap = max(120, event.width - int(360 * T.UI_SCALE)) + for lbl in self._q_labels: + try: + lbl.configure(wraplength=wrap) + except tk.TclError: + # 列表刚重绘过,旧标签已经销毁;Configure 事件是排队来的,可能晚到 + pass + + # ---------- 列表 ---------- + + def _render_rows(self): + for w in self.body.winfo_children(): + w.destroy() + self._q_labels = [] # 上一批标签已经销毁,别再拿去改 wraplength + items = self.history.items() + self.count_lbl.configure(text=f"{len(items)} 条") + if not items: + tk.Label( + self.body, + text="还没有查询记录。\n问过的问题会自动记在这里,关掉程序也不会丢。", + bg=T.c("bg"), + fg=T.c("text_faint"), + font=T.font("body"), + justify="left", + ).pack(anchor="w", padx=T.SPACE["xl"], pady=T.SPACE["xl"]) + return + for i, it in enumerate(items): + self._row(it) + if i != len(items) - 1: + tk.Frame(self.body, bg=T.c("border"), height=1).pack(fill="x") + + def _row(self, item: dict): + base = T.c("bg") + row = tk.Frame(self.body, bg=base) + row.pack(fill="x") + + meta_bits = [] + if item.get("hits") is not None: + meta_bits.append(f"{item['hits']} 页命中") + else: + meta_bits.append("未完成") + if item.get("answer"): + meta_bits.append("有答案") + tk.Label( + row, + text=" · ".join(meta_bits), + bg=base, + fg=T.c("text_faint"), + font=T.font("micro"), + ).pack(side="right", padx=(T.SPACE["md"], T.SPACE["lg"])) + + ask = tk.Label( + row, + text="重问", + bg=base, + fg=T.c("accent"), + font=T.font("micro"), + cursor="hand2", + padx=T.SPACE["xs"], + ) + ask.pack(side="right") + ask.bind("", lambda _e, q=item["question"]: self._pick(q)) + if item.get("answer"): + cp_a = tk.Label( + row, + text="复制答案", + bg=base, + fg=T.c("text_muted"), + font=T.font("micro"), + cursor="hand2", + padx=T.SPACE["xs"], + ) + cp_a.pack(side="right") + cp_a.bind("", lambda _e, t=item["answer"]: self._copy(t)) + else: + cp_a = None + cp_q = tk.Label( + row, + text="复制问题", + bg=base, + fg=T.c("text_muted"), + font=T.font("micro"), + cursor="hand2", + padx=T.SPACE["xs"], + ) + cp_q.pack(side="right") + cp_q.bind("", lambda _e, t=item["question"]: self._copy(t)) + + # 左边一块(时间 + 问题)整体可点 = 重问 + hit_area = tk.Frame(row, bg=base) + hit_area.pack(side="left", fill="x", expand=True, padx=(0, T.SPACE["md"])) + tk.Label( + hit_area, + text=_pretty(item.get("time", "")), + bg=base, + fg=T.c("text_muted"), + font=T.font("caption"), + ).pack(side="left", pady=T.SPACE["sm"]) + q_lbl = tk.Label( + hit_area, + text=item["question"], + bg=base, + fg=T.c("text"), + font=T.font("body"), + justify="left", + anchor="w", + wraplength=int(340 * T.UI_SCALE), + ) + q_lbl.pack(side="left", fill="x", expand=True, padx=(T.SPACE["md"], 0)) + self._q_labels.append(q_lbl) + + def enter(_e=None): + W.set_bg_recursive(row, T.c("card")) + q_lbl.configure(fg=T.c("accent")) + + def leave(_e=None): + W.set_bg_recursive(row, base) + q_lbl.configure(fg=T.c("text")) + + W.bind_hover(row, on_enter=enter, on_leave=leave) + W.bind_click_recursive(hit_area, lambda q=item["question"]: self._pick(q)) + W.set_cursor_recursive(hit_area) + + # ---------- 动作 ---------- + + def _pick(self, question: str): + self.close() + self.on_pick(question) + + def _copy(self, text: str): + try: + self.root.clipboard_clear() + self.root.clipboard_append(text) + except tk.TclError: + pass + + def _clear(self): + from tkinter import messagebox + + try: + ok = messagebox.askyesno( + "清空历史", "确定清空全部查询历史?此操作不可撤销。", parent=self.win + ) + except Exception: + ok = True + if not ok: + return + self.history.clear() + self._render_rows() + if self.on_clear: + self.on_clear() + + def close(self): + if self._closed: + return + self._closed = True + try: + self.win.destroy() + except Exception: + pass + if self.on_close: + self.on_close() diff --git a/pixelrag_theme.py b/pixelrag_theme.py new file mode 100644 index 0000000..e1fedf6 --- /dev/null +++ b/pixelrag_theme.py @@ -0,0 +1,187 @@ +"""PixelRAG 桌面客户端设计令牌 —— 方向 A「Notion 纸感」。 + +这是全项目唯一的"样式表"。所有颜色 / 字号 / 间距 / 圆角 / 动效时长都必须 +从这里取,业务代码里不允许再出现硬编码色值或魔法数字。 + +浅色(light)为默认主题,深色(dark)是等价反转的备选主题。 +两个色板必须拥有完全相同的键,否则切换时会 KeyError。 +""" + +from __future__ import annotations + +# --------------------------------------------------------------------------- +# 色板 +# --------------------------------------------------------------------------- +PALETTES: dict[str, dict[str, str]] = { + # 取色原则:正文与提示文字对底色的对比度需 ≥ 4.5:1(WCAG AA 普通文本)。 + # 括号里是对"文字落到自己常见底色上"的实测对比度,改动色值时请一并复核。 + # ---- 浅色:Notion 纸感(默认)---- + "light": { + "bg": "#FFFFFF", # 页面底色 + "surface": "#F7F7F5", # 侧栏 / 顶栏 / 底栏 + "card": "#FFFFFF", # 卡片表面 + "card_hover": "#FAFAF9", # 卡片 hover + "border": "#E9E9E7", # 常规分隔线 + "border_strong": "#D3D1CB", # hover / focus 时的边框 + "text": "#37352F", # 正文(14.9:1 on bg) + "text_muted": "#6B6A67", # 次要信息(5.4:1 on bg) + "text_faint": "#767571", # 极淡提示(4.6:1 on bg) + "accent": "#1A73CC", # 主色(4.8:1 on bg;白字落其上同为 4.8:1) + "accent_hover": "#155FB0", + "accent_soft": "#E7F1FB", # 主色淡底(hover 态底,非正文底) + "on_accent": "#FFFFFF", # 主色之上的文字 + "user_bubble": "#F1F1EF", # 用户消息淡灰块 + "user_text": "#37352F", # 用户消息文字 + "code_bg": "#F7F6F3", # 行内代码 / 代码块底 + "success": "#0F7B6C", # 5.2:1 on bg + "warning": "#8A6100", # 5.5:1 on bg + "danger": "#C0392B", # 5.0:1 on danger_bg + "danger_bg": "#FDF2F2", # 错误提示底 + "input_bg": "#FFFFFF", + "input_line": "#E9E9E7", # 输入框底部 1px 线(未聚焦) + "focus": "#1A73CC", # 键盘焦点环 + "scrollbar": "#D3D1CB", + "skeleton": "#F1F1EF", # 加载骨架 + }, + # ---- 深色:等价反转 ---- + "dark": { + "bg": "#191919", + "surface": "#202020", + "card": "#252525", + "card_hover": "#2C2C2C", + "border": "#333331", + "border_strong": "#4A4A47", + "text": "#E6E6E4", + "text_muted": "#A8A7A3", + "text_faint": "#8F8E8B", + "accent": "#5B9BE8", + "accent_hover": "#7CB0F0", + "accent_soft": "#1E2A3A", + "on_accent": "#0F1419", # 深色底上主色偏亮,配深墨字才够对比(6.4:1) + "user_bubble": "#2F2F2E", + "user_text": "#E6E6E4", + "code_bg": "#202020", + "success": "#4ADE80", + "warning": "#FACC15", + "danger": "#F87171", + "danger_bg": "#3A1D1D", + "input_bg": "#202020", + "input_line": "#333331", + "focus": "#5B9BE8", + "scrollbar": "#4A4A47", + "skeleton": "#2F2F2E", + }, +} + +DEFAULT_MODE = "light" + +_mode = DEFAULT_MODE + + +def mode() -> str: + """当前主题名(light / dark)。""" + return _mode + + +def set_mode(name: str) -> str: + """切换主题并返回生效的主题名。""" + global _mode + if name not in PALETTES: + raise ValueError(f"未知主题:{name!r}(可选:{sorted(PALETTES)})") + _mode = name + return _mode + + +def other_mode() -> str: + return "dark" if _mode == "light" else "light" + + +def c(key: str) -> str: + """按语义键取当前主题色。""" + try: + return PALETTES[_mode][key] + except KeyError: + raise KeyError(f"色板 {_mode!r} 中没有键 {key!r}") from None + + +# --------------------------------------------------------------------------- +# 排版 +# --------------------------------------------------------------------------- +FONT_FAMILY = "Microsoft YaHei UI" # 中文优先;缺失时 tk 会自行回退 +FONT_MONO = "Consolas" + +# token -> (字号 pt, 字重)。方向 A 的层级:20 / 16 / 15 / 14 / 12 / 11 px, +# 换算成 tk 的点值并整体略放大以保证中文可读性。 +TYPE: dict[str, tuple[int, str]] = { + "title": (15, "bold"), # 顶栏标题 + "h2": (12, "bold"), # 区块标题 / 引用来源 + "body": (11, "normal"), # 正文 + "body_lg": (12, "normal"), # 输入框 + "label": (11, "bold"), # 卡片标题 + "button": (11, "bold"), # 按钮 + "caption": (9, "normal"), # 卡片元信息 + "micro": (8, "normal"), # 角标 +} + + +# 全局字号倍率:**嫌字小就调大这一个数**(1.15 ≈ 正文 11→12.6pt、标题 15→17pt)。 +# tk 绘制与 Pillow 离屏绘制都从这里换算,保证两边的字一样大。 +FONT_SCALE = 1.15 + + +def pt(token: str) -> int: + """取某个 token 的实际字号(点值,已乘 FONT_SCALE)。 + + tk 的字体元组只接受整数点值,所以在这里就取整;Pillow 那边也从这里换算, + 保证界面上两种绘制方式的字一样大。 + """ + return max(1, round(TYPE[token][0] * FONT_SCALE)) + + +def font(token: str) -> tuple: + """取 tk 字体元组,如 ('Microsoft YaHei UI', 12.6, 'normal')。""" + return (FONT_FAMILY, pt(token), TYPE[token][1]) + + +def font_mono(size: int = 10) -> tuple: + """等宽字体(代码块)。``size`` 是设计字号,同样会乘 FONT_SCALE。""" + return (FONT_MONO, max(1, round(size * FONT_SCALE)), "normal") + + +# --------------------------------------------------------------------------- +# 间距(4px 基准) / 圆角 / 动效 +# --------------------------------------------------------------------------- +_BASE_SPACE = {"xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "2xl": 32} +_BASE_RADIUS = {"sm": 6, "md": 8, "lg": 12} +_BASE_CONTENT_MAX_WIDTH = 720 + +# 下面三个是"逻辑值 × UI_SCALE"的结果;启动时由 set_ui_scale 按屏幕 DPI 重算, +# 以便在高分屏上字变大的同时,内边距/圆角/行宽等比跟上,不会显得局促。 +SPACE: dict[str, int] = dict(_BASE_SPACE) +RADIUS: dict[str, int] = dict(_BASE_RADIUS) +CONTENT_MAX_WIDTH = _BASE_CONTENT_MAX_WIDTH +UI_SCALE = 1.0 + + +def set_ui_scale(factor: float) -> float: + """按屏幕缩放比例重算所有像素级令牌(0.75x ~ 2.0x)。""" + global UI_SCALE, CONTENT_MAX_WIDTH + UI_SCALE = max(0.75, min(2.0, float(factor))) + for k, v in _BASE_SPACE.items(): + SPACE[k] = max(1, round(v * UI_SCALE)) + for k, v in _BASE_RADIUS.items(): + RADIUS[k] = max(2, round(v * UI_SCALE)) + CONTENT_MAX_WIDTH = round(_BASE_CONTENT_MAX_WIDTH * UI_SCALE) + return UI_SCALE + + +# 动效时长(毫秒)。tkinter 无 CSS,统一用 root.after 驱动。 +MOTION: dict[str, int] = { + "fast": 120, + "base": 180, + "stream_throttle": 60, # 流式重绘节流间隔 + "resize_debounce": 120, # 窗口尺寸变化防抖 +} + +# 正文最大宽度(px):超过则两侧留白,保证长文阅读行宽舒适 +CONTENT_MAX_WIDTH = 720 diff --git a/pixelrag_widgets.py b/pixelrag_widgets.py new file mode 100644 index 0000000..4b3b824 --- /dev/null +++ b/pixelrag_widgets.py @@ -0,0 +1,504 @@ +"""PixelRAG 桌面客户端的绘制与交互原语。 + +tkinter 原生没有圆角、没有 hover 状态、文字也不可选中复制。本模块用 Pillow +离屏渲染补齐这些能力: + +- ``rounded_photo`` : 抗锯齿圆角/描边矩形(带缓存,避免重复渲染与 PhotoImage 被 GC) +- ``render_label_box``: 把文本直接画进圆角框,返回整块图片(用户气泡 / 按钮) +- ``bind_hover`` : 正确的 hover 绑定(处理指针移入子控件时的假 Leave) +- ``bind_click_recursive``: 让整张卡片(含所有子控件)都可点击 + +只做"画"和"绑事件",不涉及任何业务逻辑。 +""" + +from __future__ import annotations + +import re +import tkinter as tk +import tkinter.font as tkfont +from collections.abc import Callable +from functools import lru_cache + +from PIL import Image, ImageDraw, ImageFont, ImageTk + +import pixelrag_theme as T + +# --------------------------------------------------------------------------- +# 字体(PIL 侧需要真实字号像素值,而 tk 用点值) +# --------------------------------------------------------------------------- + + +@lru_cache(maxsize=4) +def _font_path(bold: bool) -> str | None: + candidates = ( + ( + "C:/Windows/Fonts/msyhbd.ttc", + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + "C:/Windows/Fonts/segoeuib.ttf", + ) + if bold + else ( + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + "C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/arial.ttf", + ) + ) + import os + + for p in candidates: + if os.path.exists(p): + return p + return None + + +@lru_cache(maxsize=64) +def pil_font(size_px: int, bold: bool = False) -> ImageFont.FreeTypeFont: + """取 PIL 字体;系统字体缺失时回退到位图字体(不会崩)。""" + path = _font_path(bold) + if not path: + return ImageFont.load_default() + try: + return ImageFont.truetype(path, size_px) + except Exception: + return ImageFont.load_default() + + +def font_px(token: str) -> int: + """把主题里的点值字号换算成像素(96dpi:px = pt * 4/3),已含 FONT_SCALE。""" + return max(10, round(T.pt(token) * 4 / 3)) + + +# --------------------------------------------------------------------------- +# 文本换行(CJK 友好) +# --------------------------------------------------------------------------- + +# 每个 token 要么是一个 CJK 字,要么是一段连续的非空白西文,要么是空白 +_TOKEN_RE = re.compile( + r"[\u2e80-\u9fff\u3000-\u303f\uff01-\uff60\uffe0-\uffe6]" + r"|[^\s\u2e80-\u9fff\u3000-\u303f\uff01-\uff60\uffe0-\uffe6]+" + r"|\s+" +) + + +def wrap_lines(text: str, font: ImageFont.FreeTypeFont, max_w: int) -> list[str]: + """按像素宽度贪心折行。西文按词断,CJK 按字断,保留显式换行。""" + scratch = ImageDraw.Draw(Image.new("RGB", (1, 1))) + out: list[str] = [] + for para in text.split("\n"): + if not para: + out.append("") + continue + cur = "" + for tok in _TOKEN_RE.findall(para): + cand = cur + tok + if cur and scratch.textlength(cand, font=font) > max_w: + out.append(cur.rstrip()) + cur = tok if tok.strip() else "" # 行首不留空白 + else: + cur = cand + out.append(cur.rstrip()) + return out + + +# --------------------------------------------------------------------------- +# 圆角矩形 +# --------------------------------------------------------------------------- + +_photo_cache: dict[tuple, ImageTk.PhotoImage] = {} +_SUPERSAMPLE = 4 # 先放大再缩小 = 抗锯齿 + + +def _clear_cache() -> None: + """主题切换后调用,丢弃旧底色的缓存图。""" + _photo_cache.clear() + + +def rounded_photo( + w: int, + h: int, + radius: int, + fill: str, + bg: str, + outline: str | None = None, + outline_w: int = 1, +) -> ImageTk.PhotoImage: + """抗锯齿圆角矩形。``bg`` 是"挖空"的角所透出的底色(必须是父容器底色)。""" + w, h = max(1, int(w)), max(1, int(h)) + key = (w, h, radius, fill, bg, outline, outline_w) + cached = _photo_cache.get(key) + if cached is not None: + return cached + + s = _SUPERSAMPLE + img = Image.new("RGB", (w * s, h * s), bg) + d = ImageDraw.Draw(img) + d.rounded_rectangle( + [0, 0, w * s - 1, h * s - 1], + radius=min(radius * s, w * s // 2, h * s // 2), + fill=fill, + outline=outline, + width=outline_w * s if outline else 0, + ) + img = img.resize((w, h), Image.LANCZOS) + photo = ImageTk.PhotoImage(img) + _photo_cache[key] = photo # 持有引用,防止 GC 后图上白 + return photo + + +# --------------------------------------------------------------------------- +# 文本块 / 气泡 / 胶囊按钮 +# --------------------------------------------------------------------------- + + +def render_label_box( + text: str, + font_token: str, + text_fill: str, + bg: str, + box_fill: str | None = None, + radius: int = 0, + pad_x: int = 12, + pad_y: int = 8, + max_w: int | None = None, + outline: str | None = None, + align: str = "left", + line_gap: int = 5, + bold: bool = False, +) -> Image.Image: + """把文本渲染成一张图片。``max_w=None`` 表示不折行(按钮用)。""" + font = pil_font(font_px(font_token), bold=bold or T.TYPE[font_token][1] == "bold") + scratch = ImageDraw.Draw(Image.new("RGB", (1, 1))) + + if max_w is None: + lines = text.split("\n") + text_w = int(max(scratch.textlength(ln, font=font) for ln in lines)) + else: + lines = wrap_lines(text, font, max(40, max_w - 2 * pad_x)) + text_w = int(max(scratch.textlength(ln, font=font) for ln in lines)) + + asc, desc = font.getmetrics() + line_h = asc + desc + line_gap + w = text_w + 2 * pad_x + h = line_h * len(lines) + 2 * pad_y - line_gap + + if box_fill is None: + img = Image.new("RGB", (w, h), bg) + else: + # 直接在此图上画圆角(而不是复用 rounded_photo),避免缩放两次糊掉文字 + s = _SUPERSAMPLE + img = Image.new("RGB", (w * s, h * s), bg) + d = ImageDraw.Draw(img) + d.rounded_rectangle( + [0, 0, w * s - 1, h * s - 1], + radius=min(radius * s, w * s // 2, h * s // 2), + fill=box_fill, + outline=outline, + width=1 * s if outline else 0, + ) + img = img.resize((w, h), Image.LANCZOS) + + d = ImageDraw.Draw(img) + y = pad_y + for ln in lines: + if align == "center": + x = (w - scratch.textlength(ln, font=font)) / 2 + else: + x = pad_x + d.text((x, y), ln, font=font, fill=text_fill) + y += line_h + return img + + +def photo_of(img: Image.Image) -> ImageTk.PhotoImage: + """PIL 图 → tk 图片。调用方必须自己持有返回值,否则会被 GC 成白块。""" + return ImageTk.PhotoImage(img) + + +# --------------------------------------------------------------------------- +# 交互绑定 +# --------------------------------------------------------------------------- + + +def bind_hover( + widget, + on_enter: Callable[[], None] | None = None, + on_leave: Callable[[], None] | None = None, +) -> None: + """正确的 hover:指针移入子控件时 tk 会误发 ,这里下一帧复查真实位置。""" + state = {"inside": False} + + def _enter(_e=None): + if state["inside"]: + return + state["inside"] = True + if on_enter: + on_enter() + + def _leave(_e=None): + def check(): + try: + x, y = widget.winfo_pointerxy() + wx, wy = widget.winfo_rootx(), widget.winfo_rooty() + inside = ( + wx <= x < wx + widget.winfo_width() + and wy <= y < wy + widget.winfo_height() + ) + except Exception: + inside = False + if not inside and state["inside"]: + state["inside"] = False + if on_leave: + on_leave() + + widget.after_idle(check) + + widget.bind("", _enter, add="+") + widget.bind("", _leave, add="+") + + +def bind_click_recursive(widget, handler: Callable[[], None]) -> None: + """让 widget 及其所有后代都可点击(卡片里的 Label 也要能开 PDF)。""" + widget.bind("", lambda _e: handler(), add="+") + for child in widget.winfo_children(): + bind_click_recursive(child, handler) + + +def set_cursor_recursive(widget, cursor: str = "hand2") -> None: + try: + widget.configure(cursor=cursor) + except Exception: + pass + for child in widget.winfo_children(): + set_cursor_recursive(child, cursor) + + +def set_bg_recursive(widget, bg: str) -> None: + """把整棵子树的底色换成 ``bg`` —— 列表行做 hover 高亮用。""" + try: + widget.configure(bg=bg) + except Exception: + pass + for child in widget.winfo_children(): + set_bg_recursive(child, bg) + + +def make_focusable( + widget, + ring_bg: str, + on_activate: Callable[[], None] | None = None, + thickness: int | None = None, +) -> None: + """让控件能被 Tab 聚焦、用 Enter/Space 激活,并带可见焦点环。 + + 焦点环用的是 tk 自带的 highlight 描边:失焦时填 ``ring_bg``(即控件所在 + 容器的底色,视觉上不可见),因此**描边始终占位**,聚焦前后几何完全一致, + 不会引起布局跳动。聚焦时 tk 自动改用 ``highlightcolor``。 + + ``ring_bg`` 必须传控件父容器的底色,否则失焦时会看到一圈异色边框。 + """ + t = thickness if thickness is not None else max(1, round(2 * T.UI_SCALE)) + try: + widget.configure( + takefocus=1, + highlightthickness=t, + highlightbackground=ring_bg, + highlightcolor=T.c("focus"), + ) + except tk.TclError: + return + if on_activate is not None: + widget.bind("", lambda _e: on_activate(), add="+") + widget.bind("", lambda _e: on_activate(), add="+") + widget.bind("", lambda _e: on_activate(), add="+") + + +# --------------------------------------------------------------------------- +# 可选中 / 可复制的只读文本 +# --------------------------------------------------------------------------- + +# 允许穿透的按键:光标移动 / 选中 / 复制 / 全选。其余按键一律吞掉(只读)。 +_PASS_KEYS = frozenset( + { + "Left", + "Right", + "Up", + "Down", + "Home", + "End", + "Prior", + "Next", + "Shift_L", + "Shift_R", + "Control_L", + "Control_R", + "Tab", + "ISO_Left_Tab", + "Escape", + } +) +_CTRL = 0x0004 + + +def _readonly_guard(event) -> str | None: + """只读 Text 的按键闸门:放行导航与复制,拦下一切编辑键。""" + if event.keysym in _PASS_KEYS: + return None + # Ctrl+C / Ctrl+Insert 复制、Ctrl+A 全选放行;Ctrl+V/X 等编辑键继续拦 + if (event.state & _CTRL) and event.keysym.lower() in ("c", "insert", "a"): + return None + if event.keysym == "Insert" and not (event.state & _CTRL): + return "break" + return "break" + + +def selectable_text( + parent, + font: tuple, + bg: str, + fg: str, + pad_x: int = 0, + pad_y: int = 0, + line_gap: int = 2, +) -> tk.Text: + """建一个只读但可选、可复制的正文 Text。 + + tk.Label 无法选中文字,所以"输出内容要能复制"只能靠 Text 承载。这里不用 + ``state="disabled"``(禁用态在 Windows 上会吃掉一部分选择/复制交互),而是 + 保持 normal 并在按键层拦截编辑,行为更接近网页上的只读文本。 + """ + txt = tk.Text( + parent, + bg=bg, + fg=fg, + font=font, + relief="flat", + bd=0, + highlightthickness=0, + wrap="word", + height=1, + padx=pad_x, + pady=pad_y, + spacing1=line_gap, + spacing3=line_gap, + cursor="xterm", + insertwidth=0, + takefocus=1, + ) + txt.bind("", _readonly_guard) + return txt + + +def _base_line_px(txt: tk.Text) -> int: + """控件基准行高(像素):字体行距 + 控件级的上下留白。 + + tk 的 ``-height`` 就是按这个值折算的,不能只取字体行距 —— 否则算出来的行数 + 会偏多。 + """ + ls = tkfont.Font(font=txt.cget("font")).metrics("linespace") or 1 + sp1 = int(txt.cget("spacing1") or 0) + sp3 = int(txt.cget("spacing3") or 0) + return int(ls) + sp1 + sp3 + + +def _display_line_px(txt: tk.Text, index: str, base: int) -> int: + """推算某个显示行的高度:取该行首字符上各 tag 里最高的那套字体 + 上下留白。 + + tag 没写 spacing 时用控件级的值(和 tk 的解析规则一致)。 + """ + w1 = int(txt.cget("spacing1") or 0) + w3 = int(txt.cget("spacing3") or 0) + base_ls = base - w1 - w3 # 控件基准字体本身的行距 + h = base + try: + for tag in txt.tag_names(index): + spec = txt.tag_cget(tag, "font") + ls = tkfont.Font(font=spec).metrics("linespace") if spec else base_ls + sp1 = txt.tag_cget(tag, "spacing1") + sp3 = txt.tag_cget(tag, "spacing3") + sp1 = int(sp1) if str(sp1).strip() else w1 + sp3 = int(sp3) if str(sp3).strip() else w3 + h = max(h, int(ls) + sp1 + sp3) + except Exception: + pass + return h + + +def fit_height(txt: tk.Text, settle: bool = False) -> None: + """把只读 Text 的高度收到刚好放下内容,免得内部出现第二条滚动条。 + + ``settle=True`` 会先 ``update_idletasks()`` 逼出真实宽度;窗口刚建好或宽度 + 变过之后必须传 True,否则按旧宽度算出来的行数会偏少。流式刷新时宽度不变, + 可以走 False 这条便宜路径。 + + 注意 ``-height`` 的单位是"控件基准字体的行数",而代码段换了字体、还带上下 + 留白,一行比正文高 —— 只按显示行数设高,末尾几行会落到视口外被切掉(行数 + ≠ 像素高度)。所以这里按 **像素** 累加:已经排版出来的行直接量,还没排到的 + 行(在视口下方)按该行的 tag 推算,最后向上取整换算回行数 —— 宁可多一行 + 留白,也不切字。 + """ + try: + if settle: + txt.update_idletasks() + n = max(1, int(txt.count("1.0", "end", "displaylines")[0])) + base = _base_line_px(txt) + except Exception: + return + + # 视口外的行只能推算,推算值会比实际略高(末行没有换行符,矮一点),所以定稿 + # 时再量一遍:第二遍整块内容都已排版,能拿到精确值。流式刷新不做第二遍。 + for attempt in range(2 if settle else 1): + total = 0 + for i in range(n): + idx = f"1.0 + {i} displaylines" + info = txt.dlineinfo(idx) + total += info[3] if info is not None else _display_line_px(txt, idx, base) + + need = max(1, -(-total // base)) # 向上取整 + if int(txt.cget("height")) == need: + break + txt.configure(height=need) + if attempt == 0 and settle: + txt.update_idletasks() + + +def attach_text_menu(widget: tk.Text, root, editable: bool = False) -> None: + """给 Text 挂上右键菜单。``editable=False`` 时只给复制/全选。""" + menu = tk.Menu(root, tearoff=0, font=T.font("caption")) + + def add(label, accel, fn, state="normal"): + menu.add_command(label=label, accelerator=accel, command=fn, state=state) + + add("复制", "Ctrl+C", lambda: widget.event_generate("<>")) + if editable: + add("剪切", "Ctrl+X", lambda: widget.event_generate("<>")) + add("粘贴", "Ctrl+V", lambda: widget.event_generate("<>")) + menu.add_separator() + else: + menu.add_separator() + + def select_all(): + widget.tag_add("sel", "1.0", "end-1c") + widget.mark_set("insert", "1.0") + + add("全选", "Ctrl+A", select_all) + + def popup(event): + try: + widget.focus_set() + except Exception: + pass + # 没有选中内容时,"复制"没有意义 —— 置灰而不是点了没反应 + try: + has_sel = bool(widget.tag_ranges("sel")) + except Exception: + has_sel = False + menu.entryconfigure(0, state="normal" if has_sel else "disabled") + try: + menu.tk_popup(event.x_root, event.y_root) + finally: + menu.grab_release() + return "break" + + widget.bind("", popup, add="+") From 72c8fffef392dd449c6b7aa8e58642730c77a294 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 16:58:58 +0800 Subject: [PATCH 05/11] feat(ui): sidebar history, larger question bubble, tighter answer text - type scale: add bubble / answer / input / side tokens. The user's own question is now the largest thing on screen, while answers and the input box drop a step below body -- long-form text reads better smaller, and navigation should sit behind the content it navigates. - history panel -> in-window left sidebar (260px). Hover reveals "ask again" in a fixed-width slot so the question text never shifts; each row has a right-click menu; the scrollbar shows only when the list overflows. - history entries now keep their citation refs, so clicking one restores that whole exchange from disk -- no re-retrieval, no model call. - window 1140x800 (min width 880) to make room for the sidebar. --- pixelrag_client.py | 150 ++++++++++++--- pixelrag_history.py | 433 +++++++++++++++++++++++++------------------- pixelrag_theme.py | 8 +- 3 files changed, 380 insertions(+), 211 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index f826ea8..84e2890 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -36,6 +36,9 @@ HEALTH_URL = f"http://{HOST}:{PORT}/health" SEARCH_URL = f"http://{HOST}:{PORT}/search" ARTICLES_JSON = os.path.join(ROOT, "knowledge_index", "articles.json") +TILES_DIR = os.path.join( + ROOT, "knowledge_index", "tiles" +) # 与 _serve_cmd 里的 --tiles-dir 同一处 # ---- VLM 生成配置(DeepSeek-V4.1-Flash,原生视觉)---- VLM_MODEL = ( @@ -49,8 +52,9 @@ # (界面表现就是"模型没有返回任何内容")。实测 8192 能稳定作答。 VLM_MAX_TOKENS = 8192 -# 窗口逻辑尺寸(会按屏幕 DPI 缩放) -WIN_W, WIN_H = 900, 780 +# 窗口逻辑尺寸(会按屏幕 DPI 缩放)。比原来宽,是因为左边多了历史栏。 +WIN_W, WIN_H = 1140, 800 +WIN_MIN_W = 880 # 侧栏可收起,但收起后对话区也不该挤到读不了 # --------------------------------------------------------------------------- @@ -133,6 +137,31 @@ def _load_titles() -> dict[int, str]: return {} +def history_tiles(item: dict) -> list[tuple[str, dict]]: + """把历史里的引用页还原成 ``(图片 base64, hit)``。 + + 读的是本地 tiles 目录,所以打开旧问答既不联网也不用检索服务; + 文件被删掉时缩略图留空,卡片其余信息照常显示。 + """ + out: list[tuple[str, dict]] = [] + for ref in item.get("refs") or []: + if not isinstance(ref, dict): + continue + b64 = "" + try: + article = int(ref.get("article_id")) + tile = int(ref.get("tile_index")) + path = os.path.join( + TILES_DIR, f"{article}.png.tiles", f"tile_{tile:04d}.jpg" + ) + with open(path, "rb") as f: + b64 = base64.b64encode(f.read()).decode("ascii") + except Exception: + b64 = "" + out.append((b64, dict(ref))) + return out + + def screen_scale() -> float: try: import ctypes @@ -273,7 +302,7 @@ def __init__(self, root: tk.Tk): self.root = root root.title("PixelRAG 视觉问答") root.configure(bg=T.c("bg")) - root.minsize(int(660 * T.UI_SCALE), int(480 * T.UI_SCALE)) + root.minsize(int(WIN_MIN_W * T.UI_SCALE), int(480 * T.UI_SCALE)) # 消息数据模型:主题切换 / 窗口缩放时据此重放渲染 self.messages: list[dict] = [] @@ -294,9 +323,11 @@ def __init__(self, root: tk.Tk): self._gen = None self._stopped = False self._last_question: str | None = None - # 查询历史:落盘存储 + 面板 + 输入框上下键召回 + # 查询历史:落盘存储 + 左侧栏 + 输入框上下键召回 self.history = H.History() - self._history_panel: H.HistoryPanel | None = None + self.sidebar: H.HistorySidebar | None = None + self._sidebar_open = True # 顶栏「历史」按钮切换它 + self._current_hist: dict | None = None # 当前打开的那轮问答 self._hist_entry: dict | None = None # 本次提问对应的历史条目,答完回填 self._hist_pos: int | None = None # None = 不在召回态;0 = 最新一条 self._hist_draft = "" # 召回前用户已经敲了一半的内容 @@ -456,7 +487,7 @@ def _build_bottom_bar(self): relief="flat", bd=0, highlightthickness=0, - font=T.font("body_lg"), + font=T.font("input"), wrap="word", padx=0, pady=0, @@ -492,6 +523,21 @@ def _build_bottom_bar(self): def _build_body(self): body = tk.Frame(self.root, bg=T.c("bg")) body.pack(side="top", fill="both", expand=True) + self.body = body + + # 左侧历史栏(DeepSeek 式)。顶栏/底栏仍是通栏,只有中间分左右。 + self.sidebar = None + if self._sidebar_open: + self.sidebar = H.HistorySidebar( + body, + self.history, + on_open=self._open_history_item, + on_reask=self._reask, + on_new=self._new_chat, + ) + self.sidebar.pack(side="left", fill="y") + tk.Frame(body, bg=T.c("border"), width=1).pack(side="left", fill="y") + self.sidebar.set_current(self._current_hist) self.canvas = tk.Canvas(body, bg=T.c("bg"), highlightthickness=0, bd=0) self.scroll = tk.Scrollbar( @@ -542,6 +588,9 @@ def _schedule_reflow(self): ) def _on_wheel(self, event): + # 这个绑定是 bind_all 的,滚轮落在侧栏上时不该带着对话区一起滚 + if self.sidebar is not None and str(event.widget).startswith(str(self.sidebar)): + return try: self.canvas.yview_scroll(int(-event.delta / 120) * 3, "units") except Exception: @@ -654,7 +703,7 @@ def _render_user(self, msg: dict): row.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["xs"])) img = W.render_label_box( msg["text"], - "body", + "bubble", T.c("user_text"), T.c("bg"), box_fill=T.c("user_bubble"), @@ -764,7 +813,7 @@ def _render_answer(self, msg: dict): # 否则窗口一宽,长行就会横贯整个画布(和引用卡片对不齐)。 avail = max(1, self.canvas.winfo_width() - 2 * T.SPACE["xl"]) inset = max(0, (avail - self._content_w) // 2) - txt = W.selectable_text(container, T.font("body"), T.c("bg"), T.c("text")) + txt = W.selectable_text(container, T.font("answer"), T.c("bg"), T.c("text")) txt.pack(fill="x", padx=(inset, inset)) W.attach_text_menu(txt, self.root) txt.tag_configure( @@ -1156,6 +1205,10 @@ def _run_query(self, text: str): self._hist_entry = self.history.remember(text) self._hist_pos = None self._hist_draft = "" + # 新问一轮就离开"打开旧问答"的状态;这一步顺带把新条目刷进侧栏 + self._current_hist = None + if self.sidebar is not None: + self.sidebar.set_current(None) self._busy = True self._update_send_button() self._show_stop(True) @@ -1186,18 +1239,50 @@ def _select_all_input(self, _e=None): # ---------- 查询历史 ---------- def _on_history(self): - if self._history_panel is not None: - self._history_panel.win.lift() + """顶栏「历史」= 收起 / 展开左侧栏。""" + self._sidebar_open = not self._sidebar_open + self._rebuild_ui() + + def _refresh_sidebar(self): + if self.sidebar is not None: + self.sidebar.refresh() + + def _new_chat(self): + """开一段新对话:主区域回到欢迎语,历史一条不动。""" + if self._busy: # 生成中换视图会让答案落到错误的对话里 return - self._history_panel = H.HistoryPanel( - self.root, - self.history, - on_pick=self._reask, - on_close=self._forget_history_panel, - ) + self._current_hist = None + self.messages = [] + self._render_all((True, 0.0)) + self._add_welcome() + self._clear_input() + if self.sidebar is not None: + self.sidebar.set_current(None) + self._scroll_bottom(True) + + def _open_history_item(self, item: dict): + """打开一条历史:还原当时的提问 / 答案 / 引用卡片。 - def _forget_history_panel(self): - self._history_panel = None + 答案和引用页都存在历史里,所以这里不检索、不调模型;缩略图直接读本地 tiles。 + """ + if self._busy: + return + self._current_hist = item + if self.sidebar is not None: + self.sidebar.set_current(item) + self._clear_input() + self.messages = [] + self._render_all((False, 0.0)) + self._add_user(item.get("question", "")) + answer = item.get("answer") or "" + if answer: + self._append({"kind": "answer", "text": answer, "streaming": False}) + else: + self._add_note("这条提问没有存下答案,点它右边的「重问」可以重新生成。") + tiles = history_tiles(item) + if tiles: + self._append({"kind": "citations", "tiles": tiles}) + self._scroll_bottom(True) def _reask(self, question: str): """从历史里点一条:填进输入框并直接提问。""" @@ -1509,12 +1594,24 @@ def _finish_answer(self, images, stopped: bool = False, failed: bool = False): else: self._redraw_answer(msg) - # 回填历史:命中页数 + 答案(方便以后直接复制,不用重跑一遍) + # 回填历史:命中页数 + 答案 + 引用页。存下引用页是为了以后点开这条历史时 + # 能把引用卡片一起还原 —— 缩略图从本地 tiles 读,不用再问一次检索服务。 self.history.update( self._hist_entry, hits=len(images), answer=(msg or {}).get("text", ""), + refs=[ + { + "article_id": h.get("article_id"), + "tile_index": h.get("tile_index"), + "chunk_index": h.get("chunk_index"), + "score": h.get("score"), + "url": h.get("url"), + } + for _b64, h in images + ], ) + self._refresh_sidebar() if images: self._append({"kind": "citations", "tiles": list(images)}) @@ -1564,13 +1661,16 @@ def _toggle_theme(self): def _apply_theme(self): """换主题 = 用令牌重放全部界面(tkinter 不支持热改样式)。""" + self._rebuild_ui() + + def _rebuild_ui(self): + """按当前令牌重放整个界面。换主题、收起/展开侧栏都走这里。 + + tkinter 没有"只重画"这条路,一律销毁重建;输入到一半的内容和滚动位置 + 在销毁前取出来,重建后放回去。 + """ draft = self.input.get("1.0", "end-1c") state = self._view_state() # 必须在销毁旧界面之前取 - # 历史面板是独立窗口,颜色也是构建时取的 —— 关掉,重建完再打开 - reopen_history = self._history_panel is not None - if self._history_panel is not None: - self._history_panel.close() - self._history_panel = None for w in self.root.winfo_children(): w.destroy() @@ -1583,8 +1683,6 @@ def _apply_theme(self): self._render_all(view_state=state) self._set_status() self._update_send_button() - if reopen_history: - self._on_history() # ---------- 打开图片 / PDF ---------- diff --git a/pixelrag_history.py b/pixelrag_history.py index 5cb178f..f0449f8 100644 --- a/pixelrag_history.py +++ b/pixelrag_history.py @@ -1,7 +1,12 @@ -"""查询历史:落盘存储 + 查看面板 + 输入框上下键召回。 +"""查询历史:落盘存储 + 左侧历史栏 + 输入框上下键召回。 历史写在用户目录下的 ``~/.pixelrag/history.json``(不放仓库里,免得污染项目), -一条记录 = 时间 / 问题 / 命中页数 / 答案。存储只做增删查,不涉及任何检索逻辑。 +一条记录 = 时间 / 问题 / 命中页数 / 答案 / 引用页。存储只做增删查,不涉及任何检索逻辑。 + +答案与引用页都存下来,所以点开一条历史 = 直接还原当时那轮问答,不必重跑模型。 + +界面是主窗口左侧的一栏(DeepSeek 那种排版),不是弹窗 —— 弹窗会挡住内容, +而看历史本来就是为了对照着看。 """ from __future__ import annotations @@ -10,6 +15,7 @@ import os import time import tkinter as tk +import tkinter.font as tkfont from collections.abc import Callable from datetime import datetime, timezone @@ -19,6 +25,7 @@ HISTORY_PATH = os.path.join(os.path.expanduser("~"), ".pixelrag", "history.json") MAX_ITEMS = 200 # 只留最近这么多条 REUSE_WINDOW = 300 # 秒;同一个问题在这段时间内重复提交(含"重新生成")算同一条 +SIDEBAR_W = 260 # 侧栏逻辑宽度(会按屏幕 DPI 缩放) # --------------------------------------------------------------------------- @@ -45,7 +52,7 @@ def _load(self) -> list[dict]: return [d for d in data if isinstance(d, dict) and d.get("question")] def items(self) -> list[dict]: - """新的在前(面板按这个顺序显示)。""" + """新的在前(侧栏按这个顺序显示)。""" return list(reversed(self._items)) def questions(self) -> list[str]: @@ -71,6 +78,7 @@ def remember(self, question: str) -> dict: "ts": now, "hits": None, "answer": "", + "refs": [], } self._items.append(item) if len(self._items) > MAX_ITEMS: @@ -79,15 +87,21 @@ def remember(self, question: str) -> dict: return item def update( - self, item: dict | None, hits: int | None = None, answer: str | None = None + self, + item: dict | None, + hits: int | None = None, + answer: str | None = None, + refs: list[dict] | None = None, ): - """问答结束后回填命中页数与答案。""" + """问答结束后回填命中页数、答案与引用页。""" if not item: return if hits is not None: item["hits"] = int(hits) if answer is not None: item["answer"] = answer + if refs is not None: + item["refs"] = list(refs) self._save() def clear(self) -> None: @@ -115,7 +129,7 @@ def _iso(ts: float) -> str: def _pretty(iso: str) -> str: - """显示成本地时间。统一用 ``MM-DD HH:MM``:每行等宽,问题那列才对得齐。""" + """显示成本地时间。统一用 ``MM-DD HH:MM``:每行等宽,看起来才齐。""" try: dt = datetime.fromisoformat(iso).astimezone() except Exception: @@ -124,267 +138,328 @@ def _pretty(iso: str) -> str: # --------------------------------------------------------------------------- -# 面板 +# 左侧历史栏 # --------------------------------------------------------------------------- -class HistoryPanel: - """历史列表面板(独立窗口)。 +def _elide(text: str, font_spec: tuple, max_px: int) -> str: + """单行截断:太长就砍到放得下再加省略号(侧栏一行只放得下一个问题)。""" + f = tkfont.Font(font=font_spec) + text = " ".join(text.split()) # 换行/多余空白压成单空格,免得标签里折行 + if f.measure(text) <= max_px: + return text + lo, hi = 0, len(text) + while lo < hi: + mid = (lo + hi + 1) // 2 + if f.measure(text[:mid] + "…") <= max_px: + lo = mid + else: + hi = mid - 1 + return text[:lo] + "…" + + +class HistorySidebar(tk.Frame): + """主窗口左侧的历史栏:新对话 / 历史列表 / 清空。 - ``on_pick(question)``:点某条问题 / 点「重问」时回调,由调用方决定怎么重问。 + - 左键点一条 = ``on_open(item)``,由调用方还原那轮问答 + - 行尾「重问」= ``on_reask(question)``,用同一个问题重新检索生成 + - 右键 = 打开 / 重问 / 复制问题 / 复制答案 """ def __init__( self, - root: tk.Tk, + parent, history: History, - on_pick: Callable[[str], None], + on_open: Callable[[dict], None], + on_reask: Callable[[str], None] | None = None, + on_new: Callable[[], None] | None = None, on_clear: Callable[[], None] | None = None, - on_close: Callable[[], None] | None = None, ): - self.root = root + super().__init__(parent, bg=T.c("surface"), width=int(SIDEBAR_W * T.UI_SCALE)) + self.pack_propagate(False) # 固定宽度,别被里面的长文本撑开 self.history = history - self.on_pick = on_pick + self.on_open = on_open + self.on_reask = on_reask + self.on_new = on_new self.on_clear = on_clear - self.on_close = on_close - self._closed = False - - win = tk.Toplevel(root) - self.win = win - win.title("查询历史") - win.configure(bg=T.c("bg")) - win.transient(root) - w, h = int(620 * T.UI_SCALE), int(560 * T.UI_SCALE) - win.geometry(f"{w}x{h}+{root.winfo_rootx() + 80}+{root.winfo_rooty() + 60}") - win.minsize(int(420 * T.UI_SCALE), int(260 * T.UI_SCALE)) - win.bind("", lambda _e: self.close()) - win.bind("", self._on_wheel) - win.protocol("WM_DELETE_WINDOW", self.close) + self._current: dict | None = None + # 一行放问题的宽度:去掉左右留白和行尾「重问」的位置 + self._text_w = max( + 80, + int(SIDEBAR_W * T.UI_SCALE) - 3 * T.SPACE["md"] - int(34 * T.UI_SCALE), + ) self._build() - self._render_rows() - win.focus_set() + self.refresh() # ---------- 骨架 ---------- def _build(self): - head = tk.Frame(self.win, bg=T.c("surface")) - head.pack(side="top", fill="x") - inner = tk.Frame(head, bg=T.c("surface")) - inner.pack(fill="x", padx=T.SPACE["lg"], pady=T.SPACE["sm"]) + top = tk.Frame(self, bg=T.c("surface")) + top.pack(side="top", fill="x", padx=T.SPACE["sm"], pady=T.SPACE["sm"]) + self.new_btn = tk.Label( + top, + text="+ 新对话", + bg=T.c("surface"), + fg=T.c("accent"), + font=T.font("button"), + anchor="w", + cursor="hand2", + padx=T.SPACE["sm"], + pady=T.SPACE["sm"], + ) + self.new_btn.pack(fill="x") + self.new_btn.bind("", lambda _e: self._new()) + W.bind_hover( + self.new_btn, + on_enter=lambda: self.new_btn.configure(bg=T.c("accent_soft")), + on_leave=lambda: self.new_btn.configure(bg=T.c("surface")), + ) + W.make_focusable(self.new_btn, T.c("surface"), self._new) + tk.Frame(self, bg=T.c("border"), height=1).pack(side="top", fill="x") + head = tk.Frame(self, bg=T.c("surface")) + head.pack(side="top", fill="x", padx=T.SPACE["md"], pady=(T.SPACE["md"], 0)) tk.Label( - inner, text="查询历史", bg=T.c("surface"), fg=T.c("text"), font=T.font("h2") + head, + text="查询历史", + bg=T.c("surface"), + fg=T.c("text_faint"), + font=T.font("caption"), ).pack(side="left") self.count_lbl = tk.Label( - inner, + head, text="", bg=T.c("surface"), fg=T.c("text_faint"), - font=T.font("caption"), - ) - self.count_lbl.pack( - side="left", padx=(T.SPACE["sm"], 0), pady=(T.SPACE["xs"], 0) + font=T.font("micro"), ) + self.count_lbl.pack(side="right") + # 先 pack 底部,再让列表吃掉剩下的空间 —— 否则底部会被挤出去 + foot = tk.Frame(self, bg=T.c("surface")) + foot.pack(side="bottom", fill="x") + tk.Frame(foot, bg=T.c("border"), height=1).pack(side="top", fill="x") self.clear_btn = tk.Label( - inner, + foot, text="清空历史", bg=T.c("surface"), - fg=T.c("text_muted"), + fg=T.c("text_faint"), font=T.font("caption"), + anchor="w", cursor="hand2", padx=T.SPACE["sm"], - pady=T.SPACE["xs"], + pady=T.SPACE["sm"], ) - self.clear_btn.pack(side="right") + self.clear_btn.pack(fill="x", padx=T.SPACE["sm"], pady=T.SPACE["xs"]) self.clear_btn.bind("", lambda _e: self._clear()) W.bind_hover( self.clear_btn, on_enter=lambda: self.clear_btn.configure(fg=T.c("danger")), - on_leave=lambda: self.clear_btn.configure(fg=T.c("text_muted")), + on_leave=lambda: self.clear_btn.configure(fg=T.c("text_faint")), ) W.make_focusable(self.clear_btn, T.c("surface"), self._clear) - tk.Frame(self.win, bg=T.c("border"), height=1).pack(side="top", fill="x") - body = tk.Frame(self.win, bg=T.c("bg")) - body.pack(side="top", fill="both", expand=True) - self.canvas = tk.Canvas(body, bg=T.c("bg"), highlightthickness=0, bd=0) + body = tk.Frame(self, bg=T.c("surface")) + body.pack(side="top", fill="both", expand=True, pady=(T.SPACE["xs"], 0)) + self.canvas = tk.Canvas(body, bg=T.c("surface"), highlightthickness=0, bd=0) self.scroll = tk.Scrollbar( body, orient="vertical", command=self.canvas.yview, bg=T.c("scrollbar"), - troughcolor=T.c("bg"), + troughcolor=T.c("surface"), activebackground=T.c("border_strong"), borderwidth=0, + width=10, + relief="flat", + elementborderwidth=0, ) - self.scroll.pack(side="right", fill="y") self.canvas.pack(side="left", fill="both", expand=True) self.canvas.configure(yscrollcommand=self.scroll.set) - self.body = tk.Frame(self.canvas, bg=T.c("bg")) - self._win = self.canvas.create_window((0, 0), window=self.body, anchor="nw") - self.body.bind( - "", - lambda _e: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", lambda e: self.canvas.itemconfigure(self._win, width=e.width) - ) + self.list = tk.Frame(self.canvas, bg=T.c("surface")) + self._win = self.canvas.create_window((0, 0), window=self.list, anchor="nw") + self.list.bind("", self._on_list_configure) + self.canvas.bind("", self._on_canvas_configure) + for w in (self, self.canvas, self.list): + w.bind("", self._on_wheel, add="+") - # 面板变宽时问题重新折行(只绑一次,一次改所有行) - self._q_labels: list[tk.Label] = [] - self.win.bind("", self._on_resize) + def _on_list_configure(self, _event=None): + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + self._sync_scrollbar() - tip = tk.Label( - self.win, - text="点问题即可重新提问 · ↑ / ↓ 在输入框里快速召回", - bg=T.c("bg"), - fg=T.c("text_faint"), - font=T.font("micro"), - ) - tip.pack(side="bottom", pady=(T.SPACE["xs"], T.SPACE["sm"])) + def _on_canvas_configure(self, event): + self.canvas.itemconfigure(self._win, width=event.width) + self._sync_scrollbar() + + def _sync_scrollbar(self): + """列表放得下就收起滚动条 —— 空列表边上杵一根槽看着像坏了。 + + 重新 pack 时必须 ``before=self.canvas``:pack 顺序代表"谁先占位置", + 追加到末尾的话滚动条会被撑满的画布挤成 0 宽。 + """ + try: + need = self.list.winfo_reqheight() > self.canvas.winfo_height() + 1 + shown = bool(self.scroll.winfo_ismapped()) + except tk.TclError: + return + if need and not shown: + self.scroll.pack(side="right", fill="y", before=self.canvas) + elif not need and shown: + self.scroll.pack_forget() def _on_wheel(self, event): try: - self.canvas.yview_scroll(int(-event.delta / 120) * 3, "units") + self.canvas.yview_scroll(int(-event.delta / 120) * 2, "units") except Exception: pass - def _on_resize(self, event): - wrap = max(120, event.width - int(360 * T.UI_SCALE)) - for lbl in self._q_labels: - try: - lbl.configure(wraplength=wrap) - except tk.TclError: - # 列表刚重绘过,旧标签已经销毁;Configure 事件是排队来的,可能晚到 - pass - # ---------- 列表 ---------- - def _render_rows(self): - for w in self.body.winfo_children(): + def refresh(self): + """整表重绘。条数上限 200,重建的开销可以忽略。""" + for w in self.list.winfo_children(): w.destroy() - self._q_labels = [] # 上一批标签已经销毁,别再拿去改 wraplength + # 请求高度要等布局算完才准,所以放到空闲时再决定滚动条要不要露面 + self.canvas.after_idle(self._sync_scrollbar) items = self.history.items() - self.count_lbl.configure(text=f"{len(items)} 条") + self.count_lbl.configure(text=f"{len(items)} 条" if items else "") if not items: tk.Label( - self.body, - text="还没有查询记录。\n问过的问题会自动记在这里,关掉程序也不会丢。", - bg=T.c("bg"), + self.list, + text="还没有查询记录。\n问过的问题会记在这里,\n关掉程序也不会丢。", + bg=T.c("surface"), fg=T.c("text_faint"), - font=T.font("body"), + font=T.font("caption"), justify="left", - ).pack(anchor="w", padx=T.SPACE["xl"], pady=T.SPACE["xl"]) + anchor="w", + wraplength=int(SIDEBAR_W * T.UI_SCALE) - 2 * T.SPACE["md"], + ).pack(anchor="w", fill="x", padx=T.SPACE["md"], pady=T.SPACE["md"]) return - for i, it in enumerate(items): + for it in items: self._row(it) - if i != len(items) - 1: - tk.Frame(self.body, bg=T.c("border"), height=1).pack(fill="x") - def _row(self, item: dict): - base = T.c("bg") - row = tk.Frame(self.body, bg=base) - row.pack(fill="x") + def set_current(self, item: dict | None): + """高亮"当前打开的那一轮问答",并整表重绘(顺带并入新记的一条)。""" + self._current = item + self.refresh() - meta_bits = [] - if item.get("hits") is not None: - meta_bits.append(f"{item['hits']} 页命中") - else: - meta_bits.append("未完成") - if item.get("answer"): - meta_bits.append("有答案") - tk.Label( - row, - text=" · ".join(meta_bits), - bg=base, - fg=T.c("text_faint"), - font=T.font("micro"), - ).pack(side="right", padx=(T.SPACE["md"], T.SPACE["lg"])) + def _row(self, item: dict): + cur = item is self._current + base = T.c("accent_soft") if cur else T.c("surface") + row = tk.Frame(self.list, bg=base) + row.pack(fill="x", padx=T.SPACE["xs"], pady=1) + # 行尾「重问」:固定宽度占位,平时留空 —— 显隐时不会挤动左边的问题 ask = tk.Label( row, - text="重问", + text="重问" if (cur and self.on_reask) else "", + width=4, bg=base, fg=T.c("accent"), font=T.font("micro"), cursor="hand2", - padx=T.SPACE["xs"], + anchor="e", ) - ask.pack(side="right") - ask.bind("", lambda _e, q=item["question"]: self._pick(q)) - if item.get("answer"): - cp_a = tk.Label( - row, - text="复制答案", - bg=base, - fg=T.c("text_muted"), - font=T.font("micro"), - cursor="hand2", - padx=T.SPACE["xs"], - ) - cp_a.pack(side="right") - cp_a.bind("", lambda _e, t=item["answer"]: self._copy(t)) - else: - cp_a = None - cp_q = tk.Label( - row, - text="复制问题", - bg=base, - fg=T.c("text_muted"), - font=T.font("micro"), - cursor="hand2", - padx=T.SPACE["xs"], + ask.pack(side="right", padx=(0, T.SPACE["sm"])) + if self.on_reask: + ask.bind("", lambda _e, q=item["question"]: self._reask(q)) + + box = tk.Frame(row, bg=base) + box.pack( + side="left", + fill="x", + expand=True, + padx=(T.SPACE["sm"], 0), + pady=T.SPACE["xs"], ) - cp_q.pack(side="right") - cp_q.bind("", lambda _e, t=item["question"]: self._copy(t)) - - # 左边一块(时间 + 问题)整体可点 = 重问 - hit_area = tk.Frame(row, bg=base) - hit_area.pack(side="left", fill="x", expand=True, padx=(0, T.SPACE["md"])) tk.Label( - hit_area, - text=_pretty(item.get("time", "")), + box, + text=_elide(item["question"], T.font("side"), self._text_w), bg=base, - fg=T.c("text_muted"), - font=T.font("caption"), - ).pack(side="left", pady=T.SPACE["sm"]) - q_lbl = tk.Label( - hit_area, - text=item["question"], - bg=base, - fg=T.c("text"), - font=T.font("body"), + fg=T.c("accent") if cur else T.c("text"), + font=T.font("side"), + anchor="w", justify="left", + ).pack(anchor="w", fill="x") + n = item.get("hits") + meta = _pretty(item.get("time", "")) + meta += f" · {n} 页" if n is not None else " · 未完成" + if item.get("answer"): + meta += " · 有答案" + tk.Label( + box, + text=meta, + bg=base, + fg=T.c("text_faint"), + font=T.font("micro"), anchor="w", - wraplength=int(340 * T.UI_SCALE), - ) - q_lbl.pack(side="left", fill="x", expand=True, padx=(T.SPACE["md"], 0)) - self._q_labels.append(q_lbl) + justify="left", + ).pack(anchor="w", fill="x") def enter(_e=None): - W.set_bg_recursive(row, T.c("card")) - q_lbl.configure(fg=T.c("accent")) + if item is not self._current: + W.set_bg_recursive(row, T.c("card_hover")) + if self.on_reask: + ask.configure(text="重问") def leave(_e=None): - W.set_bg_recursive(row, base) - q_lbl.configure(fg=T.c("text")) + if item is not self._current: + W.set_bg_recursive(row, base) + if not (item is self._current): + ask.configure(text="") W.bind_hover(row, on_enter=enter, on_leave=leave) - W.bind_click_recursive(hit_area, lambda q=item["question"]: self._pick(q)) - W.set_cursor_recursive(hit_area) + W.bind_click_recursive(box, lambda i=item: self._open(i)) + W.set_cursor_recursive(box) + W.make_focusable(row, T.c("surface"), lambda i=item: self._open(i)) + + menu = tk.Menu(row, tearoff=0, font=T.font("caption")) + menu.add_command(label="打开这轮问答", command=lambda i=item: self._open(i)) + if self.on_reask: + menu.add_command( + label="用这个问题重问", + command=lambda q=item["question"]: self._reask(q), + ) + menu.add_separator() + menu.add_command( + label="复制问题", command=lambda t=item["question"]: self._copy(t) + ) + if item.get("answer"): + menu.add_command( + label="复制答案", command=lambda t=item["answer"]: self._copy(t) + ) + row.bind( + "", + lambda e, m=menu: self._popup(e, m), + add="+", + ) # ---------- 动作 ---------- - def _pick(self, question: str): - self.close() - self.on_pick(question) + def _popup(self, event, menu: tk.Menu): + try: + menu.tk_popup(event.x_root, event.y_root) + finally: + menu.grab_release() + return "break" + + def _open(self, item: dict): + self.set_current(item) + self.on_open(item) + + def _reask(self, question: str): + if self.on_reask: + self.on_reask(question) + + def _new(self): + if self.on_new: + self.on_new() def _copy(self, text: str): try: - self.root.clipboard_clear() - self.root.clipboard_append(text) + self.winfo_toplevel().clipboard_clear() + self.winfo_toplevel().clipboard_append(text) except tk.TclError: pass @@ -393,24 +468,16 @@ def _clear(self): try: ok = messagebox.askyesno( - "清空历史", "确定清空全部查询历史?此操作不可撤销。", parent=self.win + "清空历史", + "确定清空全部查询历史?此操作不可撤销。", + parent=self.winfo_toplevel(), ) except Exception: ok = True if not ok: return self.history.clear() - self._render_rows() + self._current = None + self.refresh() if self.on_clear: self.on_clear() - - def close(self): - if self._closed: - return - self._closed = True - try: - self.win.destroy() - except Exception: - pass - if self.on_close: - self.on_close() diff --git a/pixelrag_theme.py b/pixelrag_theme.py index e1fedf6..3580406 100644 --- a/pixelrag_theme.py +++ b/pixelrag_theme.py @@ -115,8 +115,12 @@ def c(key: str) -> str: TYPE: dict[str, tuple[int, str]] = { "title": (15, "bold"), # 顶栏标题 "h2": (12, "bold"), # 区块标题 / 引用来源 - "body": (11, "normal"), # 正文 - "body_lg": (12, "normal"), # 输入框 + "body": (11, "normal"), # 正文(欢迎语 / 提示 / 错误) + "body_lg": (12, "normal"), # 大号正文 + "bubble": (13, "normal"), # 用户提问气泡:比正文大一号,一眼能认出"我问的" + "answer": (10, "normal"), # 答案正文:读长文,比正文收一号 + "input": (10, "normal"), # 输入框 + "side": (10, "normal"), # 侧栏条目:导航文字要退到正文后面,别抢阅读焦点 "label": (11, "bold"), # 卡片标题 "button": (11, "bold"), # 按钮 "caption": (9, "normal"), # 卡片元信息 From c86ef5225c1ffc96c86b4f74cf8edb82ef7c4ba4 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 17:22:28 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat(ui):=20DeepSeek=20=E8=89=B2=E6=9D=BF?= =?UTF-8?q?/=E5=AD=97=E5=8F=B7/=E8=A1=8C=E9=AB=98/=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=9B=9B=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐 chat.deepseek.com 的视觉规格,分四步落地: 1. 色板换成 DeepSeek 一套(冷灰侧栏 #F9F9F9 + 白底 + #4D6BFE),深色板同步; 主色拆成 accent(填充/边框)与 accent_text(白底上的文字)—— #4D6BFE 落白底 只有 4.33:1,不够正文级,文字另取 #3D5AE0(5.6:1)。 2. 字号改 px 原生:TYPE 直接存 DeepSeek 的像素值,pt() 负责 px→pt,肉眼可比。 同时修掉 Pillow 侧字号小 31% 的 DPI bug —— font_px() 原先按 96dpi 写死 4/3, 本机 tk scaling 实为 1.93,导致用户气泡与所有按钮的字一直偏小。现在改问 tk 实测值。 3. 行高令牌 LINE_HEIGHT=1.6。tk 的 spacing2 会被对半分给折点两侧,所以只有 spacing1=ceil(gap/2) / spacing2=gap / spacing3=floor(gap/2) 这一组能让段首行、 折行、硬换行的行盒完全相等(实测均为 37px)。 4. 消息列宽 768 且居中(原先只在答案里做了内缩,其余块通栏左对齐),消息间距 收敛到单一来源 SPACE["msg"]=24,引用卡片组内保持 sm。 顺带修 font_mono 的语义:上一轮它已从"点"改成"设计像素",代码块还写着 font_mono(9),实际只剩 8.7 物理像素,改为 14。 --- pixelrag_client.py | 67 +++++++++------ pixelrag_theme.py | 193 +++++++++++++++++++++++++++----------------- pixelrag_widgets.py | 84 +++++++++++++++++-- 3 files changed, 241 insertions(+), 103 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index 84e2890..7eeb5f2 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -334,6 +334,7 @@ def __init__(self, root: tk.Tk): self._serve_ready = check_health() self._titles = _load_titles() self._content_w = T.CONTENT_MAX_WIDTH + self._inset = T.SPACE["xl"] # 消息列到画布边的留白(宽屏时变大,见 _content_metrics) self._canvas_w = 0 self._apply_window_geometry() @@ -565,15 +566,26 @@ def _build_body(self): def _on_frame_configure(self, _e=None): self.canvas.configure(scrollregion=self.canvas.bbox("all")) + def _content_metrics(self, canvas_w: int) -> tuple[int, int]: + """消息列的 ``(列宽, 左右留白)``。 + + DeepSeek 的正文是一条居中的窄栏:列宽封顶 ``CONTENT_MAX_WIDTH``,窗口再宽也 + 不变宽,多出来的宽度两边均分。窗口窄的时候留白退回 ``SPACE["xl"]``(此时 + 列宽本身就等于可用宽度,没有富余可分)。 + """ + avail = max(1, canvas_w - 2 * T.SPACE["xl"]) + w = max(280, min(T.CONTENT_MAX_WIDTH, avail)) + return w, max(T.SPACE["xl"], (canvas_w - w) // 2) + def _on_canvas_configure(self, event): self.canvas.itemconfigure(self._win, width=event.width) - new_w = max(280, min(T.CONTENT_MAX_WIDTH, event.width - 2 * T.SPACE["xl"])) - # 画布宽变了也要重排(即使 _content_w 已到上限):正文的左右内缩量是按 - # 画布宽算的,不重排的话首次拿到真实宽度后行宽会一直偏宽。 + new_w, new_inset = self._content_metrics(event.width) + # 画布宽变了也要重排(即使 _content_w 已到上限):两侧留白是按画布宽算的, + # 不重排的话窗口一宽正文就一直贴在左边,和居中的引用卡片对不齐。 moved = abs(event.width - self._canvas_w) > 8 self._canvas_w = event.width - if abs(new_w - self._content_w) > 8 or moved: - self._content_w = new_w + if abs(new_w - self._content_w) > 8 or abs(new_inset - self._inset) > 8 or moved: + self._content_w, self._inset = new_w, new_inset self._schedule_reflow() def _schedule_reflow(self): @@ -652,6 +664,11 @@ def _render_all(self, view_state: tuple[bool, float] | None = None): self._photos.clear() self._answer_text = None + # 每条消息只带"下边距 = SPACE['msg']"(消息间距只有这一个来源),所以最上面 + # 那条前面得补一份上留白,否则会被画布顶边贴住。 + if self.messages: + tk.Frame(self.frame, bg=T.c("bg"), height=T.SPACE["msg"]).pack(fill="x") + for msg in self.messages: self._render_message(msg) if was_loading: @@ -687,7 +704,7 @@ def _render_message(self, msg: dict): def _render_welcome(self, msg: dict): wrap = tk.Frame(self.frame, bg=T.c("bg")) - wrap.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["2xl"], T.SPACE["lg"])) + wrap.pack(fill="x", padx=self._inset, pady=(T.SPACE["2xl"], T.SPACE["lg"])) tk.Label( wrap, text=msg["text"], @@ -700,7 +717,7 @@ def _render_welcome(self, msg: dict): def _render_user(self, msg: dict): row = tk.Frame(self.frame, bg=T.c("bg")) - row.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["xs"])) + row.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) img = W.render_label_box( msg["text"], "bubble", @@ -728,11 +745,11 @@ def _render_note(self, msg: dict): justify="left", anchor="w", wraplength=self._content_w, - ).pack(fill="x", padx=T.SPACE["xl"], pady=T.SPACE["xs"]) + ).pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) def _render_error(self, msg: dict): row = tk.Frame(self.frame, bg=T.c("bg")) - row.pack(fill="x", padx=T.SPACE["xl"], pady=T.SPACE["sm"]) + row.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) img = W.render_label_box( msg["text"], "body", @@ -785,7 +802,9 @@ def paint(hover: bool): def _render_section(self, text: str): wrap = tk.Frame(self.frame, bg=T.c("bg")) - wrap.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["sm"])) + # 区块标题属于它上面那条消息的一部分,所以贴着上面走(上边距 0),只留 + # 下边距 —— 它的"和上一条消息的距离"由上面那条的 msg 边距提供。 + wrap.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["sm"])) tk.Label( wrap, text=text, @@ -803,22 +822,18 @@ def _render_section(self, text: str): def _render_answer(self, msg: dict): container = tk.Frame(self.frame, bg=T.c("bg")) - container.pack( - fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["xs"], T.SPACE["sm"]) - ) + container.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) msg["_widget"] = container # 正文用只读 Text 而不是 Label —— Label 不能选中,复制不出来。 - # Text 没有 wraplength,只能靠左右内缩把行宽压回 _content_w, + # Text 没有 wraplength,行宽靠容器的 padx=self._inset 压回 _content_w, # 否则窗口一宽,长行就会横贯整个画布(和引用卡片对不齐)。 - avail = max(1, self.canvas.winfo_width() - 2 * T.SPACE["xl"]) - inset = max(0, (avail - self._content_w) // 2) txt = W.selectable_text(container, T.font("answer"), T.c("bg"), T.c("text")) - txt.pack(fill="x", padx=(inset, inset)) + txt.pack(fill="x") W.attach_text_menu(txt, self.root) txt.tag_configure( "code", - font=T.font_mono(9), + font=T.font_mono(14), background=T.c("code_bg"), lmargin1=T.SPACE["md"], lmargin2=T.SPACE["md"], @@ -884,10 +899,12 @@ def copy(_e=None): def _render_citations(self, msg: dict): self._render_section("引用来源") - for i, (b64, hit) in enumerate(msg["tiles"], 1): - self._render_citation_card(b64, hit, i) + tiles = msg["tiles"] + # 卡片是一组:卡与卡之间用 sm(挨着才像一组),整组结束后才轮到消息间距。 + for i, (b64, hit) in enumerate(tiles, 1): + self._render_citation_card(b64, hit, i, last=(i == len(tiles))) - def _render_citation_card(self, b64: str, hit: dict, idx: int): + def _render_citation_card(self, b64: str, hit: dict, idx: int, last: bool = True): """白底描边卡片:缩略图 + 标题 + 页码/相似度。点缩略图看大图,点其余开 PDF。""" pad = T.SPACE["md"] w = max(240, self._content_w) @@ -902,7 +919,11 @@ def _render_citation_card(self, b64: str, hit: dict, idx: int): bd=0, cursor="hand2", ) - card.pack(fill="x", padx=T.SPACE["xl"], pady=(0, T.SPACE["sm"])) + card.pack( + fill="x", + padx=self._inset, + pady=(0, T.SPACE["msg"] if last else T.SPACE["sm"]), + ) title = self._titles.get(hit.get("article_id")) or title_of(hit.get("url", "")) x = pad @@ -1063,7 +1084,7 @@ def _start_loading(self, text: str = "正在检索相关页面"): self._loading_base = text self._loading_frame = tk.Frame(self.frame, bg=T.c("bg")) self._loading_frame.pack( - fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["lg"], T.SPACE["sm"]) + fill="x", padx=self._inset, pady=(0, T.SPACE["msg"]) ) self._loading_label = tk.Label( self._loading_frame, diff --git a/pixelrag_theme.py b/pixelrag_theme.py index 3580406..2bb3af5 100644 --- a/pixelrag_theme.py +++ b/pixelrag_theme.py @@ -1,4 +1,4 @@ -"""PixelRAG 桌面客户端设计令牌 —— 方向 A「Notion 纸感」。 +"""PixelRAG 桌面客户端设计令牌 —— 对齐 DeepSeek 聊天界面。 这是全项目唯一的"样式表"。所有颜色 / 字号 / 间距 / 圆角 / 动效时长都必须 从这里取,业务代码里不允许再出现硬编码色值或魔法数字。 @@ -18,58 +18,65 @@ # ---- 浅色:Notion 纸感(默认)---- "light": { "bg": "#FFFFFF", # 页面底色 - "surface": "#F7F7F5", # 侧栏 / 顶栏 / 底栏 + "surface": "#F9F9F9", # 侧栏 / 顶栏 / 底栏(DeepSeek 冷灰) "card": "#FFFFFF", # 卡片表面 - "card_hover": "#FAFAF9", # 卡片 hover - "border": "#E9E9E7", # 常规分隔线 - "border_strong": "#D3D1CB", # hover / focus 时的边框 - "text": "#37352F", # 正文(14.9:1 on bg) - "text_muted": "#6B6A67", # 次要信息(5.4:1 on bg) - "text_faint": "#767571", # 极淡提示(4.6:1 on bg) - "accent": "#1A73CC", # 主色(4.8:1 on bg;白字落其上同为 4.8:1) - "accent_hover": "#155FB0", - "accent_soft": "#E7F1FB", # 主色淡底(hover 态底,非正文底) + "card_hover": "#F5F6F8", # 卡片 hover + "border": "#E5E7EB", # 常规分隔线 + "border_strong": "#D1D5DB", # hover / focus 时的边框 + "text": "#1F2329", # 正文(15.8:1 on bg) + "text_muted": "#4B5563", # 次要信息(7.6:1 on bg) + "text_faint": "#6B7280", # 极淡提示(4.8:1 on bg;再淡就跌破 4.5) + # 主色拆两个键:填充/边框用原值,白底上的**文字**用 accent_text。 + # #4D6BFE 落白底只有 4.33:1,不够正文级的 4.5:1,所以文字另取 #3D5AE0(5.6:1)。 + "accent": "#4D6BFE", # DeepSeek 蓝:填充 / 边框 / 焦点环 + "accent_text": "#3D5AE0", # 白底上的强调色文字(5.6:1 on bg) + "accent_hover": "#3D5AE0", # 实心按钮 hover(白字落其上 5.6:1) + "accent_soft": "#F0F3FF", # 主色淡底(hover 态底,非正文底) + # 实心按钮 = accent 底 + 白字 = 4.33:1,是全表唯一低于 4.5 的组合; + # 这是 DeepSeek 自己的做法,按用户指定的原值保留。 "on_accent": "#FFFFFF", # 主色之上的文字 - "user_bubble": "#F1F1EF", # 用户消息淡灰块 - "user_text": "#37352F", # 用户消息文字 - "code_bg": "#F7F6F3", # 行内代码 / 代码块底 + "user_bubble": "#E8EEFF", # 用户消息浅蓝块(DeepSeek 风格) + "user_text": "#1F2329", # 用户消息文字(13.6:1 on user_bubble) + "code_bg": "#F6F7F9", # 行内代码 / 代码块底 "success": "#0F7B6C", # 5.2:1 on bg "warning": "#8A6100", # 5.5:1 on bg "danger": "#C0392B", # 5.0:1 on danger_bg - "danger_bg": "#FDF2F2", # 错误提示底 + "danger_bg": "#FEF2F2", # 错误提示底 "input_bg": "#FFFFFF", - "input_line": "#E9E9E7", # 输入框底部 1px 线(未聚焦) - "focus": "#1A73CC", # 键盘焦点环 - "scrollbar": "#D3D1CB", - "skeleton": "#F1F1EF", # 加载骨架 + "input_line": "#E5E7EB", # 输入框 1px 边框(未聚焦) + "focus": "#4D6BFE", # 键盘焦点环 + "scrollbar": "#D1D5DB", + "skeleton": "#F1F2F4", # 加载骨架 }, - # ---- 深色:等价反转 ---- + # ---- 深色:照 DeepSeek 深色模式推的等价反转 ---- "dark": { - "bg": "#191919", - "surface": "#202020", - "card": "#252525", - "card_hover": "#2C2C2C", - "border": "#333331", - "border_strong": "#4A4A47", - "text": "#E6E6E4", - "text_muted": "#A8A7A3", - "text_faint": "#8F8E8B", - "accent": "#5B9BE8", - "accent_hover": "#7CB0F0", - "accent_soft": "#1E2A3A", - "on_accent": "#0F1419", # 深色底上主色偏亮,配深墨字才够对比(6.4:1) - "user_bubble": "#2F2F2E", - "user_text": "#E6E6E4", - "code_bg": "#202020", + "bg": "#1A1A1A", + "surface": "#1E1E1E", + "card": "#242424", + "card_hover": "#2A2A2A", + "border": "#333333", + "border_strong": "#454545", + "text": "#E8E8E8", # 14.2:1 on bg + "text_muted": "#B4B4BB", + "text_faint": "#A1A1AA", # 6.8:1 on bg + # 深色底上主色要提亮才够对比;同样拆成"填充用 / 文字用"两个键。 + "accent": "#7C93FF", # 6.2:1 on bg + "accent_text": "#8FA4FF", # 7.4:1 on bg + "accent_hover": "#8FA4FF", + "accent_soft": "#232B45", + "on_accent": "#1A1A1A", # 深色底上主色偏亮,配深墨字才够对比(6.2:1) + "user_bubble": "#253052", # 深色下的用户气泡:深蓝而非深灰 + "user_text": "#E8E8E8", # 10.2:1 on user_bubble + "code_bg": "#242424", "success": "#4ADE80", "warning": "#FACC15", "danger": "#F87171", "danger_bg": "#3A1D1D", - "input_bg": "#202020", - "input_line": "#333331", - "focus": "#5B9BE8", - "scrollbar": "#4A4A47", - "skeleton": "#2F2F2E", + "input_bg": "#1E1E1E", + "input_line": "#333333", + "focus": "#7C93FF", + "scrollbar": "#4A4A4A", + "skeleton": "#2A2A2A", }, } @@ -110,54 +117,99 @@ def c(key: str) -> str: FONT_FAMILY = "Microsoft YaHei UI" # 中文优先;缺失时 tk 会自行回退 FONT_MONO = "Consolas" -# token -> (字号 pt, 字重)。方向 A 的层级:20 / 16 / 15 / 14 / 12 / 11 px, -# 换算成 tk 的点值并整体略放大以保证中文可读性。 +# token -> (字号 px, 字重)。**这里存的是 CSS 语义的像素值**,直接照 DeepSeek 的规格写, +# 肉眼可比。tk 只认点值,由 pt() 换算;Pillow 那边再从 pt() 换回像素,两边严格同源。 +# +# DeepSeek 的层级:正文统一 16(提问与答案同号,只靠蓝色气泡区分), +# 标题 20 / 16 / 14 / 12,界面文字最小 12。 TYPE: dict[str, tuple[int, str]] = { - "title": (15, "bold"), # 顶栏标题 - "h2": (12, "bold"), # 区块标题 / 引用来源 - "body": (11, "normal"), # 正文(欢迎语 / 提示 / 错误) - "body_lg": (12, "normal"), # 大号正文 - "bubble": (13, "normal"), # 用户提问气泡:比正文大一号,一眼能认出"我问的" - "answer": (10, "normal"), # 答案正文:读长文,比正文收一号 - "input": (10, "normal"), # 输入框 - "side": (10, "normal"), # 侧栏条目:导航文字要退到正文后面,别抢阅读焦点 - "label": (11, "bold"), # 卡片标题 - "button": (11, "bold"), # 按钮 - "caption": (9, "normal"), # 卡片元信息 - "micro": (8, "normal"), # 角标 + "title": (20, "bold"), # 顶栏品牌 + "h2": (16, "bold"), # 区块标题 / 引用来源 + "body": (16, "normal"), # 正文(欢迎语 / 提示 / 错误) + "body_lg": (16, "normal"), # 大号正文(保留键;DeepSeek 正文统一 16,暂与 body 同) + "bubble": (16, "normal"), # 用户提问气泡 + "answer": (16, "normal"), # 答案正文 + "input": (16, "normal"), # 输入框 + "side": (14, "normal"), # 侧栏条目 + "label": (14, "bold"), # 卡片标题 + "button": (14, "bold"), # 按钮 + "caption": (12, "normal"), # 卡片元信息 + "micro": (12, "normal"), # 角标 } -# 全局字号倍率:**嫌字小就调大这一个数**(1.15 ≈ 正文 11→12.6pt、标题 15→17pt)。 +# 全局字号倍率:**嫌字小就调大这一个数**。1.0 = 上面那套 DeepSeek 原值。 # tk 绘制与 Pillow 离屏绘制都从这里换算,保证两边的字一样大。 -FONT_SCALE = 1.15 +FONT_SCALE = 1.0 +# 96dpi 下 1px = 0.75pt(CSS 的 px 与物理点的换算基准)。 +PX_PER_PT = 0.75 -def pt(token: str) -> int: - """取某个 token 的实际字号(点值,已乘 FONT_SCALE)。 - tk 的字体元组只接受整数点值,所以在这里就取整;Pillow 那边也从这里换算, - 保证界面上两种绘制方式的字一样大。 - """ - return max(1, round(TYPE[token][0] * FONT_SCALE)) +def pt(token: str) -> int: + """取某个 token 的点值(tk 字体元组只接受整数,所以在这里取整)。""" + return max(1, round(TYPE[token][0] * PX_PER_PT * FONT_SCALE)) def font(token: str) -> tuple: - """取 tk 字体元组,如 ('Microsoft YaHei UI', 12.6, 'normal')。""" + """取 tk 字体元组,如 ('Microsoft YaHei UI', 12, 'normal')。""" return (FONT_FAMILY, pt(token), TYPE[token][1]) -def font_mono(size: int = 10) -> tuple: - """等宽字体(代码块)。``size`` 是设计字号,同样会乘 FONT_SCALE。""" - return (FONT_MONO, max(1, round(size * FONT_SCALE)), "normal") +def font_mono(size: int = 14) -> tuple: + """等宽字体(代码块)。``size`` 是设计像素值。""" + return (FONT_MONO, max(1, round(size * PX_PER_PT * FONT_SCALE)), "normal") + + +# 行高倍数:DeepSeek 正文 16px / 行高 1.6。tk 与 Pillow 两边都从这里取,所以屏幕上 +# 的正文和离屏画出来的气泡行距完全一致(改行距只动这一个数)。 +LINE_HEIGHT = 1.6 + + +def px_size(token: str) -> int: + """该 token 的字号,设计像素(含 FONT_SCALE)。""" + return max(1, round(TYPE[token][0] * FONT_SCALE)) + + +def line_box_px(token: str) -> int: + """该 token 一行该占多高(物理像素)= 字号 × 行高 × 屏幕缩放。 + + 注意这里含 ``UI_SCALE``:字号走 tk 的 pt→px 换算后实际就是放大了 UI_SCALE 倍, + 行盒必须用同一把尺子,否则高分屏上行距会显得比字紧。 + """ + return max(1, round(px_size(token) * LINE_HEIGHT * UI_SCALE)) + + +# tk 把点值换算成像素的系数。**必须实测,不能按 96dpi 猜成 4/3** —— 本机是 150% 缩放 +# (tk scaling ≈ 1.93),猜 4/3 会让 Pillow 画出来的字比 tk 那边小 31%,也就是用户气泡 +# 和所有按钮的字一直偏小。这里问一次 tk 再缓存,调用方无需关心。 +_TK_SCALING: float | None = None + + +def tk_scaling() -> float: + """tk 的点值→像素系数(需要已存在 Tk 根窗口;没有就回退到 96dpi)。""" + global _TK_SCALING + if _TK_SCALING is None: + try: + import tkinter + + root = tkinter._default_root + _TK_SCALING = ( + float(root.winfo_fpixels("1i")) / 72 if root else 1 / PX_PER_PT + ) + except Exception: + _TK_SCALING = 1 / PX_PER_PT # 拿不到就退回 96dpi,至少不比以前差 + return _TK_SCALING # --------------------------------------------------------------------------- # 间距(4px 基准) / 圆角 / 动效 # --------------------------------------------------------------------------- -_BASE_SPACE = {"xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "2xl": 32} +_BASE_SPACE = {"xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "2xl": 32, "msg": 24} _BASE_RADIUS = {"sm": 6, "md": 8, "lg": 12} -_BASE_CONTENT_MAX_WIDTH = 720 +# 消息列的宽度上限(DeepSeek 是 768)。窗口再宽也不跟着变宽 —— 富余的宽度均分成 +# 两侧留白,正文始终是一条居中的窄栏,长行才不会横贯整屏。 +_BASE_CONTENT_MAX_WIDTH = 768 # 下面三个是"逻辑值 × UI_SCALE"的结果;启动时由 set_ui_scale 按屏幕 DPI 重算, # 以便在高分屏上字变大的同时,内边距/圆角/行宽等比跟上,不会显得局促。 @@ -186,6 +238,3 @@ def set_ui_scale(factor: float) -> float: "stream_throttle": 60, # 流式重绘节流间隔 "resize_debounce": 120, # 窗口尺寸变化防抖 } - -# 正文最大宽度(px):超过则两侧留白,保证长文阅读行宽舒适 -CONTENT_MAX_WIDTH = 720 diff --git a/pixelrag_widgets.py b/pixelrag_widgets.py index 4b3b824..43e3390 100644 --- a/pixelrag_widgets.py +++ b/pixelrag_widgets.py @@ -66,8 +66,41 @@ def pil_font(size_px: int, bold: bool = False) -> ImageFont.FreeTypeFont: def font_px(token: str) -> int: - """把主题里的点值字号换算成像素(96dpi:px = pt * 4/3),已含 FONT_SCALE。""" - return max(10, round(T.pt(token) * 4 / 3)) + """把主题里的点值字号换算成像素,已含 FONT_SCALE。 + + 换算系数必须用 tk 自己的(实测 DPI),不能按 96dpi 写死 4/3 —— 在 150% 缩放的屏上 + 那会让 Pillow 画的字比同一 token 的 tk 文字小 31%(用户气泡/按钮长期偏小就是这原因)。 + """ + return max(1, round(T.pt(token) * T.tk_scaling())) + + +def line_gap_px(token: str, bold: bool = False) -> int: + """Pillow 侧的行距补偿:目标行盒(``T.line_box_px``)减去字体自然行高。 + + Pillow 的 ``getmetrics()`` 给出的是字体自身的 asc+desc(tk 的 ``linespace`` 同理), + 单倍行高约等于字号的 1.29 倍;要凑到 1.6 倍,补的就是这个差值。行高是"目标 - 实测", + 所以换字体、改 FONT_SCALE、换 DPI 都不用回来改这里的数字。 + """ + font = pil_font(font_px(token), bold=bold or T.TYPE[token][1] == "bold") + asc, desc = font.getmetrics() + return max(0, T.line_box_px(token) - (asc + desc)) + + +def tk_line_gap_px(font: tuple) -> int: + """tk 侧的同一个补偿量(送给 Text 的 ``spacing2``)。 + + tk 的字体元组只给点值,物理字号 = 点值 × ``tk_scaling()`` —— 拿它算目标行盒, + 再用 ``linespace`` 量字体自然行高,两边相减即可。这样即使传进来的是等宽字体 + (代码块)也算得对,不需要知道 token 名。 + """ + try: + f = tkfont.Font(font=font) + natural = int(f.metrics("linespace") or 0) + size_px = abs(int(f.actual("size"))) * T.tk_scaling() + except Exception: + return 0 + target = round(size_px * T.LINE_HEIGHT) + return max(0, target - natural) # --------------------------------------------------------------------------- @@ -164,11 +197,18 @@ def render_label_box( max_w: int | None = None, outline: str | None = None, align: str = "left", - line_gap: int = 5, + line_gap: int | None = None, bold: bool = False, ) -> Image.Image: - """把文本渲染成一张图片。``max_w=None`` 表示不折行(按钮用)。""" - font = pil_font(font_px(font_token), bold=bold or T.TYPE[font_token][1] == "bold") + """把文本渲染成一张图片。``max_w=None`` 表示不折行(按钮用)。 + + ``line_gap=None`` 时按主题行高(``T.LINE_HEIGHT``)自动算,多行文本才有区别 —— + 单行时上下各减一次,最终高度与 line_gap 无关,所以按钮不会因此变胖。 + """ + bold = bold or T.TYPE[font_token][1] == "bold" + if line_gap is None: + line_gap = line_gap_px(font_token, bold=bold) + font = pil_font(font_px(font_token), bold=bold) scratch = ImageDraw.Draw(Image.new("RGB", (1, 1))) if max_w is None: @@ -360,14 +400,41 @@ def selectable_text( fg: str, pad_x: int = 0, pad_y: int = 0, - line_gap: int = 2, + line_gap: int | None = None, ) -> tk.Text: """建一个只读但可选、可复制的正文 Text。 tk.Label 无法选中文字,所以"输出内容要能复制"只能靠 Text 承载。这里不用 ``state="disabled"``(禁用态在 Windows 上会吃掉一部分选择/复制交互),而是 保持 normal 并在按键层拦截编辑,行为更接近网页上的只读文本。 + + 行距按主题行高(``T.LINE_HEIGHT``)铺开,``line_gap=None`` 时自动算。 + + tk 的三个 spacing 语义各不相同,而 ``spacing2``(段内折行)会被 tk **上下对半 + 分配**给折点两侧 —— 实测的每行行盒是: + + ===================== ========================================== + 行的位置 实际行盒 + ===================== ========================================== + 单行段落 linespace + spacing1 + spacing3 + 多行段落的段首行 linespace + spacing1 + floor(spacing2/2) + 多行段落的中间/段末行 linespace + ceil(spacing2/2) + … + 硬换行(``\\n``)后那行 linespace + spacing3 + spacing1 + ===================== ========================================== + + 要把这些**全部**拉到同一个行盒 ``linespace + gap``,只有一组解: + ``spacing1 = ceil(gap/2)``、``spacing2 = gap``、``spacing3 = floor(gap/2)``。 + 随手写成 ``spacing1=0, spacing2=spacing3=gap`` 会让段首行矮 4px、段末行矮 3px, + 整段看着忽紧忽松 —— ``linecheck.py`` 就是钉这个的(断言每行行盒完全相等)。 + + 顺带:单行段落的行盒 = linespace + spacing1 + spacing3 正好也是 ``fit_height`` + 折算 ``-height`` 用的基准,所以行距一改控件高度自动跟上。段与段之间的空档由 + 正文里的空行承担(空行本身也是一个"单行段落")。 """ + if line_gap is None: + line_gap = tk_line_gap_px(font) + half_hi = -(-line_gap // 2) # ceil:折点上半 + half_lo = line_gap // 2 # floor:折点下半 txt = tk.Text( parent, bg=bg, @@ -380,8 +447,9 @@ def selectable_text( height=1, padx=pad_x, pady=pad_y, - spacing1=line_gap, - spacing3=line_gap, + spacing1=half_hi, + spacing2=line_gap, + spacing3=half_lo, cursor="xterm", insertwidth=0, takefocus=1, From b7c11cb91c92be09945c48f2b08bc29f8054ec63 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 17:40:49 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat(ui):=20DeepSeek=20=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86/=E6=B0=94=E6=B3=A1/=E4=BE=A7=E6=A0=8F=E5=9B=9B?= =?UTF-8?q?=E6=AD=A5=E6=94=B6=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5. 用户气泡浅蓝 #E8EEFF、圆角 8、内边距 10·14,右对齐;助手答案左对齐、无气泡, 正文色 16px/1.6 —— 提问与答案同号,只靠气泡区分(严格照 DeepSeek 的层级)。 6. 引用卡片 / 代码块 / 骨架条改走令牌:卡片标题在白底上用 accent_text(小字不加粗 时 4.33:1 的 accent 不够),代码块走 font_mono(px_size("code")) 与同一套行距。 7. 输入框从「裸 Text + 一条下划线」改成圆角方框:Canvas 铺圆角底图 + create_window 放 Text,聚焦时描边换 accent。发送 / 停止按钮改成浮在框内右下角的胶囊(半径 RADIUS_PILL,绘制时自动夹到高度的一半),发送按钮用 min_w 钉住宽度、位置预留 _btn_reserve 在建模时一次算死,所以按钮在「发送 / 生成中 / 停止」之间切换时, 输入框里的文字不会跟着伸缩。底栏也不再通栏:按侧栏开合与滚动条宽度让位, 和消息列严格对齐(实测左右各差 0 / 1px)。 8. 侧栏条目换成 DeepSeek 的浅灰圆角块(hover=nav_hover、当前=nav_active,不再是 蓝底蓝字),「+ 新对话」同样是整条胶囊,底色始终是正文色。圆角靠「铺满整行的 Canvas + 沉到最底层 + 贴一张圆角图」实现,挖空色传列表底色,所以四角透出的是 底色而不是方角 —— 这一点用截图像素逐角断言过,不是"看着像"。 验证:9 套自检全绿(侧栏 41 项、侧栏像素 45 项、对比度 40 项、字号 13 项、行高、 布局、输入框、交互链路、截图断言),深浅两套主题各出一组截图肉眼复核,真实启动 一次通过。ruff check / format 干净。 --- pixelrag_client.py | 277 +++++++++++++++++++++++++++++++++++--------- pixelrag_history.py | 127 +++++++++++++++----- pixelrag_theme.py | 29 ++++- pixelrag_widgets.py | 60 +++++----- 4 files changed, 379 insertions(+), 114 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index 7eeb5f2..69d2dee 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -334,7 +334,9 @@ def __init__(self, root: tk.Tk): self._serve_ready = check_health() self._titles = _load_titles() self._content_w = T.CONTENT_MAX_WIDTH - self._inset = T.SPACE["xl"] # 消息列到画布边的留白(宽屏时变大,见 _content_metrics) + self._inset = T.SPACE[ + "xl" + ] # 消息列到画布边的留白(宽屏时变大,见 _content_metrics) self._canvas_w = 0 self._apply_window_geometry() @@ -402,7 +404,7 @@ def _build_topbar(self): W.bind_hover( self.theme_btn, on_enter=lambda: self.theme_btn.configure( - fg=T.c("accent"), bg=T.c("accent_soft") + fg=T.c("accent_text"), bg=T.c("accent_soft") ), on_leave=lambda: self.theme_btn.configure( fg=T.c("text_muted"), bg=T.c("surface") @@ -427,7 +429,7 @@ def _build_topbar(self): W.bind_hover( self.history_btn, on_enter=lambda: self.history_btn.configure( - fg=T.c("accent"), bg=T.c("accent_soft") + fg=T.c("accent_text"), bg=T.c("accent_soft") ), on_leave=lambda: self.history_btn.configure( fg=T.c("text_muted"), bg=T.c("surface") @@ -455,32 +457,72 @@ def _build_bottom_bar(self): self.bottom.pack(side="bottom", fill="x") tk.Frame(self.bottom, bg=T.c("border"), height=1).pack(side="top", fill="x") - row = tk.Frame(self.bottom, bg=T.c("bg")) - row.pack(fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["md"], T.SPACE["sm"])) + # 输入框正对着正文那条窄栏:同宽、同留白,不是通栏(DeepSeek 就是这样)。 + # 留白在 _on_canvas_configure 里跟着聊天画布的宽度同步 —— 侧栏一开一合, + # 输入框和消息列会一起挪,不会各走各的。 + self.bottom_row = tk.Frame(self.bottom, bg=T.c("bg")) + self.bottom_row.pack( + fill="x", padx=T.SPACE["xl"], pady=(T.SPACE["md"], T.SPACE["sm"]) + ) - # 发送按钮(圆角图片按钮,支持 hover / disabled 两态) - self.send_btn = tk.Label(row, bg=T.c("bg"), bd=0, cursor="hand2") - self.send_btn.pack(side="right", padx=(T.SPACE["md"], 0), anchor="s") + # 输入框:圆角方框 + 1px 描边,聚焦时描边变主色(DeepSeek 样式)。 + # tk 没有 CSS 圆角,所以复用引用卡片那套画法 —— Canvas 铺一张圆角底图并 + # tag_lower 沉底,再用 create_window 放进 Text 和两个按钮。Text 自己的底色 + # 和填充同色,于是方角被藏进圆角里,只有描边露出来。 + self.input_box = tk.Canvas( + self.bottom_row, bg=T.c("bg"), highlightthickness=0, bd=0, height=1 + ) + self.input_box.pack(fill="x", expand=True) + self._input_bg_id = None + self._input_win_id = None + self._input_photo = None + self._input_focused = False + self._input_pad = (T.SPACE["lg"], T.SPACE["md"]) # 文字到描边的距离 + + # 发送 / 停止按钮是画布里的窗口项,浮在方框右下角(DeepSeek 的按钮在框内)。 + # 底色取 input_bg 而不是 bg:它们压在圆角填充上,方角才不会露出来。 + # bd/padx/pady 全清零:Label 的默认边框和内边距会给按钮图套一圈看不见的 + # 外框,让底图看起来离描边比设定的间距更远。 + self.send_btn = tk.Label( + self.input_box, + bg=T.c("input_bg"), + bd=0, + padx=0, + pady=0, + highlightthickness=0, + cursor="hand2", + ) self.send_btn.bind("", lambda _e: self._on_send()) W.bind_hover( self.send_btn, on_enter=self._send_hover_on, on_leave=self._send_hover_off ) - W.make_focusable(self.send_btn, T.c("bg"), self._on_send) + W.make_focusable(self.send_btn, T.c("input_bg"), self._on_send) - # 停止按钮:只在生成期间出现,平时不 pack(不占位) - self.stop_btn = tk.Label(row, bg=T.c("bg"), bd=0, cursor="hand2") + # 停止按钮:只在生成期间显示(位置一直预留着,文字区不会跟着跳) + self.stop_btn = tk.Label( + self.input_box, + bg=T.c("input_bg"), + bd=0, + padx=0, + pady=0, + highlightthickness=0, + cursor="hand2", + ) self.stop_btn.bind("", lambda _e: self._on_stop()) W.bind_hover( self.stop_btn, on_enter=self._stop_hover_on, on_leave=self._stop_hover_off ) - W.make_focusable(self.stop_btn, T.c("bg"), self._on_stop) + W.make_focusable(self.stop_btn, T.c("input_bg"), self._on_stop) self._stop_shown = False + self._stop_item = None + self._send_item = None + self._btn_reserve = 0 # 文字右侧留给按钮的宽度(建完按钮才知道) + self._send_img_w = self._stop_img_w = 0 + self._bottom_inset = (-1, -1) # 底栏当前 (左, 右) 留白,用来跳过无变化的设置 - # 输入区:底线式,聚焦时底线变强调色 - box = tk.Frame(row, bg=T.c("bg")) - box.pack(side="left", fill="x", expand=True) + sp1, sp2, sp3 = W.line_spacing(T.font("input")) self.input = tk.Text( - box, + self.input_box, height=2, bg=T.c("input_bg"), fg=T.c("text"), @@ -492,10 +534,10 @@ def _build_bottom_bar(self): wrap="word", padx=0, pady=0, - spacing1=2, - spacing3=2, + spacing1=sp1, + spacing2=sp2, + spacing3=sp3, ) - self.input.pack(fill="x") self.input.bind("", self._on_enter) self.input.bind("", lambda _e: self._clear_input()) self.input.bind("", self._select_all_input) @@ -503,14 +545,25 @@ def _build_bottom_bar(self): self.input.bind("", self._on_input_up) self.input.bind("", self._on_input_down) W.attach_text_menu(self.input, self.root, editable=True) - self.input.bind( - "", lambda _e: self.input_line.configure(bg=T.c("accent")) + self.input.bind("", lambda _e: self._layout_input_box(True)) + self.input.bind("", lambda _e: self._layout_input_box(False)) + + # 先把两张按钮图渲染出来:一是量出预留宽度,二是保证第一次要显示停止按钮时 + # 手里已经有图(_update_stop_button 在未显示时是直接返回的)。 + # 预留宽度按"停止按钮也在"的最宽情况算一次就定住 —— 之后停止按钮显隐、发送 + # 按钮在「发送 / 生成中」之间切换,输入框里的文字都不会跟着伸缩。 + self._render_send_image() + self._render_stop_image() + # 发送按钮的宽度被 min_w 钉住(「发送」和「生成中」一样宽),所以按当前这张量 + # 就等于按最宽情况量。 + self._btn_reserve = ( + T.SPACE["sm"] + self._send_img_w + T.SPACE["sm"] + self._stop_img_w ) - self.input.bind( - "", lambda _e: self.input_line.configure(bg=T.c("input_line")) - ) - self.input_line = tk.Frame(box, bg=T.c("input_line"), height=1) - self.input_line.pack(fill="x", pady=(T.SPACE["sm"], 0)) + self._update_send_button() + self.stop_btn.configure(image=self._stop_photo) + self.stop_btn.image = self._stop_photo # 先装上,量宽度也让它有尺寸 + self.input_box.bind("", lambda _e: self._layout_input_box()) + self.root.after_idle(self._layout_input_box) hint = tk.Label( self.bottom, @@ -521,6 +574,98 @@ def _build_bottom_bar(self): ) hint.pack(side="top", pady=(0, T.SPACE["sm"])) + def _layout_input_box(self, focused: bool | None = None): + """重画输入框的圆角底,并把 ``Text`` 摆到内边距里面。 + + ``focused=None`` 表示"焦点态没变,只是尺寸变了"(```` 用)。底图走 + ``W.rounded_photo``,两张(聚焦/未聚焦)按 key 缓存,来回切焦点不会重画。 + """ + box = self.input_box + if not box.winfo_exists(): + return + if focused is not None: + self._input_focused = focused + pad_x, pad_y = self._input_pad + # -height 是"几行",tk 已经把它折算成含 spacing1/2/3 的完整行盒;拿不到就按 + # 令牌兜底(理论上不会走到)。输入框不随内容长高,超长内容由 Text 自己滚动。 + text_h = int(self.input.winfo_reqheight() or 0) or 2 * T.line_box_px("input") + w = max(1, box.winfo_width()) + h = text_h + 2 * pad_y + if int(box.cget("height")) != h: + box.configure(height=h) + photo = W.rounded_photo( + w, + h, + T.RADIUS["lg"], + T.c("input_bg"), + T.c("bg"), + outline=T.c("focus") if self._input_focused else T.c("input_line"), + ) + self._input_photo = photo # 必须持有,否则会被 GC 成白块 + if self._input_bg_id is None: + self._input_bg_id = box.create_image(0, 0, anchor="nw", image=photo) + box.tag_lower(self._input_bg_id) + else: + box.itemconfigure(self._input_bg_id, image=photo) + box.coords(self._input_bg_id, 0, 0) + + # 文字区右端让开按钮区(预留宽度在 _build_bottom_bar 里按最宽情况定死) + edge = T.SPACE["sm"] + win_w = max(1, w - pad_x - self._btn_reserve) + if self._input_win_id is None: + self._input_win_id = box.create_window( + pad_x, pad_y, anchor="nw", window=self.input, width=win_w, height=text_h + ) + else: + box.coords(self._input_win_id, pad_x, pad_y) + box.itemconfigure(self._input_win_id, width=win_w, height=text_h) + + # 按钮贴在方框右下角:停止在左、发送在右,离描边各留一个小间距。 + # 焦点环(highlight)也算在控件外框里,锚点要把它让出来,底图才真的离描边 edge。 + ring = int(self.send_btn.cget("highlightthickness") or 0) + btn_y = h - edge + ring + send_w = self._send_img_w + if self._send_item is None: + self._send_item = box.create_window( + w - edge + ring, btn_y, anchor="se", window=self.send_btn + ) + else: + box.coords(self._send_item, w - edge + ring, btn_y) + stop_x = w - edge + ring - send_w - T.SPACE["sm"] + if self._stop_item is None: + self._stop_item = box.create_window( + stop_x, + btn_y, + anchor="se", + window=self.stop_btn, + state="normal" if self._stop_shown else "hidden", + ) + else: + box.coords(self._stop_item, stop_x, btn_y) + box.tag_raise(self._stop_item) + box.tag_raise(self._send_item) + + def _layout_bottom_row(self, avail_w: int): + """让输入框与消息列左右对齐(同宽同留白)。 + + ``avail_w`` 是聊天画布的宽度 —— 输入框要对齐的是正文那条窄栏,不是整个窗口, + 所以侧栏一开一合输入框也要跟着挪。 + """ + _, inset = self._content_metrics(avail_w) + # 底栏是通栏的(侧栏一开一合它不动),所以要让开聊天区左边那截侧栏、右边那截 + # 滚动条,才能和消息列真正对齐 —— 只补 inset 会在侧栏打开时整体左移一整个侧栏。 + left = right = inset + sb = getattr(self, "sidebar", None) + if sb is not None and sb.winfo_exists(): + left += sb.winfo_width() + 1 # 侧栏 + 它右边那条 1px 分隔线 + sc = getattr(self, "scroll", None) + if sc is not None and sc.winfo_exists(): + right += sc.winfo_width() + if (left, right) == self._bottom_inset: + return + self._bottom_inset = (left, right) + self.bottom_row.pack_configure(padx=(left, right)) + def _build_body(self): body = tk.Frame(self.root, bg=T.c("bg")) body.pack(side="top", fill="both", expand=True) @@ -549,7 +694,7 @@ def _build_body(self): troughcolor=T.c("bg"), activebackground=T.c("border_strong"), borderwidth=0, - width=10, + width=T.SCROLLBAR_W, relief="flat", elementborderwidth=0, ) @@ -579,12 +724,17 @@ def _content_metrics(self, canvas_w: int) -> tuple[int, int]: def _on_canvas_configure(self, event): self.canvas.itemconfigure(self._win, width=event.width) + self._layout_bottom_row(event.width) # 输入框跟着消息列一起居中对齐 new_w, new_inset = self._content_metrics(event.width) # 画布宽变了也要重排(即使 _content_w 已到上限):两侧留白是按画布宽算的, # 不重排的话窗口一宽正文就一直贴在左边,和居中的引用卡片对不齐。 moved = abs(event.width - self._canvas_w) > 8 self._canvas_w = event.width - if abs(new_w - self._content_w) > 8 or abs(new_inset - self._inset) > 8 or moved: + if ( + abs(new_w - self._content_w) > 8 + or abs(new_inset - self._inset) > 8 + or moved + ): self._content_w, self._inset = new_w, new_inset self._schedule_reflow() @@ -718,6 +868,7 @@ def _render_welcome(self, msg: dict): def _render_user(self, msg: dict): row = tk.Frame(self.frame, bg=T.c("bg")) row.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) + pad_x, pad_y = T.BUBBLE_PAD img = W.render_label_box( msg["text"], "bubble", @@ -725,8 +876,8 @@ def _render_user(self, msg: dict): T.c("bg"), box_fill=T.c("user_bubble"), radius=T.RADIUS["md"], - pad_x=T.SPACE["md"], - pad_y=T.SPACE["sm"], + pad_x=pad_x, + pad_y=pad_y, max_w=int(self._content_w * 0.78), ) photo = W.photo_of(img) @@ -778,11 +929,11 @@ def paint(hover: bool): img = W.render_label_box( "重新生成", "button", - T.c("accent") if hover else T.c("text"), + T.c("accent_text") if hover else T.c("text"), T.c("bg"), box_fill=T.c("card_hover") if hover else T.c("card"), outline=T.c("accent") if hover else T.c("border_strong"), - radius=T.RADIUS["sm"], + radius=T.RADIUS_PILL, pad_x=T.SPACE["md"], pad_y=T.SPACE["sm"], align="center", @@ -831,15 +982,21 @@ def _render_answer(self, msg: dict): txt = W.selectable_text(container, T.font("answer"), T.c("bg"), T.c("text")) txt.pack(fill="x") W.attach_text_menu(txt, self.root) + # 代码块同样按 LINE_HEIGHT 铺行距,但要按**等宽字体自己的**行盒算:两种字体 + # 的自然行高差得不少,照抄正文那个像素值会让代码行挤在一起。tk 的折行间距 + # 会被对半分给折点两侧,所以 spacing1/3 取 gap 的上/下半(与 selectable_text 同理)。 + code_font = T.font_mono(T.px_size("code")) + sp1, sp2, sp3 = W.line_spacing(code_font) txt.tag_configure( "code", - font=T.font_mono(14), + font=code_font, background=T.c("code_bg"), lmargin1=T.SPACE["md"], lmargin2=T.SPACE["md"], rmargin=T.SPACE["md"], - spacing1=T.SPACE["sm"], - spacing3=T.SPACE["sm"], + spacing1=sp1, + spacing2=sp2, + spacing3=sp3, ) self._answer_text = txt @@ -892,7 +1049,7 @@ def copy(_e=None): btn.bind("", copy) W.bind_hover( btn, - on_enter=lambda: btn.configure(fg=T.c("accent")), + on_enter=lambda: btn.configure(fg=T.c("accent_text")), on_leave=lambda: btn.configure(fg=T.c("text_faint")), ) W.make_focusable(btn, T.c("bg"), copy) @@ -952,7 +1109,8 @@ def _render_citation_card(self, b64: str, hit: dict, idx: int, last: bool = True info, text=f"[{idx}] {title}", bg=T.c("card"), - fg=T.c("accent"), + fg=T.c("accent_text"), # 白底上的强调色**文字**用 accent_text(4.33:1 的 + # accent 是给填充/描边用的,小字不加粗时对比度不够) font=T.font("label"), justify="left", anchor="w", @@ -1083,9 +1241,7 @@ def _start_loading(self, text: str = "正在检索相关页面"): self._loading_active = True self._loading_base = text self._loading_frame = tk.Frame(self.frame, bg=T.c("bg")) - self._loading_frame.pack( - fill="x", padx=self._inset, pady=(0, T.SPACE["msg"]) - ) + self._loading_frame.pack(fill="x", padx=self._inset, pady=(0, T.SPACE["msg"])) self._loading_label = tk.Label( self._loading_frame, text=text, @@ -1364,46 +1520,49 @@ def _stop_hover_off(self): self._update_stop_button() def _show_stop(self, show: bool): - """打包顺序:send 先占最右,stop 紧跟其后排到它左边,box 再吃掉剩余宽度。""" + """停止按钮的显隐 = 画布窗口项的显示/隐藏(位置一直给它留着)。""" if show == self._stop_shown: return self._stop_shown = show if show: self._update_stop_button() - self.stop_btn.pack( - side="right", - after=self.send_btn, - padx=(0, T.SPACE["sm"]), - anchor="s", + if self._stop_item is not None: + self.input_box.itemconfigure( + self._stop_item, state="normal" if show else "hidden" ) - else: - self.stop_btn.pack_forget() - def _update_stop_button(self): - if not self._stop_shown: - return + def _render_stop_image(self): + """画「停止生成」的按钮图(不管此刻显不显示 —— 建界面时要先量宽度)。""" fill = T.c("border_strong") if self._stop_hover else T.c("card") img = W.render_label_box( "停止生成", "button", T.c("text"), - T.c("bg"), + T.c("input_bg"), box_fill=fill, outline=T.c("border_strong"), - radius=T.RADIUS["sm"], + radius=T.RADIUS_PILL, pad_x=T.SPACE["lg"], pad_y=T.SPACE["sm"], align="center", ) photo = W.photo_of(img) self._stop_photo = photo + self._stop_img_w = img.width + return photo + + def _update_stop_button(self): + photo = self._render_stop_image() + if not self._stop_shown: + return try: self.stop_btn.configure(image=photo, cursor="hand2") self.stop_btn.image = photo except Exception: pass - def _update_send_button(self): + def _render_send_image(self): + """画「发送 / 生成中」的按钮图(按当前 busy / hover 态)。""" if self._busy: fill, fg, label, cursor = ( T.c("border"), @@ -1424,15 +1583,21 @@ def _update_send_button(self): label, "button", fg, - T.c("bg"), + T.c("input_bg"), box_fill=fill, - radius=T.RADIUS["sm"], + radius=T.RADIUS_PILL, pad_x=T.SPACE["lg"], pad_y=T.SPACE["sm"], align="center", + min_w=T.SEND_MIN_W, ) photo = W.photo_of(img) self._send_photo = photo + self._send_img_w = img.width + return photo, cursor + + def _update_send_button(self): + photo, cursor = self._render_send_image() try: self.send_btn.configure(image=photo, cursor=cursor) self.send_btn.image = photo diff --git a/pixelrag_history.py b/pixelrag_history.py index f0449f8..fa027c6 100644 --- a/pixelrag_history.py +++ b/pixelrag_history.py @@ -158,6 +158,48 @@ def _elide(text: str, font_spec: tuple, max_px: int) -> str: return text[:lo] + "…" +def _rounded_bg(host: tk.Widget, bg: str, radius: int): + """在 ``host`` 里铺一张"跟着宿主尺寸变"的圆角底图,返回换填充色的函数。 + + tk 没有 CSS 圆角,只能画一张图贴上去。要点有三个: + + - Canvas 用 ``place`` 铺满宿主、**不参与尺寸计算**,所以宿主的宽高仍然由里面的 + 内容(文字)决定 —— 行高不用自己算; + - Canvas 必须是最后创建、再 ``lower()``:同一父窗口里后创建的子窗口在上层, + 不沉底就会把文字盖住(Label 的文字是父窗口自己画的,子窗口一定盖得住); + - 同尺寸同色的图由 ``W.rounded_photo`` 缓存,200 行也只会画几张图。 + + ``bg`` 是圆角外面"挖空"那部分透出的颜色 —— 必须是宿主的底色,不是宿主的底色 + 就会在圆角处露出方块。 + """ + cv = tk.Canvas(host, bg=bg, highlightthickness=0, bd=0) + cv.place(x=0, y=0, relwidth=1, relheight=1) + item = cv.create_image(0, 0, anchor="nw") + state: dict = {"fill": None, "photo": None} + + def paint(_e=None): + w, h = cv.winfo_width(), cv.winfo_height() + if w <= 1 or h <= 1 or state["fill"] is None: + return + photo = W.rounded_photo(w, h, radius, state["fill"], bg) + if photo is not state["photo"]: + state["photo"] = photo # 必须持有引用,否则会被 GC 成白块 + cv.itemconfigure(item, image=photo) + + cv.bind("", paint) + host.after_idle(paint) + + def set_fill(fill: str): + state["fill"] = fill + paint() + + # 注意不能写 cv.lower():Canvas 把 lower 覆写成了 tag_lower(画布里的图层操作)。 + # 要沉的是**窗口**,得回到 Misc 的那个 lower。 + tk.Misc.lower(cv) + # 也返回画布:它铺满宿主,子控件够不到的边缘(圆角那两头)只有它收得到鼠标事件 + return cv, set_fill + + class HistorySidebar(tk.Frame): """主窗口左侧的历史栏:新对话 / 历史列表 / 清空。 @@ -183,10 +225,17 @@ def __init__( self.on_new = on_new self.on_clear = on_clear self._current: dict | None = None - # 一行放问题的宽度:去掉左右留白和行尾「重问」的位置 + # 行内条目离侧栏边缘的留白:圆角块要缩进来一点才看得出是"块" + self._pad = T.SPACE["sm"] + # 一行放问题的宽度:去掉两端留白、文本块的左留白、行尾固定的「重问」槽, + # 再减掉滚动条 —— 滚动条一露面画布就窄这么多,不减的话省略号会被裁掉。 self._text_w = max( 80, - int(SIDEBAR_W * T.UI_SCALE) - 3 * T.SPACE["md"] - int(34 * T.UI_SCALE), + int(SIDEBAR_W * T.UI_SCALE) + - 2 * self._pad + - 2 * T.SPACE["sm"] + - int(34 * T.UI_SCALE) + - T.SCROLLBAR_W, ) self._build() self.refresh() @@ -196,23 +245,35 @@ def __init__( def _build(self): top = tk.Frame(self, bg=T.c("surface")) top.pack(side="top", fill="x", padx=T.SPACE["sm"], pady=T.SPACE["sm"]) + # 「+ 新对话」是整条圆角胶囊(DeepSeek 侧栏顶部就是这样),不是描边按钮。 + # 圆角底图必须**后建再沉底**:Label 的文字是它自己画的,子窗口一定盖得住, + # 底图先建就会被 Label 反过来糊住。Label 再向内缩一点,它的方角才不会啃到圆角。 self.new_btn = tk.Label( top, text="+ 新对话", bg=T.c("surface"), - fg=T.c("accent"), + fg=T.c("text"), font=T.font("button"), anchor="w", cursor="hand2", padx=T.SPACE["sm"], pady=T.SPACE["sm"], ) - self.new_btn.pack(fill="x") + self.new_btn.pack(fill="x", padx=T.SPACE["xs"], pady=T.SPACE["xs"]) + new_cv, set_new_fill = _rounded_bg(top, T.c("surface"), T.RADIUS["md"]) + + def new_hover(on: bool): + fill = T.c("nav_hover") if on else T.c("surface") + set_new_fill(fill) + self.new_btn.configure(bg=fill) + self.new_btn.bind("", lambda _e: self._new()) + new_cv.configure(cursor="hand2") + new_cv.bind("", lambda _e: self._new()) W.bind_hover( self.new_btn, - on_enter=lambda: self.new_btn.configure(bg=T.c("accent_soft")), - on_leave=lambda: self.new_btn.configure(bg=T.c("surface")), + on_enter=lambda: new_hover(True), + on_leave=lambda: new_hover(False), ) W.make_focusable(self.new_btn, T.c("surface"), self._new) tk.Frame(self, bg=T.c("border"), height=1).pack(side="top", fill="x") @@ -270,7 +331,7 @@ def _build(self): troughcolor=T.c("surface"), activebackground=T.c("border_strong"), borderwidth=0, - width=10, + width=T.SCROLLBAR_W, relief="flat", elementborderwidth=0, ) @@ -346,26 +407,27 @@ def set_current(self, item: dict | None): def _row(self, item: dict): cur = item is self._current - base = T.c("accent_soft") if cur else T.c("surface") - row = tk.Frame(self.list, bg=base) - row.pack(fill="x", padx=T.SPACE["xs"], pady=1) + # DeepSeek 的当前条目是一个中性灰圆角块(不是蓝底),文字仍是正文色 + fill = T.c("nav_active") if cur else T.c("surface") + row = tk.Frame(self.list, bg=T.c("surface")) + row.pack(fill="x", padx=self._pad, pady=1) # 行尾「重问」:固定宽度占位,平时留空 —— 显隐时不会挤动左边的问题 ask = tk.Label( row, text="重问" if (cur and self.on_reask) else "", width=4, - bg=base, - fg=T.c("accent"), + bg=fill, + fg=T.c("accent_text"), font=T.font("micro"), cursor="hand2", anchor="e", ) - ask.pack(side="right", padx=(0, T.SPACE["sm"])) + ask.pack(side="right", padx=(0, T.SPACE["sm"]), pady=T.SPACE["xs"]) if self.on_reask: ask.bind("", lambda _e, q=item["question"]: self._reask(q)) - box = tk.Frame(row, bg=base) + box = tk.Frame(row, bg=fill) box.pack( side="left", fill="x", @@ -376,8 +438,8 @@ def _row(self, item: dict): tk.Label( box, text=_elide(item["question"], T.font("side"), self._text_w), - bg=base, - fg=T.c("accent") if cur else T.c("text"), + bg=fill, + fg=T.c("text"), font=T.font("side"), anchor="w", justify="left", @@ -390,29 +452,39 @@ def _row(self, item: dict): tk.Label( box, text=meta, - bg=base, + bg=fill, fg=T.c("text_faint"), font=T.font("micro"), anchor="w", justify="left", ).pack(anchor="w", fill="x") + # 圆角底图最后建(于是它在 winfo_children() 里排第 3)再沉底。 + # 底色传 surface 而不是 fill:圆角挖掉的那四个角要透出列表底色, + # 传 fill 就成了一个方角块。 + bg_cv, set_fill = _rounded_bg(row, T.c("surface"), T.RADIUS["md"]) + set_fill(fill) + def enter(_e=None): - if item is not self._current: - W.set_bg_recursive(row, T.c("card_hover")) + if not cur: + set_fill(T.c("nav_hover")) + W.set_bg_recursive(row, T.c("nav_hover")) if self.on_reask: ask.configure(text="重问") def leave(_e=None): - if item is not self._current: - W.set_bg_recursive(row, base) - if not (item is self._current): + if not cur: + set_fill(fill) + W.set_bg_recursive(row, fill) ask.configure(text="") W.bind_hover(row, on_enter=enter, on_leave=leave) W.bind_click_recursive(box, lambda i=item: self._open(i)) W.set_cursor_recursive(box) W.make_focusable(row, T.c("surface"), lambda i=item: self._open(i)) + # 圆角块的两头(子控件够不到的地方)也要能点、能呼出右键菜单 + bg_cv.configure(cursor="hand2") + bg_cv.bind("", lambda _e, i=item: self._open(i), add="+") menu = tk.Menu(row, tearoff=0, font=T.font("caption")) menu.add_command(label="打开这轮问答", command=lambda i=item: self._open(i)) @@ -429,11 +501,12 @@ def leave(_e=None): menu.add_command( label="复制答案", command=lambda t=item["answer"]: self._copy(t) ) - row.bind( - "", - lambda e, m=menu: self._popup(e, m), - add="+", - ) + for w in (row, bg_cv): + w.bind( + "", + lambda e, m=menu: self._popup(e, m), + add="+", + ) # ---------- 动作 ---------- diff --git a/pixelrag_theme.py b/pixelrag_theme.py index 2bb3af5..605254f 100644 --- a/pixelrag_theme.py +++ b/pixelrag_theme.py @@ -21,6 +21,10 @@ "surface": "#F9F9F9", # 侧栏 / 顶栏 / 底栏(DeepSeek 冷灰) "card": "#FFFFFF", # 卡片表面 "card_hover": "#F5F6F8", # 卡片 hover + # 侧栏条目(DeepSeek 是浅灰圆角块,不是描边也不是蓝底): + # 底色是 surface(#F9F9F9),所以这两个值都要比它明显深一档才看得出来。 + "nav_hover": "#F0F0F2", # 条目 hover(11.9:1 上的文字仍清晰) + "nav_active": "#E4E6EA", # 当前打开的那一条(比 hover 再实一点) "border": "#E5E7EB", # 常规分隔线 "border_strong": "#D1D5DB", # hover / focus 时的边框 "text": "#1F2329", # 正文(15.8:1 on bg) @@ -54,6 +58,8 @@ "surface": "#1E1E1E", "card": "#242424", "card_hover": "#2A2A2A", + "nav_hover": "#2C2C2E", + "nav_active": "#38383C", "border": "#333333", "border_strong": "#454545", "text": "#E8E8E8", # 14.2:1 on bg @@ -130,6 +136,7 @@ def c(key: str) -> str: "bubble": (16, "normal"), # 用户提问气泡 "answer": (16, "normal"), # 答案正文 "input": (16, "normal"), # 输入框 + "code": (14, "normal"), # 代码块(等宽;字号走 font_mono,行高同样受 LINE_HEIGHT 管) "side": (14, "normal"), # 侧栏条目 "label": (14, "bold"), # 卡片标题 "button": (14, "bold"), # 按钮 @@ -207,27 +214,45 @@ def tk_scaling() -> float: # --------------------------------------------------------------------------- _BASE_SPACE = {"xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "2xl": 32, "msg": 24} _BASE_RADIUS = {"sm": 6, "md": 8, "lg": 12} +# 胶囊(整圆端头):给一个"远大于任何按钮高度"的值,绘制时会被自动夹到高度的一半 +# (见 widgets.render_label_box),于是按钮高度随字号 / DPI 变化时端头依然是半圆, +# 调用方不需要先知道按钮多高。DeepSeek 的发送/停止按钮都是这个形状。 +RADIUS_PILL = 999 +# 滚动条宽度(DeepSeek 是细条、无可见槽)。tk 只能给像素值,所以跟着 UI_SCALE 走。 +_BASE_SCROLLBAR_W = 8 +SCROLLBAR_W = _BASE_SCROLLBAR_W +# 发送按钮的最小宽度(设计像素):它在「发送 / 生成中」两种文案间切换,给个下限 +# 让它宽度不变,不然输入框里的文字会跟着一伸一缩。 +_BASE_SEND_MIN_W = 72 +SEND_MIN_W = _BASE_SEND_MIN_W +# 组件级尺寸(设计像素):不在 4px 栅格上、但 DeepSeek 有明确规格的那几个。 +# 单列出来是为了不被"顺手对齐到栅格"改掉 —— 改了就不是 DeepSeek 的比例了。 +_BASE_BUBBLE_PAD = (14, 10) # 用户气泡内边距 (横向, 纵向) # 消息列的宽度上限(DeepSeek 是 768)。窗口再宽也不跟着变宽 —— 富余的宽度均分成 # 两侧留白,正文始终是一条居中的窄栏,长行才不会横贯整屏。 _BASE_CONTENT_MAX_WIDTH = 768 -# 下面三个是"逻辑值 × UI_SCALE"的结果;启动时由 set_ui_scale 按屏幕 DPI 重算, +# 下面几个是"逻辑值 × UI_SCALE"的结果;启动时由 set_ui_scale 按屏幕 DPI 重算, # 以便在高分屏上字变大的同时,内边距/圆角/行宽等比跟上,不会显得局促。 SPACE: dict[str, int] = dict(_BASE_SPACE) RADIUS: dict[str, int] = dict(_BASE_RADIUS) +BUBBLE_PAD: tuple[int, int] = _BASE_BUBBLE_PAD CONTENT_MAX_WIDTH = _BASE_CONTENT_MAX_WIDTH UI_SCALE = 1.0 def set_ui_scale(factor: float) -> float: """按屏幕缩放比例重算所有像素级令牌(0.75x ~ 2.0x)。""" - global UI_SCALE, CONTENT_MAX_WIDTH + global UI_SCALE, CONTENT_MAX_WIDTH, BUBBLE_PAD, SCROLLBAR_W, SEND_MIN_W UI_SCALE = max(0.75, min(2.0, float(factor))) for k, v in _BASE_SPACE.items(): SPACE[k] = max(1, round(v * UI_SCALE)) for k, v in _BASE_RADIUS.items(): RADIUS[k] = max(2, round(v * UI_SCALE)) + BUBBLE_PAD = tuple(max(1, round(v * UI_SCALE)) for v in _BASE_BUBBLE_PAD) CONTENT_MAX_WIDTH = round(_BASE_CONTENT_MAX_WIDTH * UI_SCALE) + SCROLLBAR_W = max(4, round(_BASE_SCROLLBAR_W * UI_SCALE)) + SEND_MIN_W = max(24, round(_BASE_SEND_MIN_W * UI_SCALE)) return UI_SCALE diff --git a/pixelrag_widgets.py b/pixelrag_widgets.py index 43e3390..07047de 100644 --- a/pixelrag_widgets.py +++ b/pixelrag_widgets.py @@ -103,6 +103,23 @@ def tk_line_gap_px(font: tuple) -> int: return max(0, target - natural) +def line_spacing(font: tuple) -> tuple[int, int, int]: + """给 ``tk.Text``(控件级或 tag 级)用的 ``(spacing1, spacing2, spacing3)``。 + + tk 会把 ``spacing2``(**段内折行**)对半分给折点两侧,于是同一个行高要求下, + 段首行只吃到下半份、段末行只吃到上半份,而"硬换行后面的那行"两个份都吃 —— + 按直觉写 spacing 会让整段行距忽紧忽松。只有 + ``spacing1 = ceil(gap/2)``、``spacing2 = gap``、``spacing3 = floor(gap/2)`` + 这一组能让下面五种行盒**完全相等**(都等于 字号 × LINE_HEIGHT): + + - 单行段落、多行段落的段首行 / 中间行 / 段末行、硬换行(``\\n``)之后那行。 + + 实测记录与逐行断言见仓库外的 ``_uicheck/linecheck.py``。 + """ + gap = tk_line_gap_px(font) + return -(-gap // 2), gap, gap // 2 + + # --------------------------------------------------------------------------- # 文本换行(CJK 友好) # --------------------------------------------------------------------------- @@ -199,11 +216,15 @@ def render_label_box( align: str = "left", line_gap: int | None = None, bold: bool = False, + min_w: int | None = None, ) -> Image.Image: """把文本渲染成一张图片。``max_w=None`` 表示不折行(按钮用)。 ``line_gap=None`` 时按主题行高(``T.LINE_HEIGHT``)自动算,多行文本才有区别 —— 单行时上下各减一次,最终高度与 line_gap 无关,所以按钮不会因此变胖。 + + ``min_w`` 给图片一个最小宽度(图片比它窄时两侧留白,``align="center"`` 仍然居中): + 按钮在「发送 / 生成中」这类不同文案之间切换时宽度不跳,旁边的控件就不会被推来推去。 """ bold = bold or T.TYPE[font_token][1] == "bold" if line_gap is None: @@ -221,6 +242,8 @@ def render_label_box( asc, desc = font.getmetrics() line_h = asc + desc + line_gap w = text_w + 2 * pad_x + if min_w: + w = max(w, int(min_w)) h = line_h * len(lines) + 2 * pad_y - line_gap if box_fill is None: @@ -408,33 +431,12 @@ def selectable_text( ``state="disabled"``(禁用态在 Windows 上会吃掉一部分选择/复制交互),而是 保持 normal 并在按键层拦截编辑,行为更接近网页上的只读文本。 - 行距按主题行高(``T.LINE_HEIGHT``)铺开,``line_gap=None`` 时自动算。 - - tk 的三个 spacing 语义各不相同,而 ``spacing2``(段内折行)会被 tk **上下对半 - 分配**给折点两侧 —— 实测的每行行盒是: - - ===================== ========================================== - 行的位置 实际行盒 - ===================== ========================================== - 单行段落 linespace + spacing1 + spacing3 - 多行段落的段首行 linespace + spacing1 + floor(spacing2/2) - 多行段落的中间/段末行 linespace + ceil(spacing2/2) + … - 硬换行(``\\n``)后那行 linespace + spacing3 + spacing1 - ===================== ========================================== - - 要把这些**全部**拉到同一个行盒 ``linespace + gap``,只有一组解: - ``spacing1 = ceil(gap/2)``、``spacing2 = gap``、``spacing3 = floor(gap/2)``。 - 随手写成 ``spacing1=0, spacing2=spacing3=gap`` 会让段首行矮 4px、段末行矮 3px, - 整段看着忽紧忽松 —— ``linecheck.py`` 就是钉这个的(断言每行行盒完全相等)。 - - 顺带:单行段落的行盒 = linespace + spacing1 + spacing3 正好也是 ``fit_height`` - 折算 ``-height`` 用的基准,所以行距一改控件高度自动跟上。段与段之间的空档由 - 正文里的空行承担(空行本身也是一个"单行段落")。 + 行距按主题行高(``T.LINE_HEIGHT``)铺开,``line_gap=None`` 时自动算 + (算法与理由见 ``line_spacing``)。单行段落的行盒 = linespace + spacing1 + + spacing3 正好也是 ``fit_height`` 折算 ``-height`` 用的基准,所以行距一改控件 + 高度自动跟上。段与段之间的空档由正文里的空行承担(空行本身也是一个"单行段落")。 """ - if line_gap is None: - line_gap = tk_line_gap_px(font) - half_hi = -(-line_gap // 2) # ceil:折点上半 - half_lo = line_gap // 2 # floor:折点下半 + sp1, sp2, sp3 = line_spacing(font) if line_gap is None else (0, line_gap, 0) txt = tk.Text( parent, bg=bg, @@ -447,9 +449,9 @@ def selectable_text( height=1, padx=pad_x, pady=pad_y, - spacing1=half_hi, - spacing2=line_gap, - spacing3=half_lo, + spacing1=sp1, + spacing2=sp2, + spacing3=sp3, cursor="xterm", insertwidth=0, takefocus=1, From fed2e04e0ff3b1fa24d6705b0ba40f3691073533 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 17:56:21 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix(ui):=20=E7=AD=94=E6=A1=88=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E5=8E=BB=E6=8E=89=E6=98=9F=E5=8F=B7,=E5=B0=8F?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E5=8A=A0=E7=B2=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模型输出的答案里带着 Markdown 行内标记(`**小标题**` 和行首 `* 条目`), 直接进 Text 就是一串星号,读起来很吵。真机历史里实际只出现这两类标记 (5 处 `**N. 标题**` + 11 处 `* 条目`,没有 `#`、没有斜体、没有行内反引号), 所以只处理这两类: - `**粗体**` → 去掉星号、套 tk 的 bold 标签(tmp 的粗体只借字重,字号与行高 仍归 token 管,所以加粗后行盒不会变高);行首 `#`/`##` 标题同理去 `#` 并加粗。 - 行首 `* 条目` → 换成圆点 `•`,保住列表形态(星号消失但层次还在)。 - 不动的:`-` 列表、`a*b*c` 里的单个星号、`*.tmp` 通配符、代码块内的 `**` —— 代码块先被 `_split_code_blocks` 切走,正文里再出现 `**` 才当标记处理。 流式生成走同一条富文本管线,所以生成过程中也不会闪星号。测试里用**独立实现** 的 `strip_marks()` 与渲染结果逐字比对,证明只去掉了标记字符、正文一个不少。 验证:11 套自检全绿(新增 mdcheck 一套,含"渲染结果 == 去标记后的原文"的 逐字比对 + 截图肉眼复核),ruff check / format 干净。 注:复制答案仍是原始 Markdown(带星号),这是有意的 —— 粘到别处还得能用。 --- pixelrag_client.py | 82 +++++++++++++++++++++++++++++++++++++++++++--- pixelrag_theme.py | 11 +++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index 69d2dee..171abeb 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -292,6 +292,60 @@ def _split_code_blocks(text: str) -> list[tuple[str, str]]: return out or [("text", text)] +# ---- 答案正文里的行内标记 --------------------------------------------------- +# 模型给的答案不是纯文本,常见的是「**小标题**」和行首「* 条目」,原样塞进 Text 就会 +# 把星号显示出来。这里只认模型真会写的那三种,不做通用 Markdown —— 宁可漏掉罕见的 +# 写法,也不要把正文里的星号当成标记吃掉(数学式 a*b、脚注 * 之类都很常见)。 +_BOLD_RE = re.compile(r"\*\*(?=\S)(.+?)(?<=\S)\*\*", re.DOTALL) +_BULLET_RE = re.compile(r"^([ \t]*)\*[ \t]+") +_HEADING_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+") + + +def _rich_runs(text: str) -> list[tuple[str, str]]: + """把答案正文切成 ``(文字, 标签)``:标签 ``""`` 是普通文字,``"bold"`` 是加粗。 + + - ``**强调**`` → 去掉星号,文字加粗(模型常拿它当小标题用,粗体得保住); + - 行首 ``#`` 标题 → 去掉 # 号与空格,整行加粗(各级都是加粗,不改字号); + - 行首 ``* 条目`` → 星号换成圆点(列表还是列表,屏幕上不再有星号); + - ``-`` 开头的列表不动:它不是星号,换成圆点反而和破折号撞脸。 + + 代码块不经过这里 —— 调用方先用 :func:`_split_code_blocks` 把它们分出去了。 + """ + runs: list[tuple[str, str]] = [] + pos = 0 + for m in _BOLD_RE.finditer(text): + if m.start() > pos: + _plain_runs(text[pos : m.start()], runs) + runs.append((m.group(1), "bold")) + pos = m.end() + if pos < len(text): + _plain_runs(text[pos:], runs) + return runs + + +def _plain_runs(chunk: str, runs: list[tuple[str, str]]) -> None: + """普通片段里的行首标记:先换圆点、再剥 ``#`` 标题,逐行处理(换行符原样留着)。""" + for line in chunk.splitlines(keepends=True): + head = _BULLET_RE.match(line) + if head: + # 只换星号那一个字符:后面用来对齐的空格原样保留,列表的缩进才不变 + i = len(head.group(1)) + line = line[:i] + "•" + line[i + 1 :] + m = _HEADING_RE.match(line) + if m: + body = line[m.end() :] + nl = "\n" if body.endswith("\n") else "" + body = body[:-1] if nl else body + # 整行本来就要加粗,行内的 ** 就只剩标记作用了,一并去掉 + body = _BOLD_RE.sub(r"\1", body) + if body: + runs.append((body, "bold")) + if nl: + runs.append((nl, "")) + elif line: + runs.append((line, "")) + + # --------------------------------------------------------------------------- # 界面 # --------------------------------------------------------------------------- @@ -998,6 +1052,13 @@ def _render_answer(self, msg: dict): spacing2=sp2, spacing3=sp3, ) + # 行内加粗(答案里的小标题)。它自带行距,理由和代码块一样:粗体的行盒比 + # 正文字体高 1px,不补 spacing 的话同一段里会隔三差五冒出一行高 1px。 + bold_font = T.font("answer", "bold") + bs1, bs2, bs3 = W.line_spacing(bold_font) + txt.tag_configure( + "bold", font=bold_font, spacing1=bs1, spacing2=bs2, spacing3=bs3 + ) self._answer_text = txt text = msg.get("text", "") @@ -1010,20 +1071,33 @@ def _render_answer(self, msg: dict): self._render_copy_button(container, text) def _set_answer_into(self, txt: tk.Text, s: str, settle: bool = False): - """整体重写正文。内容短、节流到 60ms 一次,所以不做增量插入。""" + """整体重写正文。内容短、节流到 60ms 一次,所以不做增量插入。 + + 流式也走 :func:`_rich_runs`:否则生成过程中会先看到一堆星号,定稿时才消失。 + 代价是半个标记(``**开头`` 还没等到收尾)会短暂按原样显示,收尾一到就变粗。 + """ txt.delete("1.0", "end") - txt.insert("1.0", s) + self._insert_runs(txt, s) W.fit_height(txt, settle=settle) def _fill_answer(self, txt: tk.Text, text: str): - """定稿排版:代码段套 code 标签(等宽 + 淡底 + 内缩)。""" + """定稿排版:代码段套 code 标签(等宽 + 淡底 + 内缩),其余按行内标记分粗/不粗。""" for seg_kind, seg_text in _split_code_blocks(text): if seg_kind == "code": txt.insert("end", seg_text + "\n", "code") elif seg_text: - txt.insert("end", seg_text) + self._insert_runs(txt, seg_text) W.fit_height(txt, settle=True) + @staticmethod + def _insert_runs(txt: tk.Text, text: str): + """按 :func:`_rich_runs` 的分段插入:加粗的套 bold 标签,其余原样。""" + for s, tag in _rich_runs(text): + if tag: + txt.insert("end", s, tag) + else: + txt.insert("end", s) + def _render_copy_button(self, parent, text: str): bar = tk.Frame(parent, bg=T.c("bg")) bar.pack(fill="x", pady=(T.SPACE["sm"], 0)) diff --git a/pixelrag_theme.py b/pixelrag_theme.py index 605254f..d9f53aa 100644 --- a/pixelrag_theme.py +++ b/pixelrag_theme.py @@ -158,9 +158,14 @@ def pt(token: str) -> int: return max(1, round(TYPE[token][0] * PX_PER_PT * FONT_SCALE)) -def font(token: str) -> tuple: - """取 tk 字体元组,如 ('Microsoft YaHei UI', 12, 'normal')。""" - return (FONT_FAMILY, pt(token), TYPE[token][1]) +def font(token: str, weight: str | None = None) -> tuple: + """取 tk 字体元组,如 ('Microsoft YaHei UI', 12, 'normal')。 + + ``weight`` 可以覆盖 token 自带的那一档(取值同 tk:``normal`` / ``bold`` / + ``italic`` / ``bold italic``)。正文里临时加粗(答案里的小标题)用它 —— + 字号与行高仍然归 token 管,免得为了一个粗体另起一套数字。 + """ + return (FONT_FAMILY, pt(token), weight or TYPE[token][1]) def font_mono(size: int = 14) -> tuple: From 5e6176233eea531dc5eae708d7de735391b18446 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Thu, 8 Oct 2026 18:05:44 +0800 Subject: [PATCH 09/11] =?UTF-8?q?feat(ui):=20=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=87=8C=E4=B9=9F=E8=A6=81=E6=9C=89=20PDF=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 点开一条旧问答,答案在、引用卡片却一张不剩。查下来是两种情况: 1. 存引用页的代码是后加的,之前留下的记录里 refs 是空的 —— 数据从来没存过, 光读是读不出来的; 2. 检索成功但没走完生成的那一轮(没配 API key / 截图加载失败,只降级显示引用 卡片),回填历史的调用挂在生成成功那条路上,这些轮次只留下 hits=None,侧栏 显示「未完成」。引用页是检索的结果,不该跟着答案一起丢。 改法: - 抽 refs_of() / tiles_from():命中列表 → 引用页 / 引用卡片,按 (文章, 页, 段) 去重。存进历史的仍然是数据(article_id / tile_index / url 五个字段)不是图片, 缩略图照旧由 history_tiles() 从本地 tiles 现读,历史文件不会因此变大。 - 降级路径也回填历史:_show_results() 顺带把这轮检索到的页记下来,所以"没生成 出答案"不等于"没引用过"。 - 老记录点开时按原问题补跑一次检索,把卡片补上。只问本地检索服务,没有模型调用, 所以打开旧问答依然不花钱;补到的页写回历史,补一次就够。补不到(服务没起来、 也没截图)就明说没补到 —— 空着不吭声会让人以为这条记录本来就没有引用。 - 补检索来的卡片上方加一行说明:这些页是按原问题重新检索的,**未必是当时那几页**, 要还原当时那一轮请点「重问」。不能把重新检索的结果冒充成原本的出处。 - 补检索期间用户点开别的记录:数据照样写回(那条记录的事实,与此刻在看哪条无关), 但卡片不往新对话里插。 验证:新增 _uicheck/citcheck.py(26 项,覆盖新记录还原 / 老记录补检索 / 补失败 / 中途走开 / 降级轮次回填),其余 12 套自检全绿。另用真实检索服务打了一次 BFO 问题: 5 条命中、refs_of 与 tiles_from 各 5 条、PDF 与 tile 文件都在磁盘上 —— 用户那条 历史记录下次点开就会自动补上。ruff check / format 干净(仅 query.py 是既有问题)。 --- pixelrag_client.py | 156 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 136 insertions(+), 20 deletions(-) diff --git a/pixelrag_client.py b/pixelrag_client.py index 171abeb..a832ce5 100644 --- a/pixelrag_client.py +++ b/pixelrag_client.py @@ -137,6 +137,56 @@ def _load_titles() -> dict[int, str]: return {} +def refs_of(hits) -> list[dict]: + """把命中列表压成"引用页"列表,按 (文章, 页, 段) 去重。 + + 存进历史的是**数据不是图片**:缩略图由 :func:`history_tiles` 从本地 tiles 现读, + 所以历史文件很小,而且 tiles 重新渲染过也照样对得上。 + + 去重是因为同一个问题走不同路径(直接生成 / 降级只显示引用)可能拿到同一页两条命中, + 不去重历史里就会存两份一模一样的卡片。 + """ + out: list[dict] = [] + seen: set = set() + for h in hits or []: + if not isinstance(h, dict): + continue + ref = { + "article_id": h.get("article_id"), + "tile_index": h.get("tile_index"), + "chunk_index": h.get("chunk_index"), + "score": h.get("score"), + "url": h.get("url"), + } + key = (ref["article_id"], ref["tile_index"], ref["chunk_index"]) + if key in seen: + continue + seen.add(key) + out.append(ref) + return out + + +def tiles_from(hits) -> list[tuple[str, dict]]: + """命中列表 → 引用卡片要的 ``(图片 base64, hit)``。 + + 按相似度降序排、按 (文章, 页, 段) 去重,并且丢掉没带截图的命中 —— 卡片是靠 + 截图显示"引用的是哪一页"的,没有图就没得显示。 + """ + out: list[tuple[str, dict]] = [] + seen: set = set() + for h in sorted(hits or [], key=lambda x: float(x.get("score", 0.0)), reverse=True): + if not isinstance(h, dict): + continue + key = (h.get("article_id"), h.get("tile_index"), h.get("chunk_index")) + if key in seen: + continue + seen.add(key) + b64 = h.get("image_base64") + if b64: + out.append((b64, h)) + return out + + def history_tiles(item: dict) -> list[tuple[str, dict]]: """把历史里的引用页还原成 ``(图片 base64, hit)``。 @@ -383,6 +433,7 @@ def __init__(self, root: tk.Tk): self._sidebar_open = True # 顶栏「历史」按钮切换它 self._current_hist: dict | None = None # 当前打开的那轮问答 self._hist_entry: dict | None = None # 本次提问对应的历史条目,答完回填 + self._refetching = False # 正在给老记录补检索引用页(同时只补一条) self._hist_pos: int | None = None # None = 不在召回态;0 = 最新一条 self._hist_draft = "" # 召回前用户已经敲了一半的内容 self._serve_ready = check_health() @@ -1130,6 +1181,19 @@ def copy(_e=None): def _render_citations(self, msg: dict): self._render_section("引用来源") + # 补检索来的引用页要写清楚来路:这些页不是当时那几页,不能让人当成本来的出处 + note = msg.get("note") + if note: + tk.Label( + self.frame, + text=note, + bg=T.c("bg"), + fg=T.c("text_faint"), + font=T.font("caption"), + justify="left", + anchor="w", + wraplength=self._content_w, + ).pack(fill="x", padx=self._inset, pady=(0, T.SPACE["sm"])) tiles = msg["tiles"] # 卡片是一组:卡与卡之间用 sm(挨着才像一组),整组结束后才轮到消息间距。 for i, (b64, hit) in enumerate(tiles, 1): @@ -1533,8 +1597,66 @@ def _open_history_item(self, item: dict): tiles = history_tiles(item) if tiles: self._append({"kind": "citations", "tiles": tiles}) + elif (item.get("question") or "").strip(): + # 这条记录里没有引用页(存引用页的功能上线前留下的老记录,或者检索成功 + # 但没走完生成的那一轮)。按原问题补跑一次检索,别让引用卡片就这么空着。 + self._refetch_refs(item) self._scroll_bottom(True) + def _refetch_refs(self, item: dict): + """给没存下引用页的记录补一次检索,把引用卡片补齐。 + + 只问本地检索服务,没有模型调用,所以打开旧问答依然是"不花一分钱"。补到的页 + 写回历史,下次点开直接就有;补不到就明说没补到 —— 空着不吭声会让人以为 + 这条记录本来就没有引用。 + """ + if self._refetching or not (item.get("question") or "").strip(): + return + if not self._serve_ready: + self._add_note("这条记录当初没有存下引用页,当前检索服务未就绪,没法补齐。") + return + self._refetching = True + question = item["question"] + self._start_loading("正在补齐这条记录的引用页") + + def work(): + try: + hits = search(question)["results"][0]["hits"] + except Exception: + hits = [] + self.root.after(0, lambda: apply(hits)) + + def apply(hits: list): + self._refetching = False + self._stop_loading() + tiles = tiles_from(hits) + if tiles: + # 数据先写回历史:"这条记录引用过哪几页"是跟这条记录有关的事实, + # 与用户此刻正在看哪一条无关。补到一次就存下来,下次点开不用再查。 + self.history.update( + item, hits=len(tiles), refs=refs_of([h for _b64, h in tiles]) + ) + self._refresh_sidebar() + # 这期间用户可能已经点开别的记录、或者又问了一轮 —— 内容就别往新对话里塞 + if self._current_hist is not item: + return + if not tiles: + self._add_note( + "这条记录当初没有存下引用页,刚按原问题补检索也没拿到截图。" + "「重问」可以重新生成完整的一轮。" + ) + return + self._append( + { + "kind": "citations", + "tiles": tiles, + "note": "这条记录当初没有存下引用页。以下是按原问题重新检索到的" + "页面,未必是当时那几页;要还原当时那一轮请点「重问」。", + } + ) + + threading.Thread(target=work, daemon=True).start() + def _reask(self, question: str): """从历史里点一条:填进输入框并直接提问。""" self._replace_input(question) @@ -1699,20 +1821,23 @@ def _do_search(self, text: str): self._generate(text, hits) # 仍在后台线程,继续生成答案 def _show_results(self, hits: list): - """VLM 不可用时的降级:只展示检索到的引用卡片。""" - seen, tiles = set(), [] - for h in sorted(hits, key=lambda x: float(x.get("score", 0.0)), reverse=True): - key = (h.get("article_id"), h.get("tile_index"), h.get("chunk_index")) - if key in seen: - continue - seen.add(key) - b64 = h.get("image_base64") - if b64: - tiles.append((b64, h)) + """VLM 不可用时的降级:只展示检索到的引用卡片。 + + 顺带把这轮检索到的页记进历史。这一轮没有答案,但"引用过哪几页"是检索的 + 结果、和答案不是一回事,不该跟着答案一起丢 —— 否则回头点开这条记录, + 侧栏写着「未完成」、引用卡片一张也不剩。 + """ + tiles = tiles_from(hits) if tiles: self._append({"kind": "citations", "tiles": tiles}) else: self._add_note("没有可显示的截图。") + self.history.update( + self._hist_entry, + hits=len(tiles), + refs=refs_of([h for _b64, h in tiles]), + ) + self._refresh_sidebar() def _generate(self, text: str, hits: list): """准备截图 → 调 VLM 流式生成 → 前端流式显示答案 + 引用卡片。运行于后台线程。""" @@ -1860,16 +1985,7 @@ def _finish_answer(self, images, stopped: bool = False, failed: bool = False): self._hist_entry, hits=len(images), answer=(msg or {}).get("text", ""), - refs=[ - { - "article_id": h.get("article_id"), - "tile_index": h.get("tile_index"), - "chunk_index": h.get("chunk_index"), - "score": h.get("score"), - "url": h.get("url"), - } - for _b64, h in images - ], + refs=refs_of([h for _b64, h in images]), ) self._refresh_sidebar() From be0dfe069d39f7ecdfd9000a5b47cde81f3c32a4 Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Fri, 9 Oct 2026 00:57:09 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=E5=8A=A0=20rebuild.bat=20?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E9=87=8D=E5=BB=BA=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 往 papers\ 里加完论文后双击一次,把"重建索引 -> 停旧服务 -> 起新服务" 串起来。第 2 步是重点:serve 只在启动时把索引读进内存,之后不重载, 不重启就还在用老索引回答,重建等于白等。 - 脚本按 GBK(cp936)存、不带 chcp:UTF-8 码页下 cmd 的批处理读取器 遇到多字节汉字会随机错位,把 echo 的内容当命令执行(实测踩到)。 - 数 PDF 用纯 cmd 的 for + set /a,不调 dir/find —— git 自带的同名 find 会被优先命中,报一堆无关错误。 - 等待用 ping 而不是 timeout:timeout 需要控制台 stdin,重定向时报 "不支持输入重定向"。 - 杀进程只挑 30001 上 LISTENING 的那个,不会误伤连上来的客户端连接; 本来就没有服务在跑时也照常往下走。 - build 失败走 :failed 分支,不碰旧服务,旧索引保持完好。 验证:做了个干跑副本(真实命令换成 echo、杀进程换成打印)在仓库目录下 按 %~dp0 实跑,中文显示正常、退出码 0、正确抓到 30001 上的旧服务 PID。 --- rebuild.bat | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 rebuild.bat diff --git a/rebuild.bat b/rebuild.bat new file mode 100644 index 0000000..8c5b836 --- /dev/null +++ b/rebuild.bat @@ -0,0 +1,92 @@ +@echo off +cd /d "%~dp0" +setlocal + +rem --------------------------------------------------------------- +rem PixelRAG index rebuild. +rem Run this after dropping new PDFs into papers\. +rem step 1 rebuild the index +rem step 2 stop the old search server. DO NOT SKIP: serve reads +rem the index into memory once at startup and never reloads +rem it, so without a restart it keeps answering from the +rem stale index and the rebuild is wasted. +rem step 3 start a fresh server +rem NOTE: this file is saved as GBK (cp936), NOT UTF-8, and has no +rem chcp line. Under chcp 65001 cmd's batch reader desyncs on +rem multi-byte characters and starts executing echo lines as +rem commands. Keep rem comments ASCII; edit this file as GBK. +rem --------------------------------------------------------------- + +set "NPAPERS=0" +for %%f in ("papers\*.pdf") do set /a NPAPERS+=1 + +echo ============================================================ +echo PixelRAG Ë÷ÒýÖؽ¨ +echo ============================================================ +echo. +echo ÂÛÎÄĿ¼ papers\ ÓÐ %NPAPERS% ¸ö PDF +echo Ë÷ÒýÊä³ö knowledge_index\ +echo. +echo עÒâ: ǶÈë½׶ÎÊÇȫÁ¿ÖØÅÜ,²»»áÒòΪֻ¼ÓÁ˼¸ƪ¾ÍÉÙÅܡ£ +echo µ±ǰ¹æģÔÚ CPU ÉÏԼÐè 20 СʱÒÔÉϡ£ +echo ÖÐ;¹صô´°¿Úû¹Øϵ,ÖØÅÜʱ´ӶϵãÐøÅÜ,²»»á´ÓͷÀ´¡£ +echo. +pause + +set HF_HUB_OFFLINE=1 +set TRANSFORMERS_OFFLINE=1 + +echo. +echo ------------------------------------------------------------ +echo [1/3] Öؽ¨Ë÷Òý(Õâһ²½×î¾Ã,¿ÉÒԷÅ×Ų»¹Ü)... +echo ------------------------------------------------------------ +.venv\Scripts\pixelrag.exe index build -c pixelrag.yaml +if errorlevel 1 goto :failed + +echo. +echo ------------------------------------------------------------ +echo [2/3] ͣµô¾ɵļìË÷·þÎñ... +echo ------------------------------------------------------------ +call :stop_serve +rem 2s wait for the port to free up. ping, not timeout: timeout wants a +rem console on stdin and errors out when the script runs headless. +ping -n 3 127.0.0.1 >nul + +echo. +echo ------------------------------------------------------------ +echo [3/3] Æô¶¯ÐµļìË÷·þÎñ(¼ÓÔØģÐÍԼÐè 1 ·ÖÖÓ)... +echo ------------------------------------------------------------ +start "PixelRAG serve" cmd /k ".venv\Scripts\pixelrag.exe serve --index-dir knowledge_index --tiles-dir knowledge_index/tiles --articles-json knowledge_index/articles.json --device cpu --port 30001" + +echo. +echo ============================================================ +echo Íê³ɡ£µÈԼ 1 ·ÖÖӺó˫»÷ PixelRAG¿ͻ§¶Ë.bat ²éѯÐÂË÷Òý¡£ +echo пªÄǸö´°¿ھÍÊǼìË÷·þÎñ,±ð¹ØËü¡£ +echo ============================================================ +pause +exit /b 0 + + +:stop_serve +rem Kill whatever listens on 30001 (the old serve). Skip if nothing does. +set "KILLED=" +for /f "tokens=5" %%p in ('netstat -ano ^| findstr ":30001" ^| findstr "LISTENING"') do ( + taskkill /f /pid %%p >nul 2>&1 + if not errorlevel 1 ( + echo ÒѽáÊø¾ɷþÎñ PID %%p + set "KILLED=1" + ) +) +if not defined KILLED echo (¶˿Ú 30001 ÉÏûÓÐÔÚÅܵķþÎñ,Ìø¹ý) +exit /b 0 + + +:failed +echo. +echo ============================================================ +echo [X] Öؽ¨ʧ°Ü,Ë÷ÒýûÓиüÐÂ(¾ÉË÷ÒýÈÔÊÇÍêºõÄ)¡£ +echo ÍùÉϷ­Äܿ´µ½¿¨ÔÚÄÄһ²½;Ð޺úóÖØÅܱ¾½ű¾¼´¿É, +echo ÒѾ­ǶºõIJ¿·ֻá´ӶϵãÐøÅÜ,²»»á°×Åܡ£ +echo ============================================================ +pause +exit /b 1 From 8f2cbefb57a02ba707944c131868210f9aa3695a Mon Sep 17 00:00:00 2001 From: SX-ele <18157366946@163.com> Date: Fri, 9 Oct 2026 00:57:25 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix(embed):=20=E6=96=AD=E7=82=B9=E9=97=B4?= =?UTF-8?q?=E9=9A=94=E7=94=B1=E6=AF=8F=2010=20chunk=20=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=E5=88=B0=E6=AF=8F=201=20chunk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 嵌入阶段全量重跑、CPU 上约 125s/页,而进度全在内存里 —— 只有断点文件 能活过进程被杀(内存不足保护的常见后果)。原先每 10 个 chunk 才存一次, 一次 kill 最多丢约 20 分钟。 - 抽出 --checkpoint-every(默认 1),间隔交给 CLI,默认值收紧到每 1 个 chunk,一次 kill 最多丢 1 个约 2 分钟。 - 启动时打印一行说明断点落点与间隔,不用等模型加载完才知道会不会续跑。 - 存盘逻辑没动,仍是原子写(.tmp.npz + os.replace):写一半被杀不会弄坏 已有断点。 代价可忽略:全程约写 860MB / 22h。 注意 index build --force 会 rmtree 掉 embeddings/,断点文件正好在里面, 会被一起删掉 —— 重建时别带 -f。 验证:另写了个只测断点文件本身的脚本(仓库外,不提交),覆盖原子写、 只存已完成行、重复存盘覆盖不回退、坏 .tmp 不影响正式文件,7 项断言全过; --help 可见新参数。完整的"被杀后重启续跑"闭环未跑 —— 它要加载 2B 模型 (约 4~5GB),而本机当前仅剩 2.8GB 可用,留待下次 rebuild 时看日志里是否 出现 "Resuming from checkpoint: N/M"。 ruff check / format 干净。 --- embed/src/pixelrag_embed/embed_cpu.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/embed/src/pixelrag_embed/embed_cpu.py b/embed/src/pixelrag_embed/embed_cpu.py index ca8a0c9..aa1922c 100644 --- a/embed/src/pixelrag_embed/embed_cpu.py +++ b/embed/src/pixelrag_embed/embed_cpu.py @@ -275,6 +275,15 @@ def main(): "--instruction", default="", help="Instruction prefix for queries" ) parser.add_argument("--limit", type=int, default=None, help="Max chunks to embed") + parser.add_argument( + "--checkpoint-every", + type=int, + default=1, + help="Save a resume checkpoint every N chunks (default: 1). Embedding " + "keeps all progress in memory; only the checkpoint survives a kill, so " + "this bounds what an OOM kill costs. The write is a few MB at most, so " + "saving after every chunk is cheap.", + ) args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) @@ -289,12 +298,20 @@ def main(): items = items[: args.limit] else: logger.info("Found %d chunks to embed", len(items)) + + checkpoint_every = max(1, args.checkpoint_every) + logger.info( + "Checkpoint every %d chunk(s) -> %s (an OOM kill costs at most that much)", + checkpoint_every, + Path(args.output_dir) / "embed_checkpoint.npz", + ) embeddings, checkpoint_path = embed_items( items, args.model, device=args.device, instruction=args.instruction, checkpoint_dir=args.output_dir, + checkpoint_every=checkpoint_every, ) output_path = Path(args.output_dir) / "shard_000.npz"