# DereVisionScorer.py # # ComfyUI custom node. # # INPUT: # vision_result : STRING # The complete 1-110 response produced by dere_vision_questionnaire_prompt.md # # OUTPUTS: # 1. dere : Highest-scoring Dere # 2. top_3_deres : Top three Dere names, ranked # 3. scoring_summary : Parser diagnostics + ranked score summary # # The exact hidden 122-Dere scoring matrix and the exact Q11-Q110 option # texts are embedded below in compressed form. No external JSON files and # no third-party Python packages are required. import base64 import difflib import json import re import unicodedata import zlib from collections import Counter _EMBEDDED_DATA_B85 = r""" c-rl~TXQ2vk|z3BXnHQ6qmrPKO7$?Ns_N<1_Vi`C)!R1b*m*#aNfKQkz(oQ@vDe0If5dq?e{X- uhQ}>4c>yCm+~bnSmu|8|fh- ^T{ne|l&X=2u)x~n(v|s)B|9th+&GMqzHR|8rFF*S)SG$$_a{GDfzC7&PgZ=u$^=f0k{J8AC+PVM!QT?C$a=UY{@EiZtZ~ZU*w(&3e+wB3|mk0mf5B`^axBS P-e|h6x|97k2mmBxx@0U0FXV3ok|N8&V-mmtL{_SJ4+TY_puFkLBtNwoF-}UdiCHQ^!4u0?7*6$Ao|J6U;mw!0>XunwAFa2NoZ_C@|rfK!RH&@- i|8{$E(X_4l=Re;2<@nP-w(iybv~lm?Pp!K$fAP!ymu>6c_h0qjJ^R1T{<^$yZ}qRs-hc0|miltP_dj#kEVpMLw%fJ;=6B6<@4xx$VYh3R_#c1iuIDdXzuv# Jem&He_wHr?=Kk?-t^cR)*7@5%- G}*4^QrY>{B?Ey`N2P_uRpI=_>WunLH_S;@9%Bkr|Yf%@@8qj{G{&V?!CDmxY+L8zh15UD{Pw2{uTaw@c(>!*sbiB?e$?}zudbg;{5#DU$6J~+rwvnr!2esu lfAhzuCcM$8YeOH#Z0Odw*Qrx;6RJ!QbQROMjtm4p;7jH_O(q|F!?;- @D%$+lyj=Mu^jGYc?cvk5``Uk~zwMX3?QN@XBlVSfPVMG~d!gOj)U{cf&FxxkP9?Plf(Lb+4bB$54HBj nCl!`|7Kk<$kyN^{XHM=U4yn`ZfOFPx^oCa{ajI?b$!VqPK19|Ku)<{>E?J`Um~)t- o^{xcz3=`$4~rT>q!v^uKZ2IsTa6(9M5wd%FHlhu)95T@}CaJG=fJzqy<5- 2SeA(CzU0KRw>$e_TAd&6}V0JH7q3_t}2mUpl|xo1Z&u`rjOSKlab=8@u)EA9{NC*FW{t{*V8`R{;G2z6juda%Dh&- S#fGs|ENUx@w^R)8m3*{@K?A{2gBv>^JTq(%*KKq5D?XhVENe9rR240%5+}_O6+)684+>?$7+K_n}>}Fy9RBS64FhH;<~uqkpx91OI?49r|Zn^)PSI6_6hny %OTS(M6>9=DQ!$g~WZMONskN7Zd+#x}3Q0bU|_7=#t{T*>_KGudbMX=t_&d+Xwf4S76Ld_o&9`f9}eR`PNk$bDz0l^W&mdZuAv)1;<`oS98p_uI!j6r+WsTD n0lgeDSf{)O~LU+xE5rUxUn^<*E>V=j)JuiLMmcEuy~-`hujtb2SNH#8oA`oY&o7?n;ySZ`;8$-`jEjIM^S3mGa- |LS??|e==XK>~rX!HCXg(R`1c%Rm&{*a@D^SpSvIJs+axlbN5DF1@l|{*51UnjQzOi6*BWfu9p3{=&G6DXR)BUEl}4qf7iLMyUOL{zh3F?+t2!SlteU(4XRV)lG3gJ+8w4650nm_blAgP?MDq?*Blat06T89XRx@VKHKDQFN@(Vj18{d)FfIU8HeqKg@jsu_$ZX9Cs iNHK#!HG@Gp8&lyS48XT!;5LVK9747+g)~{#KFKB=j?L%mf27{6Y!gTN*Yj&y0zFKjTVY%nNop?%uZg$;r#+ptZ8=lisw2&_C*(R- D&;naD2=387hW2RT746xA_WaE8xfTtc@6l+r>`0e3u9m@(HVuySX<=;|BODO|tErSuo44A9g^TmwVsX- 4ovZ@_x(IRUagcr5(JsKHHW2kDO^G9lbHf{nLHh>In(UQ#{1;&s3)5qwt##PmbSQ=|p3mQEJcWRHPj|JjsAXm^prlQFKYSB~2@#K(cz-cgjJgrJN4U*w$q4l gfG;IVsq%t%ukRVbJo<=EY(r6mv2vQJFBNR3`f~P${f`q9dYC#i7AdetJiH|IBS}inpMxM0Hgx1KXh|ToXcj*&q6P#=jY8xcjvrsb(;%24!ocQ_A3u_swSZntj>%sXkw4|!5%LZmfoJl_PJU$J@ypWqGyyz~W?>wUr_q`<&R#9r!WiNZHAyP}1Q4|#%SR?Wt?23FsOF3!xMnJA$sCM{W$pO}je#O^q@+P!G%dcSg_g 9SiY9=kQKyf*fErLd`GM4^CGmQMpn?`Tc}!vTNOuw8bL5|$9%G$E=BeXo=A+=;k;Zx~#_bWpMvo9MdnDOC!V7p>O2KttK@*147)c@&I1MPg$8@HT91=B%WAw +{J~A=-AMyNmNac+z!TKS=TWFwb8`KaI#1NY1hQSnP3OwoQ987i>zROe&FOj^81KD9t&e)Z97`pj87;M^P$&I(Zt%>(jU^S|*MNo84oyGk=A4kAf*PK(XZyR9(yG%+P=|8cT#ucljhCmMwtNg})nFlL`ACYoE- ~pkv{j=H9>;O`MU$>+y&5dUJRc#O>bevfKXLHLN_rfhod!I38i(SVQqkr>(8(mI#Y(Sv{T?McwK_PDMQYR1aURLW{7?Yt5oW{35u8Vjr+q&PPbvH4`~`Je8ywh@3lS@((G(!ni!EN12?NlS~B7=Jp7*(IXd@Ry3Xect!glthlB%Y3z}pkt`8r &&ToO$5cF#as}70NF53~9n(i)&&M)iL50cuc|0GhvwY-pe54pYj>6LzEcw$p^0UHe(U$zxfN3C#rj?a*ossxR;yubZ^T*TqLo- 2hg3Bi?n=9?jmw>B$3%hq}oK#3IjoTO+?6e8oS|RE=ztfecIIIX~0bpiA)Afji -(7(pZ`_$gWR2;VzBW?h#}y`AZb1!6IjTn)7>nJ1##B*t@mdevf=Kk$8Dp?p|&5;E`?a7}KfMY+p>Y#$t?^VaQMuZPy6$G;wtr&?3|rKsDgfs4+*VMP^+CZ) MAhr$KzT#;dtb&g2(s)Jija%mJps%;X=9rcq2EfxS^<R2V7He&F|3#g4Zd=$t}ONXW!w&%tPr##7h=%^pkidW58S8U#@uN7po<>eIgY`Z1oyp}gj`drX%#Gg*Zi#2P- 1A3nw#K5}QMNjN@=mbBC~e#}FBJT`qKDz0fXk+7WWU~jB&6Or KgV|LCX0ZtpGPlI%LT0Vi=?Dv=~5j4(7qPajiz^XD =X;hsLeFGF!Hho+r6Lj=mFh-77mz;sF)l*AD^1;uqdk)MMiLiZ67Wr*++LOcv)etg)- *u}W1ygEP$rxvKY2`U%+J8*m&4PN`iL;#X$f5UqY<@8PyQP4G?IzP9H4gcfEq|6Y9|D!om_evdYB(aJYFc1`TD0fmF;D)CO&Xad!rZ7$h$JFh)7Us7N&Ft;2qM6yPlG z~G`^@ATE=yvCN0*IKb|2n6*w)SH`cMSV;YXg)U;_uVrftbmM;OS#($@N4O*B@lvv6!ciG=_- K^s(ghv?NUaJfp`cWv3N3cx0VB!lEi?tZ=(W5=Df<5D6bW)@6>BQlAEOl{8+Ch*UiT0- 9F4iwLhWO_U=7WRgfZXj(xu5u!JLT+nqPED`PiHL8sWPemI~c`Tr!X*}(_V)N5#7(W)EPh<6aoQ6FXj2Ea?Hh%<>05ygIWHRO>k321S{z&3JGEg5g=cj=noC ZbsJ#w^a6vM}Kz1qy@Pgc@0)A@NM*Uy)<1Yy@)ty(JDBe&{0r{^QDsBGP6mfzxKi^~qaf)j{T$hSbVP2Cd2+i=??O$sG$yW%G~Fr! jCIDRz%3S^Rt!B8k*#@f9swK}+F_l~0`(Z6PAS- )FHdhF~NO@@y_j#x07$e%9eT0nH2&PW94ME>YfR)QlIiy_v@I6a0t@{gxHN*Wjk_h=eV`#!loO- `N`%H`)8J<@oOP_8`f$mlUzcO5%<1j^_!s;t!?pC%%>4oA}DGOmGt_Q+7zScZ=@Du2ZAF`+qDG@V}{KdlgoNN6Z1EErYS+=e1ys9GGT#_T5&ZO+fh5(y- UM2sKva70)rA_WD50)s}4nIi%V$z0HEERk^`YRoK=I7BT>aUDE;{E%$=2y){`lG- }AAAgvEF)Y$J7|TXn&$GuQv&Ymx8o!AM!Na&_XRP$;X+@788K#d%@icbDH8(+orcje&h@2onBnnX*SJdM3(+V{)u3vFoHbEq1%5^+FgcdWy^Gsq;MKT+Dvw@M?sPZvqFtV5@F}@^FwJ=zsHPbA{35@D4xc_c_g)H1VoLZwidF+V#d>0Cy(Rdv`AsvSS$_L=8k26X%LU5L9i!(YIm$6O!- +n`DyXiKq5V+^v24VaLqY<@k=l5}%%y!XK+tvq!oqxEVzTD9E9SdsL{tUktcFb$%p{7Kl4QCTw*@oB{Lv^dwtTIi1)>__2NjoBCr$|OHhyhMON<|q3- vM@yAJ&fzGY2&AlMTyf2b}*j39{CDd!nEsTkeYBogZSYidqv~&FwPC9!GhiIk((eAIDJg%Ad*?424VBZsg57n{IOzH*X7S1fi`?R_1PoY8Y__V7&>|kwGbgH )Uvu`2~8hG^l7w`#%lLCfr=(*Aj0kUD9U?`WQjy4&5Y$eMuvhW%em$%XzaYlbZ(EpAx~qb@e9Lg5{CTY6%B$YBI7OjneepNYr-#&- bzO4Q^%yq)2af5ZWFBZW^T*Jj>+$o)BF2xf;MQGdm0gp0A|PqXU()z77eSF`It82TGmEH%tb_y_6Rle9?S3%iB(- gP9{IimcJY)kz6$`Ur7TJpq4S=xVGo8hMO#~VH- cgGSld}MGi4$e=L9X_&Zu8+(fXU(+?KLlb-`9~RJrI1{w4+MqOP+IBK5N@_(cpfMO^7!NR*1+M6l|BxX)9f+SznIDIk-MTnG@cep5GhJ;4e7OM!3+^eg2- 4e5ebTjh~uM}g>l}pCTsYZDd$=?<~k)oq*!$tgf(eG0cua- X^Y2r8b@Vgj=IjQPaBV=jUPOMnBY2AZ_Tev8{4Oyo;OxJMIa)HsxB5sex!cWMxuo<{H0`1NU&xEV@z8b6mmkwA@?B9bg`hM`- NN}GXL0wfPdq*$36KdYZ*4hhM8jD-zF0x2RuQK`|UkVJ||w2N_M+6)NeX|p~}Wc~=F2auwMkF!Ee!XB#%ecFj7g0c@E1?c=q^l4La$AaVfw0xXLUK)S7lt*C XJo4((M3)iOw=UvA*WC_k5OGr@TXx9?urvZt}KPna !n8j&?(PKWpN8kX|Xw90eLajP${!-gDD7;@wsZoQ|5vj#DYupMo7CVswyR~AQHThmm#P2aW5|qT{pVeuQL7qlH(ij^w0fAadecFl0(?-x9lL^#- gZC&bP=k~TwFCk+h~_;q+C2i3- yTr;JdLeE8>P7Bb>s&DBLA`eEKr+vEae)gRZXC%RTZ8_br4C)5TO+{83*IM1d#}R+M5J^j|4o8+3iut>=9T3)KX<#iy>- q4q3F?Hs0(J1faB{%^o2gSA?UwLG%EUY5tgM{78+QfkhFPk74siM#IPBc-qY77h&^9+VeBRX@PMwleXud(`H_R(q?!5JnsA{+(ahj&Y#%tk!T_Ui`hlwgk40 aWJe~hU{OY7Ds!&X@^ii3Aolk~tOwns&+&*H7lBeNWe?6;auebZU9(8YFMmSfRAJs?`#mCR*1@P^ U@wK7yD)jl=bkiOQd$vPtOEfLPMvHP@_hGqhH1j++_RsxeL=6WbV1lg>Yq8N!e^Q+C`;%378- ZU&gx{CU!5qAS|*>ErD8SSCXxmD?l4;c7EQWTuFeoe7$dB2tgxBhA9N4u(ju- (#ranmIm=W*|Z%@)u+v0%`PV*%*&})hybNKeLAjzlF#cG%Y&i8ZxmS%O^dGW?YXbXY{NYxsdC)dIm4)(|#DGPm8Z?xjHq7l&BFZTmGz>NCKohQ=}*gB$EIes v|)%S2pfoP<9R^hdqC6&P=d2Ep=gw1=M0=LB;da1pFRJY<>p6#}w>G!EUXRW{uZQM6y|%oZllXQW`#1CQ@T@6q%^uBP~8H%$pyi3?Ie)9)V)`D8~<}- |(?U|B%xB9-}yqW6Rlz7(OOs@w3w%VUf- 6u@1XONJbHn6R5r9_sGwAgji#&Xc|8ooEDyNO~_9R8#yw^fpW^OIW4i`{rFQd9t(2gPtAG^CWss`3z4uM4fvWgi>JMv^=YN *)21y?gURXBSZE@%K8>HzKg-j2dbKI)(-;y#VV0-O@-$w>b<*KuMQJlI-RWbI#jF~A8iPSrxx>dc@kmvZFz4qkYuVwn^3XI`6mR!9!)c|$X^LosNVWSrjH~zEzk5ZyW|=clh#}(m0TBysR?9U^F~}VPah#- {K$}S%?PJ4E3QAz43WeP5g2V@%pITx6l1K47$TwsktrD%!=gk7V@|tAek_ecpf)=gi!nqrp7wgi(`J2Inff%C1!^aYsLlGcJb>CM;A!l=8Zb9&1hdD{v&UHf V!1vo^XRbzugBeFO7kE|9V#E_ZXq{s0z{%9~Q+Czi_rKO`i#_H3kU>dtPEql&&x|nN- =JAt)t|5HvSO)P?-baL&_Q*MSq=Z~sJnb9#{Iqa3KgdlVVXEWD*y&>>!$&A)`WQWYgk;l40UWK6Oi+RGBT<|t%6bGD+9QWTjWuhAefpR-KMlC~{8{b!Cr9Vc m!B3<)oKsBuAa;v$`Q##<|h}nsm7-TvqT__CKARHVYCt%gQtn+r?Kj-X;4~yf@_Ab88Ie53>#X@{fq)u;#Gk6B#RsSrjHp31kXj*#TD&n9_{1lO7a9UEgh?Lu7$ztm%Ypt_kX^=5It$M%5!0BU&YHOn3V?sD>l6jz`@U%s0JS| k7Hsfh&@ifQ^sKJb=aUg2)fEp}HGkSz75VeGW+9-1@X5ynLpeB!}v0E62kspcE$6z~=^yXNFd1K{k*RpkMke2$GI_esvM~`K=V@)P(hG-@- YWheueGE{jF#>9YxEUFJ8qL5s&_hJfz*weB3v>`EsY^?o2Rf1;Qe=}h8yG{qO%{ 6cXvkMXpW4#u%wBK0Y*tKjlOelkBZm!GFko8{J_>=0PYg6mi|e*(dEHa0)V(fQ@`(;(6Gk%8_Asrim_@oBR@Esh(Kf$>;lRXaU>TKTU08N<`kRkSMfX|Ry=5 Gjix0y9M(+Kq}Svi}igE1`1w=hoZU_2Wb&$CBv!L{`C5$5qDQJmh^OAt3h@yC)u)Tr)Q!TD)NrjMztMDnPuDScYBfpKh~Mj}2!m5rJV@iEd(1fHKh2G_F$>5f)Powf4L7e!QJnK4~%^!@X9qrNLIUU2F-s)qQ+V3jm- 5pRdWpbdlI~c!$^7|^LCyw^k5CL{^_x8$~2KJdK)k&EmI@%Yu5EK|n;jp2T;w%;l-H-x^3zBPwP3w1lN}U- Gqw!Ehj+Qh@vIr0<{jo+OG=Un;Nd%a>wLp1VEd4Pu>^g0JS|Z9L1hV;adp$z2;<2IAwSmh>RX #QAv7Qs5=(O`#o}^Y5Bo4!IB2~Yg#Cp#=&@m3}sEg9V- k`n{HJTPP(ReJ`&ACAZ1nKriqjTr%^nNiTYTP(WGpB(ZH7yqmem~#h;07%1$v 3hSejIJS`v5uOhzIgObscM_Lvh(gW|2RASIjz4EzudK&>h-5d!rQnDWzPJ^5!#ei37=*^{4(#y?d{er{=+4Km5p#Hjq4Jo)oU)T(kYj#H=&r;Zv=`&J%LlXb^RBTmcWja7v;mZ+TwNVaV9`m{`4TS4 >361}n5)oHUg7KQu>>F_jQC}|ApG{K5C7EfahyDrL@KM|ZJCrwLB3W=^{MOb>(cKvlO5-0u(yAH9{BdN=46&erm^=- _^l714*AMZul*31ggGkYVpq1u6~^a%VY8fo;%&f*7o8b7dT)TAgPR0rdaQ&3z3iSd|uGJlIujqSjkO{X_Uv3bk~wL##3`K1~z&~bS7wtHpZp67)P5JQ~CVim|AouXl6hyXMS2DI4!GzF)0o- je{{1ji!m_fwE9U%JajbvqK;soEFX9hfwl<#^^MTKu~d?$D|&{L4aBWp7v&x- y?DQ$m=Ciw2g7Fmk8_d5z=YYq@9d28W~HZr`6+@BHR|n6*?H_HZabgI8(BH@r2xq`N)rw?!~- 8pe33YSHgT8F?TF2Zf1l&ja1Mm+BAZDv8*#z`h4qH{}4fItg4(Iqy0k!5ViWvLt+V!q7%qc%&|!JA?&>wmnDC&fk?C`zm$c@i~6(?V@C#w8Z73R>rzr?lBQh q@k2(d(|})|RuS1To9McDV}AZ%P?9k}3*}KDJ1s%RHN^Nmen01@&G~6TbdeK`Pm>D;4YuV!R^8?VwT#6rHb8C8PfM4dmb6uir-<- &YOpBM9xH#Z2C`g!daFipdQ2LiMr_qm)Uu@f$7G#aF#C}uLXE+^I75V5_MB@Ho)*jy`M{Dk6Ucdl_~By~(xYJBOe)G_`GGT#nDQu%sR0F43(f|GMQxV+sq$v R1=pf%M@UI`WD;CQ^Bo}t;V}YFlhmgbM-ky8@JIVS&J2<4S)ft`5eQ|7B&lW;K#dlkmM#iZI$$PYep(&zX%JS?!USqO`m`T}^l9-iXV}aMXU)v+SkrgMN~%vQTAn6iClWYzEJ>Y4YSc!cX%IzyglIsG-@CYSaT*Ov1Aa4+GOYO- &@>K(8f2EJaVykf*^fcZnvmZkuPJ|86A@TQc|HbGA15Dl9oeph(nWF^KLTkCNo~|bK(*OHWK_nrsDa32{6ix3X(M{Hgm@aozgW=lvDo;uEN~j9JPml8wA5Id m^_W4N#pauDkVX!M9arA93SN)LD?N2a~nQdJne0Azegy)SxfKFAJ(dYWUqEAevi_5GZXQ9tP@iM4p5EKtWhLtd7Cv3LlF_T$Jl1=B|ME?a$UCKI)&gmkjbA$ o+fC}9~(9Uc_nH=Lq}%E$HbUgnewzUa%O@dHJG$QEpysTAfQIpr;(1+ v8h?b}XS7kgZ}U#WC^T(b}S88{T_iIOyeSpq~eOiQAK3@9!cXzO1D<3Gk^7RYe8#%DYr);MO_P +VpS`)24=N&Cd)^D`{Gc!HI4nqF1W5&Z%uN0gsl2BNJx4Nb`hzO^q57Tme}%9#7jgrcBYur85mR9W_?ek#YPu8NbJ9c^X^53^O- `wLc4k(^%P#+;Cbc`PPAOS_u{+^08^TqCj~9t~tU$AtgVAOPdB|z-f_$M|crWd;JogCTu1Wh^U2n^GBDrB6C6Wb4A$A9^- H{UR_#NstEt^F>8Sfo;{|c^Hb{^dwC-nWK~k#2=-WUPR3dN9+?wCOQwj_$q)(FsMW*}NmtnFX7uOE0}U- {?5>aO)>!EXBE*U&k~Nc=A9B2+eF$XpGsx3or;k7xKSF}}BZJ-=Sj|Hs4f*3ej6seh5-(2U@eYY6I|BCzGC?FLN7f8v(Wm9KFlG- =8v&&W1cChRDa7Q)bGdhm^&8jJCvLwh@7{sE?;)i3JPkR8R(n$Y|Ol2S22)n3?1Rky2x3UhsQ-^UXLst%&hsu- #+m_%TJpmci|+tX;6_kEyMUx!jK=Tlc!}g?_w`C0rP#q?=F5vT#9LM_@sghmmWx=4-9zBnFCJ8{d$R8Sf- Ndy+$F2)QlBHVGH;b@vf9%xuOqgWV22<0&bPy05t-(xy9KM74M$?Y-o__1sve=Z(>0bm+cn+7?>X~3&a6IWjs7z|p6_PRhkEu-Hf%^j<#@#6_j9}5B1VmWSQq88_mRc_jhphH&V>@nHsF%ga?J9tbvcRW_bW_4OA>NH@Gr@^dG%coBxMO^dZX~7i!Bo-nx5koj1W=_4%I&9Q*wClXxNpbCF1QQ{*@ VUv}qg`#TFBDHaa4H&qOjBNgFeviV_M@S@4n-=@AmH{NEkx0Be4OsMPfjwHVh4I8Dkb+^LEK%2@#7E-zkv~06A`LV<^$~dRGzP;*N+4)-- b^q#B#C`7r{Wq|IgcqJwXuma$raa8pjr{;#WI3x>CBldfLeWW>xha*3YuOR(ghIghbXGdYKkrCIWGM$OPBk2B*jE8`J(M~{J oN65- }Og4K2NzUWPgsxcWGGMx_;8@VSe10)VEeD;So}M<+zc^=p8i~#y- >HRj9_eW_+03zEbSfxu0?EP=5j4lj3#i4KV@1Phz>cSp;54!HG*ZA!60b)Nz4Z|Tk!W=q2~J~ndt@~-PEUFqZO9J`VK3ugSS=zCv;- DEq)Sj^;D}5&Kn>)0{Nw!|N9Ct|Uo4)Mk}E>T(+cK367*?FnU5*pG!Vek8NOHkQhkE+^bqJVgG&^wy-DnKH!Ip=SK)XO1L(h)BvcZ_o_C9Xc}LvI&HBaBlR&7Ph&U5stiq&juB0mJxGhi)fyscW52F78fjED9(jW&(Yr9t7m$757}U=gXMYSoxJHI{0Yq*n{gPh- NC- r&^Wg9^|elg}UdNRZJD8w&$cFc~zXOpVmCW#vBtp<^Rxug5z+9^*c~XZPl3r9SeaY4hkYph=s$-6JHOK9aK2;uY5- lSe2aIgJBKV@J|}0;TaMr?IK6gjkQA)Cp(6BXjKqIip- m#4IfjAt>gKQA_>>rgh!CePYaT#z2x`E9SBNtFHQ=lMG3DV8c-u?)Z{EgfE`j}jRj@y)+B61s*6zrLEa+?Py-602H_-rLBB^>FnAaP52D5bsExG{8PDMdf-B Y&c- r@0N9fateAGxjhGxus~s3HW%$I(2IaAST*)Th5fAO4tfgCj$Q` APaYmuz}sgU~9c!^}S=AUGK8i+}$raKd#Qo;Lf35d1?<#y%t=o+j=e5(ub484e=!{Io0;^D#N;QB0pEMS2{Crx7b!poxfRIg6w{ rYdKUqn?H4T!XwyEdop{!^jvGrDk0V6ZuINBEdv{hOBFTrCNR-e*#2}kaJDoh!9e)pRZ^dPy6;{^=b9>YO_Qwxk3Njt1%b0S)f*auLgA})WY-ASSlJPn?K&Y I0>7dS8UD8<%jH$T4pN0xY#;tC5z$m^NOuYPn)6EGKzn(F!M2YISU#-vI1(ruTP8TJQiS%RUhXO3MM^{8a?KqJ;5kv- $W8*+WbJK`~$AX=)rXsn9#mSgsaTq?v2B<|cA7Mm(8ZADJrnu&ir!l)2M@%0>y|JJuZ5n5QT10=wXJX7Iyp{@_fnsco3E|cwOpHTS*XhF3sBVv8(?@P$8jpc -QhG?dfia8Xnue!=Xn-0lk|n6YB4>nJ4m?dR-#TH08UasBYV`=rfEuLWh{PjmL}^-0W{(h<2g)CzM)rFwoFWp7s6C-i`)- uqBhajjqwzG}xS1LUki}LuM#IM{6K5zBNRTBzLUNz1vgeN}6K6={$MgnSa&;|q_y{4>N3u8nF+2?x%wg9MOXwH&=1)QBAI%SmH|8hM`O8kbjx^;5mR2o`=OY x2fu&Bc9QGhDK?ux~0_Q>Rl1*z;FIijvp)A*xU{ET*w@C2Ur<|RCh!yXHw$B&Qu2c{7~e{IC%8drX)JItPNr+VWQP2P4#uP=&Ejd_3;8|9$EVfd_sBsNDJkf>s7{T!QG>;#BWH5*L?BPA#?h%!C2Fb69%X1ER6>6-ts0E*d*t9g3i=nvJM-sPT- WIM$d0G+>8*j_BqFT04lidsv`0CL2yj-kswCI(1d({fH48r^VW*Z%a6QKFQCM+Z;QWzKa$Su?Es*udFo7&sbxoc=0`K?{@)AWdQ6AGN)WX3u=1fosR;ZQhk0 r=@jMH2n<2(xIr;#}Pg!nX)fk?68X{;m1G=l4N1|kdw#s#LQrG?VU%ub`|(!#+ss)O-q#HUdi{JGs8pUhADVU*t^M>1%Z(v~?Gv?v>6f- _c*y2kAFC@^{~fy>Vo3YyUxD;YuPb$- Rrqlc({7r*YIY_Vly}xiu7KV9edC$?MZX@ibA+<4k$XE>9cnA`%LvMVn(yU7aQ#qgH}EjRjGIP)Lo!dE`z}i{SH1hNuzXv~>D34i- Np=TRiQp44xJ^eGK*HuN+Rxue%- (s6k>xt+e4IEUFh>Ltd&#Re%~%n3jzw!iK0-3#g62)2O`~ks}h#=TB4AGP(0tXeTnJuCa- (^LdE`RyAUmMoXJ1M{mu*<}Wf5G>_gI!Ufml;bUgKwY)wpIzLU)kDoSrtSt_- 7DN6~Ge<~{rj2Ahj+Um8veQB*j#JMB6~xj&Se}+L6f~63&lU+u7RFAtoYq2(HQXj6iy%a2^FHkJ+F!s^B`gjYuHradg5p*+b- Gu!Hdk(qk;3#xQi`^n4VYIVSRaEP?XK%M%H8=I6IC4&?G58#~fGjD;lEkZ9@%g|i- Mru@Mc#>{!33H*x#<<^X78jOe2p7MMA;q?o6T7mIt%)J_4Sqm0jkN3w)BT%c=9SiccYsJLPL||!s2kK~gjg3V_qN5<#gpBIN^W7Ek+@j;EC{eGKl@k`ww*L!Sm??6D*vwK5cH0)QH^t`!qEQ``g +WK-1Y)~B5|o(AkCtx$a$;WXeThj8F&{Pj#IYNlL3jSWvLfy_VSX`)?P5KCN;%4eH0!qU|1Md0HY9W5|rA!7NUbO9rh; o)+x*7)pKQ#?vHf8dY*#DW0}Stxw~%dxV0cav~D7Jh7lbh#FIV8cm-@Leo- 3sLlGcXnk5e0cu4U7|;5&67^}o-lSEjPb6h6_&%Eoq@V{&av^dU+aaC@5!qTDfQ%1jA`zO&Wi8TBX`FO2|wZ91(6Tjq1!Vs!juWG>yjOkIe)nM1 qdN)4r=4PpdLM?KJ89`Sa7H(X`p^F{wO_1xqVDKP|7{BghHVX1~X2=BL3N2U@e(dQMO~3Ae{nm8TVkrj4m+GoF@Op9Zr)Esx_PEan8Yw5E@zoHR2D!^he0aT J_32dF)qJ=P~so37tu4C`^uPMd;_aWqY&LVa2mzemoBrt!2l1?8ueI(?k;)3VGTtBX&Y2A(!`zsIWZwAt@b7*89)eWXwy#Uep7SF{TGAt&zkn8!t=IC=KIXT^nzY>`l;Mn()fkJy5E&u4rlM&jWT! zONu*Z4$51wZZEzYS&rb`pi*bADO@aN4X- tKaW2N}!enPAf{GmP>P;Hbku=$H!Tq7F^D_(KKPZ#|S))!|rj$)8_oNa^bYoiBE$?Hg~Klqh_)NsLlCl#o%ePKCME>M>csH$RTRcJpOojTFH9rKq7zAidK(8 ?X>VT7!h<0v5?yA_n1eY_V)E- JPon|YBQcz8JT~KJT1H5qrmKO`f%DQ^ZBzS&SZ7xj~+l0=8w^c+HrlFw3kRcM}#>Nl#%@y+^mI#sO7Q8sv|{>M1LG9x~|Jj1jf`ghQMnYP2^a+7Tc{ckRNHi 8gC?Mc-D0aALBsTbx=)9!+m^+r)7iFV8+u5_j`;VKSE}|M+rm?rUa>- _Uw_D%+K$PRRftHlA~$xf<8^ulRugtk|hpw{M0efk{=2hI)bc)NHR0wN;FdjRC97}vm=lBTJiZ+$-w4`8~Y|zZCPJ=LpNCke6Kw$BQrl+Nlr$IgXv=aCsvpg-2JPl5sBm!iG8ZS*G-tlodhL3UbGSPtQ1T;f_*kai8gdvu%HU~)teG%=2>a+Uy*3RJh0SCUr_I{5?Ao- 5pc$wMP!pG@MLS~wyWU!!(GT%>8iXmX$LrG|h$4~#PlNa_jpq1B8a-BL_!vzQiEq#BfF7E?$KjeVKb3jei$=*9FM9cin`|M)oPzUCdASp8J8ax>`o$K;c5JFGhEY0sxg+7- 6Oa1S{zRUW;%Z~MPxMfk(J6HIDJg7xE^oI&lPh$vP+{NX)qlkKU053s%A&QwBXTWY|soti^gJIERZzAp-zJYcv`GC7BG_eU(%ETuEa^IW0 2$w@F*IsoBoSCh$gcBH`pM)+Hf$|+2AM4Cnq*?k538{TU9)J^#M%4_T=|I(#sYX64NlAB$)6WZ%blJU#Sf9Bhj9DxGht~JKxrV1rIoL_7N_wCc4~FNX(VGT) +UW$$^Vi*?K{5wG)aG~Q%O)0WIvA9sPR^|viu%PNl$|*sMA>dj3W!1v^iF;^fVcFET9S0(zs(0!e&JL9$BsV$3tmA!_xS;{3K- >eKS#X|x`Vfz2OEe5`u@7(0FxG{*vFg&H%R9~P%$kR?p#=g0$%rt`CyV~NuF6OJDXA@eh G`A2w&d@Lgow2VN|C?bEfg)zZHL@?o6fXJVS$DgvIK>^gqoPnV1>DB~d2&ColS7%`yPUPq1hd>%0{}XuHo5cFG9OK8L#A%iIVx{(boM(@9XwyI%N|Q_mt<4& %lG!7$lKEMtk16xh#7Y{89Ws}+THrL2LXDrqKci`Nm9%2$A^bHBB#N3Sp2pd#&3Ib8>^i$Y79_&d^7&(xho`{|sGS<11~Z~Y89){Ts3p%&lPzfJBh)5c(8Tr DY;YP$rFKdb5jgFVRtBcVThBzwS$4rSlRoYH7xL5imDkbgG>%4XG^JlM8dOYs9ZwaJPP@+At4*#|gS6?OBqx#LD_h~c8gMmh%w{5~d$keyX`$Rlj#dpAdbKc 6Eq$X_NuO4%mk1DDvADXl(Ik;$)hweg|0v0IJok~uOoY?*kw_C^FntuY5=mjqFJ~nZY4- ?Y@U&nL5q1laM561_4kC#RL^5Yw^AbdOT^>apjAg?>L6jjB6bFP#5J&rfWifGb+uS+YjNee7&P^{l0vylid=TW>rBeZ8cLDv$Fj}nL)q;Aqi;AwAXJgsg#EjB$ZPIO&FuNE) 5=Fz6r3aF*hT&K&pmhaeRJgp);4YHNA>=i9aZ5>_F1l86;V(an(L6erVI-{;(j6f|HsbA0L&>HM5*{&Mg%s1Q&~g{S3r5n({nV0z`Ovhc K=w)}Y5)1QP4tJB9aEi!8{SNfiZ_Wzleh|P%VrbF3;1AHyWq!EoC1RV@QS1j0;2fIxiA&JbZV$BOah52|Yg5=2TWYw7$RnHeGsIoIr#{1DooRSBqt RkS1ok*5K**N*`;Dxbe(dwv;(8kNuw<27o$fEuThaWaQ~lILSWgMJBbtRuBeiqKDL+PF1pWn t3^*B83vH!Q`CqaSwUVR3CT5i1j&M@g=VOQQAC2fH4&0XEp&b!g&K|YC`%Bb@Ix|l9$DG^f)>VkW!Lo)L`vm{#Lz>sar$92sh@!%677%0 ZC?!H{>9Av5MWeXhcZOUb$pChs8u3ROQKJMS)g{5$Sx_2I2~}Z&F)6irHfkr;j- RH3C(n0Mo||fEsZ4WTngoEjMtc6heOym>LxB`v~G9HDcThBc2B705wrQf3mz8NKby8R^AMyp20#e7c_a~O#O`h%$YOAMb3~b*&I1TWIx8iX+Y)ke@LpiPL>M FIeipN1&#DEPM0?mG=F4`1Z9BK1g4MifSLfF1|tBqc#=pVcp5EohKQ&IDy~5^5)>BFnV?14h)^}xNdYxThp2IqL{8rEksnXX3a0@No<@PwfEiDdfzu!xo(5u o8V5TB!ssC^cp48uWK4?&i^*6%e#-`^k+>q!hL6eo`Qt$~0$U^zqd(9fi$5H6B%;O*s6nZSTK0|&1bO{gLT9oCgtAx~6J14;VrruuvLXSsqvOZ-rI;88=7Po (`s*- !WEEWtb!%*nkMh$;5FkHhr1NJaht%cJpDh(M*iR(h!#E;tCLB;p&VHmYMA$4JqdAX~43S{Tb#<>a>KE{vxz#>|rxZS=SIcd=%@{fRdp0xLKQQh8mQMsWCG8^Jb{QqU;d0K!(U1qL#+^v9b`g0y5NOFtx%wvY7e&ANa?Q6o0JP;UkF Y)1p0$GqCv?1Zt3&^f)R;E!M- BJb&c%FovZ5SS*_BG6*6xb=nBNBP>KsviKQ~zyqp2u^(ZkKgLi*X7*zx?8lk>NRCjGbrfOITPNT@a?c)f&>s_Ls8L)*NQ!F)en^bvBQP*T0JS#~fEpx2)Bur#611Y1D)xXQTtvXbSAq4q3;hkl{nfngrNcS~U@g8iPMpDm;yvp_X#|$PhR~3cSwD=+7~HOq4j oOXmkxK0gQ$`lBJW;BHOGBCANpN8Y>{DBr@EJ#a>T`Y4t-!$1-FkS-OpfKX5=ff}>rV|2=OJenpzdSnFDAh4!Mn?CX;gFfG(K{=%U!c8A*^L!L{qNHP7#R%)V_ZKP=k0xP0~#SvaM#|JEl*cf72$atcDc18bl)xVhrt$I}1ssg?85ql(h>T!}#0Fh6S{OeKsQvI5Pzx<>K&owdBx*?%YJzwg@D3k2?fFIVw0Q0#znih7KY#5OSsZT0b@ ^k7wrYWejV&CMTGd_0j1HAoSnmIhJF$$u1e#{y;sBuG#o*@HoYRcfKR*NNe@(3UM;qs9TJaRh^MlSEkfkK(%P0P5C@Pc6l#GKkt2ySV- #u(w*1VHS{Oqly@g0_d0IS)Key>4uPHw;!D(U%Y7*AP6AlF>A!b1bLTJ_?(xE?VhFUt+bq15HXwOGZ7h~>z4cH}WDRR^*(_9xNxvsTc3m- jZBS41tYbV#Pg;7L~jvsjxYO_Esv`6DLeVip~86|2oi>z6qHdTpQe9kq$Kuwq(!m3Y$!0F?t47FUoSWsb$mY?uAZHXH2YSdsMoC+G+qycxAMrQJh+F})9%AZ oC#_o#61gM4EVue#gVkK&ktj8myM-8aG{;kK+mbX*1NqgRTiNYDw@k- X5(`zsGt*u0_zaB;8ro=8WA!1Qw}0MEIMt673#YGOmFMP7AXT0aj1`P|0h`&9&ptK#yQbNk?XR1gv(=5- R#wY>v%Wu5ALwk=p53WcT5Yb*_S>^V+nn8A-?yvt<@)S$yE|{xZ>#@r@R!fNtj_nV8~l~)!)||ess87>+5O8`dh)6N;Qp}NwEAb4S64f=F>iPO`+xoa?b- Hjb9TNvH0%BS*{9|C=d-PSJNOoEmY*B*OKbd%&Drg8wYfa3&zjBlaCMFE>r- >KYi^pGPq;(kAN*kd;O%a^Z_f8^|NZT+cl!6#XSg}L+wLyfSL!qFRRw7q_2qVVwmj^ww>wqU^xt28QCnPlhMUj1`kCeKzFULqqFE2%U9}>zW&Bt$B(Oa`Dxt@7xnCN+3xk5zi7@^?dprVobS}Dx2ydU?*+U@_zPG15AV*@ZFJR LsXwR%$G6fp>&vst-RkOEy*a!C+=5>(_YeLaUbOb?0=IwgU7ug@&*00n>+PL-6Y8q$8~vs~H_h!?^QGC{>s44@Hu#- -*O$0lmyKG?v-RpTF83Zkfx7~~$KTs8*Pr#@RWH;w%bhxrsP8WEvYu_Wn=h;FT3wp6?XCGtsx+*+d- +_gJifMC6usnXpTMh+D{R*NTet2XuGJ4T>Qm{DV6M`mr{~6g#2Z|LFZLpxpC4L%75tNNvsyQ={2i=5<12IH?RI!w)caoRtE8^p!Cbz(>&85DtKHens@>zCac _S6Nqxja>sMJla{BMCHtk`(SO2H(&E?rYwx659TU5)xy-|M%FECz0eJ}2o_ypqx- <+vyZl7jc>c8k49q)7X(bekR=oQ9~`VKFX`T`dZH^){zbL#EmFTXOkj=A~NTUU#s7WT%h?82|?cDHGE!{^^TSn7duPo7#}{h*w$^w+DK+ilyf^ks54@n&^5`UTVv;QG2{{kT0p+~9qL|Lt;#TMWKeT$4NX8S&;- +tt;|et7js&zlQXZO^XM!)PA9^L2yAR{XnoW!%dCe5wB7e2aI#+ALc2!Z*0V-Zsng##{;gc0T<>?Q42_- l}c(mHK0Q{47^n{XWhQ=AG@YSIs59_p|M#zM|{ZrM;8C>(+a7x!anLa)UeC#lr_({rPkCchxStQ7^9!8dnFsIex+yzOz4ce|E9DaI3Emo$9_+Z{_a#- v4KOhqhqg#-#TLbuZhS41Y!)<9A>1&fMyM#G~4|U-Uot$Ep1vhn2eLmiVad*UdY;`*3A9dP{B3+}- f%>=SN9TmNVA%3d{UQE`tw!{@NYt7o4^T>p>FetCXy *JZukBD;+=Zc7S{vcy}H}g2Bf$3t-8RtnEj=~4{^RdtS{6%P>XffZ6)?~w;SAmF8BH-T<-Knr=RhIsyXhV(l`3S)^1gn_INM48_N9ns{QP5_~u- l0vfY7*!$!9aHAi3TMpbay4vf?;Wi{)2=}UHt3TK+vU#=JwfcAb@c5B%In@n@g+p&FddqZmNd3bOPgrUzZML^=JO17;sH*PziVv=- +U;fzTr$6lW<@Z+m0<(Ewv5Ev%9ePaPN2OL+;#t>ZNYp=IFTeii`lPVLt;7^;w}|icL G$v`oy7DntIvu5tM-V^LEWIXwCHE~9i9ufo9)fL`fGdjN9@k5_vE|P#@yeyg3b^^_1os@YA!EV=PN81=AfaM$2_Y3nyl8ir{ZO~*qVo3Jyz;j{M;=pzMNh- d`)%xt!@rC_7(A=X)dt9s&i7i-`={KuXmx9ez4WErO#ozGk=nGEBqTaoC;A^PjX&)y%Jlx){@7=BGZw>r0wGnpb16`kV=~ZXxy(&m*{q$9}^- g`vrqx?BE~{EmTh{+^P=D_dOTl`jPDA?WpikLqWxsLEz_0$Gk5gNG3iKZ_Cs@6(y(5A>{^QfBEAk32nR>v!9M&7N!Mdfjw^;AAi#w<}- >$kp)~VY%HC60?agBD4*KS7%^~l0x58Fq^-puBT&usY?ry- N(bC=%YJI<~zTn|UJxp$a|9n`l^|AYvI#uIQSDljg_piE+#q7W4Bs{pLxK-ipf_qqdesFi~H?O+`!Od- >t~q{g_qhG2_o+K|GqtAS(#zF5`r@mqMd=P=_+xmAS#P(W-75dm?9^vfpBJkHeyO*HAI)vD- Czq`f!1}lfGi=2Hx- 8*5dEpbKOJX2BT`(jsD0FR@^M_H~r4rEaZhcYOL0#Agl96vsd@r- Ex1f?f|_Jw5u!KF5CL5ZcZ#2*Q;B6_x|$YXWQ#02b;`(yNx{#?qC~rsM@!_lZdn$T&MlE- zeiMVplro&{z0?Tr{^hLfY7Eve!}HZdK!K4r2PS*y0bKsS~z2m6&Dr4caewT}=7xPONIJZM$u2nLbLX{rh6on)AkL?^f``R@Do&dDyapO^)rpY`>s3wmY){; 2ox`hpLPEyzEQYPwi**+Uh{BudUwF)tk7|JG?E5wh-udst=L2&ow2jZ8mLJ_tjQ%WA;N_Z(aX;)$3D(haGIk?a}M7G2Nl>U%&BP)?d^ztNZpsy)AXW;Wl!EC E=+bw90n|M!n?y`MK<@X&jOj|HBQ?5DRU@a4sgdaNFcTJYdrP>}Z>IUOGB=>wVBF4*i|1RWU+lz2eTsK@>- 5hc_`>*j>pK=VY5g|O@rbwcU5I~VvB9}&T7U4|Y___#>bAw4mOicR$Zi%|C2N$(>o4WN-yVCW>o+EH*wA}Z*sBU@HE_CkylRp;gdDY$ZYK>2&ZtL(bwYcM*A9ij )V~(La{jbZNUQoXc>w~qb!fVqFE&XEZ3x!X)@8!SoZP4G%G1S})>Qx_3>~{JbVP+7A+up#>e>VLCT?sJ1dVAQl2R%kJGhOw#`tDP$VcV!SSKS}I@?dtxYkkY N*ak1vQD$YE&vmo!7E>Ml)WNtIC7Q;Lcx_KScUe%oHnT0>!{P4H{;{&8lSNiGg_2RGmxUtoBZe>e7UIw+FV`)~mr@Le N757s8TWdzx`Wag4NrXK^s$VT#zY8sovomVrTCRVUU)H-&0jvfpg5a;>q;Rv$tw9Bwa- I&|NgrU0K9b*#BxuFX36?&YuQ5QYo<s!5Y-}>(4- &T4{#@$QZGgn)`q28z+vr(@U4QBCSa!hL9+nrj-q}V3o9qA4b;@T3<1tSwj?Y^=ky- 1%q|c03&Gu@yyuG%2nyyG)|98GU;CRIS5j__%oA5=i6s$0Zg8dvhvi`YlmTpHZ>)iZnDpXKA1w&Ayx@X`A=OZ)oa@jig2Hj>TtgczJyXv;{o5T5a4*~IOY-hlBEbhiV&9CO|n1>Gcmfo1swLDl#HB@ 1C(%z)P%%apgGjkez952@gL~GS%-rrkr*tgcuuU|iZY{k7|z0%E$zJ1z5d#j#a^@qEo>+r1Z2C=xgnPu}$$Di)Z$9(W8+ex$e0|- s6e4+P9kOd3e@ojVO@GQLmdV$>1H#eUUv}jxXuJxMw=^y@+Tc$s$zo~mTjdj?w=K1=ZqwMbRu#*_;NkK&TXSJnc- 3A%cyXM_yAS58B$g&~GudC%Mb*6vym@x&Q#Uwr)jzkyht_T2c0lPd8(fg@216;mZm;k+)$?LOm2M>EZg1Un*kS5DevkLsKmCLElddhQ$!abt3$LI5vL!V7^qVFqt0i*H1Ie64W~e- Va``)gQ311~!togKpLRxxrfF_Ga8h4rXp`R@8R||Jm3vnQId}E<(H1_nt)%+`{Q!z~-rIGkXJtu0wM- (P3jRlsmnA=#c!~5+Cx_aQ3A>rTGKB!GyZY&v&Xqngv4?qv%+Mg5rFWy}e80J45MtkwwMob{V7T#@e|Y~<~=dUJRdd4M#VVfwi}3=BZlVZmGOuvM}3ZQ0uEcd3U5W~$U1wLf?QyI!WNYUV-G2Npf`N94tBFrSte- 9i4h>Qt~@UvzI;y%*EE`dzF)Ytd+!eSUEr{2~5#_#z%7wk728=ojzIM~s94Ffll?ss|0XanqyN&fT10umb2aL{|oSwbtG@`z1c{hJf%2%clN+J&N3Z*&13xc SP-N0Q%kQw!w_%Zq&kkY1(d_@!1}Gm$&M^XfAv;#yN*a3qIqqQ|;_- @4*K~y*<2sxOV 92ESqRG$aj$~C_(^`0M|DX|DckO@mKy{<{LI5`N_IzZc*lGHg 6jUc8i0p5P1)gLQ~kn)!DUt((`EP0o9~~5E8NJvPcYLj(p+BJt)ss;?;7=f&K=_GaTtx&tGBGqp~#%ZX8p67M3{~AA?WHqtQnc&s&&wSeJ%}7@AgkzD|MGPu 6`TB+vRqRyeU1>L>N?k+^!7%(F3jKrS{t^-M(0WaPJ>ab5R>Tky?8=&=9Ho8(HgHsyByIckZh78??G- &9%5ZY##Q~gWFScJ{s4EAJj=|XP?rG<*gpCtjv*W@oj%rV-a`#7gWX6qw~uLBt3T|*X_P*WcK~h%-|<3G(>GCA?8!52jad5pRYDw)P~a<+PpuFi45M*6v$lX`>qw%A&NitT(`%k`1MD9(DJgTDvX6n3h_lju`5pG- N0}VK0+X~h0%!t+R*VvylICJhMvhEGI)%?#@qfQ5^SyX2SyU*c<*V{Ebd(T{@%iZ>701_H-%PgKh-#cQ1- rljDGlw4B={shGMYkUe_WvD;b+51rU0pS9>2&Y9v}`}!L=PZ3?9}j1$DhOk(K}}JhikrKbdDcj^n{_xU&7{ bYzSYN?m4E2DG*K_rJ;WfjlO(d=bqZR#hEhEej@7vCaT3hv$W(5R^=!5dMzLc*wL{=DyjyNQ_fBGGOFUcP2?)tyTSIKdmH58Hgc`T|*y`g+6nGR@e+qBo)g_L )H6y_G17=g~?^J{T(3=$L{nRqiuo1#3?#>Vz>|OHIEMD*Bj(F2CUJyOCC^Pp{YJiO^*icSD1AaSHR+X9hTW9#t5^X8)Q+xv3A(`Q8gp!ZhskNR+kaP=c7RP!Rclb3o(^@!#YVY7~k) Z)+DP3i69fv6@rnJT2-hu)A|eKGY6#&1P_Y=di~=a;zk_{H_Z6HCOmT!A5>VX$fU7xdG;tyHQx#_!ER2*Gka5K|vw- J$HgGbwf#YfWKOcR;h!*8}Hdj_r*tO1~VMZdv~&wP@IO^pEe~`Q!VaZqy_`)d(DoXx#)(pd-nDF(U;BLOnOd_yJLR}VwUuPB9XP)V$F&TPwx7%-D<82- p%@xxhGLC7cv_A#p19zw5F`v8|ceJYE_F*#@;@0cBbG9+O@h5&F3`dS_7GOWU0Y!rncFox(5!oUdw?`s9L8BJLl@?Rk#uD{UmSW&s)E1*0(s*- (WjpYp0=2>4&FJn_6CAJ=a8|^{v6%aKZecU0;dS-X6I89{FJ7S~{Zq(h>_y4|~4DiqNA@;8EUefSM?vk6Bj^hQ>n=- u!JKi&U%z~=Y&o8~O3zp*>q?jPv@TI6$Ypd&|kzD{3}abuX%yU*CM?)HuvXjK?`9&o)pw62B0- rbQj&wU%Ddb?ZmOyfOjw;wx?JlrCf;5t@6SH&ytYZ~$}Zyz>{f4Ag=8@<`shRmID?F0``#%4~c>(8g>pXj10M}jTvcpQmG!ixbkcUQu+qR15!tez}x1XXBfJ{zBy;>k%MN?Tnyi>J}laUqb|f&^5qY#L$pqn9X;_eEH!4|sL9NqdE2v$ftpUJYqTy5P9RqdPeoRyyv&9gz%a1RjQb2*Y()wWAo>t;HS5ag1j~BX(>4Xrv$JZS+WFo`NaO}vpm0ltP$)ka@VSv*_Y- (ZS)kP(MJq7jOzF0Zbs3xJvhUDGQHKSGP^SQ#VhN`{hoRqW+Y*7n@(toAui0gY-eg&7W2~T}<(J>>Bz8;y>&- N_{N#R9WKq=F3Xmjrz3EwV2oHX50G-!^H|WF04^_2C-MPBQf^HJ%iKs%GDZoeeel?Ob9SbX$~wM?lB|*t52miM0LtswS79p(iFy}I)$jcK- W>D_3Q`%xV7nzV@tyi!(Q77`Si`?31@bLOQxP7;f?f!l}oL8Q2Z|^NR8dvu1;DBd_Y0F(V`|E&7gZ%c;3B3SFFn3G&=YxS- J27pqfx$Ossmb~;=vK7@1k_Ww9gyWc`ebl5&FIqQ3{j>FQ+#d)@7cT`i?k0uGHQrB7N{F4sgs|+Nbhirv2@6!=HvGtA4BHR0A+iRzjKWnx~CSu`Fe2TJD6&? )#m_pD%$%3>`s7P<7VjgJu$SU_FQaD)#@{1%(I6~cuVeMYD|mf+;vLj>;q3=Vcrz2*?8#0P5EpwLd;j- OC_rR)67@*kFacy_g2uFc)`#jxIeQq0QD+(&bNaTdrVtx_8n&$%dF~gP%YCF!gr6Ey5L`WGGOe#q2tZ`R+oI4ReFXX{2%9dzSol)4AKLgv9cizfJS;Fo5t 4zBtLz1^wOh&*FY`8VZYbz=s!Zx)>l!sv$alzG5T&7odKk9SaKB7bgm91`pvu~Mr(WVLzfByqOWe56}-ldjGOeV{#mcR=Jo-up=t$D97%_J{5H=iW=A5XGTy 8m(R8CUk3C^4_V(51o25k)N+NTC34FSJ1b4+wG7@ntj)sa_PS3Qt!`Ks9Z99!#BQJ7zj9?lxqygas~m=H-c9SeQ5?f>~^WQy?cZmIxB;&lBQv3}w|OyYFZMjfZg%Y0)$>eID!=P1Rzr-BBHhi|XmySpL4jYYa4PG;MfLwtf?OV1}2@;&^sv^Bux_d- y?Fn7XMi9P#%C0eEM|fIQ(Id)Y$;8gSOf0)1}wqj23@n|r~^S|0dmJT|_t- N>?6L3(?{Lr6xCfYedrVz<0Ax8B1(uYaKJn`>BX{LR||%Y4Yo^Kx9R0W$lIK7V`E^~U;;_UH;6fW8`iFt*2YHNAvWB $!sx-nn=d&<6oNYYrbfcaLGYWE(IHU;Kxgbu%G?8QJCEEQ3bg!t9{$3k}CmVBg7+#~NK )R1=H@JTwNpu5!UAwJf#&EVT4z{FUEN^h0Vv~IvGfO$|OR*G8M++Zy#5H- m1*E}kc^X&t$KCvE?TZkEEI&HsS4eAns!x3`AScjG61OZ{09#n*@`?ny%uxPO+1z9e_}AoWeX++ba!aE{*e!D8(_uywh=*|xXlRD`^;UYFe4r$Khf9j 83bJ;sux3@(5}F$?PTFj+%h!Kce{RD9p^{i0v(`<$9xqdRmf^U?L)Z%nH2-WhmG+k-q=*7d!po;`IQauZF1;9nSToNa3?4R-oY?70K!lY7rGz}- dK`bIrQyY92u2S+~>7dXGWu%01pk9?!HjQ*Wt{7P=NxRW1lY%76B=svpbZt`6N&E`t)Q@B|EWc}|*!qgi=PeNcJUGpHgesS4Jr0^1%KXtkKwYS0i=GWV`J(z avWM9=$O|9GUz8K(x>5ew7Hi>eZQ*T`C%%ysphW}_-J0Q_>E8x!1Gp@2I@gs>J- z+Zdce9X4$W%cBiW$T4?SiaMebG)8#hC=U;hVf%wP^F- W0)n)K9B_s%EM&WIhMz7zwN0KGYxEUiPpER9By?Gsd|+CfIkUXM3G~8eePK=}_#68vArz%rq2?GlT=uHSlb#- rCmUmzvDn5%wPt^>N9*@AqhTUgCPdW*y;QPX{qTTSqi>-cEgPmi0IDgiF6QFMD1)eMXi$ZEkNK6HPDFP4|RCW>- >Fr$Ql?o59)N9XV-m6dtsjnoJ!s&;vP=)95akoW9jXS__%u4X85MTOM)Qa^M{td=;$!qfVyKeJro)OWf5{K}CLUO7d++lkHWw=W|zJ- #7jo~xCzlptfv)jJif^lfix5j`SZhbCb2hc~k!jrETZ<}O+4Jy18?c#C&_jmZ1k8yf{&7k_^CglyZ1zHoAXujgKf5or>))?(pV*DGaK`+>#- u_mvau!_)b?7!Y_Z`EPVqS46fw6Lch?HB_9!!zn^jk@yf- ^V+1MAeuDVkyI0#P_VdL(LCg_Ze#gw=5$az@xAowpc<|_o)2g(5}v3sbYj6hrW`Ur{5%#Z^z9nxI m^Z?l(bUS8f!S*5m|F;N$&eyF0R4r&IZGa7cbW<^>pij>arcqe419hb4S{IER%e2y0s67m!+F|&Bkuq$cuAz*szY#mvr0p- *ykNedj=AGkjvL9;q8Y+MKg1BFIN{=5G%AZaZcGF48xTb#2<7)Xd`^IgWVtnm;Au5Ecgn{YIeE8a+)u#<8nj6@TWkjt~!)r(2Z%u6;FBqV- 7nh$T6U(5M!?6XeC#9DlK^)oKtcnE7GALSGp&xK{P*qu^yv5z;gAQI+yn)iCV`p!UJ)`g_y>qzgS177yl9dKm|7b9XF6gRIL}t}^K- P;b0;^keF3H@wj|qIxagQeQWHF{|;Pa2Vx7FBE6F)YJa0t~SUlSUPdq5B~i7)4<7V- 9O;DgM@WC)Z)gD_2kGtcRfoO^DGba#Y;09_D*CT$M4y@hvcVJknOl#gy 7lh=>8w*bX?$O6VVza_#-zR{HM98U!jEnqK2- W}u3M@8u}!yBt*3l^x_3HZOAl&4uh#2!h!L97;ohmLZ9Aitd1ueV#%{#4Hl4wixlZ;}si#}LBa_>a{E^qX#+aG&y9YI4tF5VgJ7VS&_)ib#5(7<4?- =KHb0<16{rfW}i?03bFRI?}+QTO- =0>M(+iP!MIN0h|1_W6@81)Tq#KWy2{NR?PIq!%JI@3?zdG%NeT5N*U6SH(Z<>;MlRP?xfbM~|4*?Cft`c(hwQRn5}sVE*08H_Kh- @ku0vfi@}Pr3P54?(*zEZ+adu;T$e6i=b1;G1>&S=- KB{Hq)7X(EM(Z~ES^yN%`l4eaR#7`~zLS!G6pW1yw?r2WAkhyQ4*;u9_rmu}FEW@PH5a(21YSnMG==7G3PjZT0r+M{cRtMwV~Sh;F3ec3f>b*l1(@E9`{^V^Zh5uIrj36gz 3WBSQ5BL~2t;CT4ed^smruXRFP%YWw$3Y-UZjhHhQ_@3+^sAev)w@AT-82KtxJ&58Q0KT-eOyLoimf#- fZxYpi{NF`nBmozPd(KMI^&>T2@J^e)aogHAShNIQ&3iJ)$YZ0j}KRqWz+&uk0w7tZwPxVQrP3>;fouQzZR@jN9b@{W0A`d7#)sv={qZN_*CHHDQF9Nq6IyZ UK)88#W+ndCnq~EV`=IjVbPNQrtmi9@- gJ7dNTi#Mo;O*DS~+9F5$KkBm6qp0^qD=U3axitP&da;}!+gVHD{;xM4!`pXa14n>)J2*){uDF&> VGpj|E~Zz^9d$B- 9e(tSrD5rzHc}mWjgVWnuV4COW+&_MQVm@MTR%vC+YORkwY%sq#{-Lha}IopzP4+wwDrATvfj&SZtu{FY+!10Zh0P-C+AnL_Y(dTZn?S}D}LFV- B&+JEA1#eG$+&X5B7|=HqBeBC+SZqcr^sxTnyk)FClo_x!2>Fuhr~3y8(9?iKh58@fpM_TC3dV|Dmne!>$(hb$dog@_- XOq$)^c7aYiY-WY-|g^^lg~zXvHC?W^ikl$DhE>Y_ng`SDm0#)6_K5WMP+~?~~PVTr78=)lV1`C{- s1BP~4bAO@!QY~3O}q^R2y$Iw(;gN>i=djliF_HdBhSCFDwRHj(eto6t`BtGn0d1Jsb$>-4?yS-f4G6ZS!@D&BVEZ9xMdnAE-a|S~q+(?>V;lRc{XcS&^|dF- C^_ZRc3`_J=2VDmFi*cN}&w`3>Ixro1`1)39W`ABq9`nx|zu0KvTlAdWhE&$x5d- CkY}l6*ST$JXL2*6PU30JaDc^rS6Avc5m@n{WkV+iZ6$uj1FAd)Eys%+XDsg3P=E*X- @kgWM9JeT0fPALzIMyZxXK90;@C_lX?3uj#WRI~#U9o!;6T4{y#6<n8I`|+&ZV2sf#- #q9|^Hq0>zz$EfNG%eT2NEEI0N#N;P+ja+mwOG5Y?fL8rr*4sXPM>|DQG?~;Ooxa(EQhdILU%)oj+5fNs8`?r!SwK$L3Pl!Bb_5kf1@;OW@^c;c6qRdgYWI!RD0@NHg{%bg_p$M!+N4`=x|+g?{E=^4P5BQy3- eWM0VwD%i6v-x;d_QNnGk`Xg-0FR}CFWjMbQha~n*;8`$IYef- |J$4l=;dhFflBJ48y1N(1x*5usL^d1Z1EWyKVm3rjnb{%b(ptnFQQ*BSHxiNfCG|ka;c)Z7#ZfK@$`!toyItJ^+omJzR)2iJV0&Ks*KV<)?F>o+Hh$d@2v68 FR- 3aOiH%y<5VOYygmJee{*%^rF9m4vudX0r1o_is_*XeeY4DEEfwOHiquX}#%YaP|J>v1aIp~LR3i(2Z`+y)J_7)!@99t_m=tx<`mg?3v@X9BcE8@Urgjg) A7u;- opz>h7mVfjik5Ycf(AO2Zrdj)iX5a{t|2TwjnZT&&zfgJZ4Psm!4j4*$WoaM#eZ+BfFqIk9IV5eCXL_#&CCydS@Fuhu6(~&(^o^65u$yy}9WxW{l}H+&6YCh ?Z)7>Y78whimJ2W2mdZZsek4G-;wco|LWQtNKAz^Y8G6bI74ni?=;CiKT829do;- MNDsb(ZyhC&VZq{xU3Uo7ugy!aQhtG2tylqz0$tNu!mb+p!RpGjioDDvvwz3Zrx0BU_bWBK0{g- ij=LvE$rtA`u!Vfs&zlLow&{jxmc;PgGK6Mix;fzcn6yE)0;=U>j7WA5vgkZ+b(L+=luAEG?vXn8o$GH?K9rBZZ0IUZ=9jkFrA`Fuiy7wl%Th48{8}pxtpuK cZ}}#6{C{V$9he5y7b2I?re!#aua`O{iyZN;q2knK){4#8TEXfyHSAJi1uhF!GqH7^ok_T8cL?W2jUx14<@^#@N!1H9UBj(SJC)zB2Vd*vbCiVN{$|CrZR3SA^TvaKE9A6dG2Ou!vERPTFuaK^OvQ h&&2C`M*N*{AM#&dCPj;d&UIIeeme5s5Eyc%~N*!5#pwwgA1ZQQvw)_w#BVH@oN*&6a`@3^_bXZefC)@Z!R=>waLD~%7Dq36NT==B}Y= h*yW6y8<~V5*zJBAjcRRoQn2S3_r7H)OYSmj|}FT5hW?rS!9^yF^=c>^^B&n;y{Kb&fRMru29Vw8Vw~`mg``KZ+*^pa """ def _load_embedded_data(): compact = re.sub(r"\s+", "", _EMBEDDED_DATA_B85) raw = zlib.decompress(base64.b85decode(compact.encode("ascii"))) return json.loads(raw.decode("utf-8")) _DATA = _load_embedded_data() _CANDIDATES = tuple(_DATA["candidates"]) _MATRIX = _DATA["matrix"] _OPTIONS = _DATA["options"] def _normalize_text(value): """Normalize GPT formatting differences without changing semantic words.""" if value is None: return "" s = unicodedata.normalize("NFKC", str(value)) s = ( s.replace("\u2018", "'") .replace("\u2019", "'") .replace("\u201c", '"') .replace("\u201d", '"') .replace("\u2013", "-") .replace("\u2014", "-") .replace("\u00a0", " ") ) s = s.casefold() s = re.sub(r"\s+", " ", s).strip() # Make harmless output punctuation differences irrelevant. s = s.strip(" \t\r\n\"'`*_-") s = re.sub(r"[.]+$", "", s).strip() return s def _extract_numbered_blocks(text): """ Parse outputs such as: 11. chosen answer 12) chosen answer Q13: chosen answer Question 14 - chosen answer Multi-line answers are supported. """ pattern = re.compile( r""" ^[ \t]* (?:(?:Q|Question)[ \t]*)? (?P\d{1,3}) [ \t]* [\.\)\:\-] [ \t]* (?P.*?) (?= ^[ \t]* (?:(?:Q|Question)[ \t]*)? \d{1,3} [ \t]* [\.\)\:\-] | \Z ) """, re.MULTILINE | re.DOTALL | re.VERBOSE, ) found = {} for match in pattern.finditer(text or ""): q = int(match.group("num")) if 1 <= q <= 110: found[q] = match.group("body").strip() return found def _explicit_letter(body): """Accept compact outputs such as '(A)', 'A)', 'A:', '[A]'.""" if not body: return None patterns = ( r"^\s*\(([ABCDabcd])\)(?:\s+|$)", r"^\s*\[([ABCDabcd])\](?:\s+|$)", r"^\s*([ABCDabcd])\s*[\)\.\:\-](?:\s+|$)", r"^\s*([ABCDabcd])\s*$", ) for pattern in patterns: m = re.search(pattern, body) if m: return m.group(1).upper() return None def _answer_letter(question_number, body): """ Resolve a question body to A/B/C/D. Resolution order: 1. Explicit answer letter. 2. Exact normalized option-text equality. 3. Exact option text occurring inside the body. 4. Conservative fuzzy matching for minor GPT punctuation/wording drift. """ qkey = str(question_number) option_map = _OPTIONS.get(qkey) if not option_map: return None, "unknown-question" letter = _explicit_letter(body) if letter in option_map: return letter, "explicit-letter" norm_body = _normalize_text(body) if not norm_body: return None, "empty" norm_options = { letter: _normalize_text(text) for letter, text in option_map.items() } # Exact normalized equality. for letter, norm_option in norm_options.items(): if norm_body == norm_option: return letter, "exact-text" # GPT occasionally returns "(B) full option text" even though the explicit # prefix formatting varies. Also allow the option to appear within a longer # sentence. for letter, norm_option in norm_options.items(): if norm_option and norm_option in norm_body: return letter, "contained-text" # Conservative fuzzy fallback. This is intentionally not permissive: # a wrong option is worse than reporting one question as unparsed. similarities = [] for letter, norm_option in norm_options.items(): ratio = difflib.SequenceMatcher( None, norm_body, norm_option, autojunk=False, ).ratio() similarities.append((ratio, letter)) similarities.sort(reverse=True) best_ratio, best_letter = similarities[0] second_ratio = similarities[1][0] if best_ratio >= 0.78 and (best_ratio - second_ratio) >= 0.035: return best_letter, "fuzzy-text" return None, "unresolved" def _parse_answers(text): blocks = _extract_numbered_blocks(text) answers = {} methods = {} unresolved = {} # Primary parse from numbered response blocks. for q in range(11, 111): body = blocks.get(q) if body is None: unresolved[q] = "missing" continue letter, method = _answer_letter(q, body) if letter is None: unresolved[q] = method else: answers[q] = letter methods[q] = method # Extra fallback for compact lines that did not fit the main block parser: # "11 A", "Q11 = B", "11 -> C". if len(answers) < 100: compact_pattern = re.compile( r"(?mi)^\s*(?:Q(?:uestion)?\s*)?(\d{1,3})\s*(?:=|->|:)?\s*[\(\[]?([ABCD])[\)\]]?\s*$" ) for match in compact_pattern.finditer(text or ""): q = int(match.group(1)) if 11 <= q <= 110 and q not in answers: answers[q] = match.group(2).upper() methods[q] = "compact-letter" unresolved.pop(q, None) return answers, methods, unresolved def _score_answers(answers): scores = {candidate: 0 for candidate in _CANDIDATES} for q, letter in answers.items(): row = _MATRIX.get("Q{}".format(q), {}).get(letter) if not row: continue for candidate in _CANDIDATES: scores[candidate] += int(row[candidate]) # Stable deterministic tie break: score descending, then candidate name. ranked = sorted( scores.items(), key=lambda item: (-item[1], item[0].casefold()), ) return ranked def _fit_percent(score, parsed_count): """ Normalized fit index for the amount of evidence actually parsed. Not a probability. With 100 questions: score -400 -> 0% score 0 -> 50% score +400 -> 100% """ if parsed_count <= 0: return 0.0 max_abs = 4 * parsed_count return ((score + max_abs) / (2.0 * max_abs)) * 100.0 class DereVisionScorer: @classmethod def INPUT_TYPES(cls): return { "required": { "vision_result": ( "STRING", { "multiline": True, "default": "", }, ), } } RETURN_TYPES = ("STRING", "STRING", "STRING") RETURN_NAMES = ("dere", "top_3_deres", "scoring_summary") FUNCTION = "score_dere" CATEGORY = "Salia/Dere" def score_dere(self, vision_result): answers, methods, unresolved = _parse_answers(vision_result) parsed_count = len(answers) if parsed_count == 0: return ( "", "", ( "ERROR: Parsed 0/100 scored answers (Q11-Q110).\n" "Expected the numbered output produced by the Dere Vision " "questionnaire prompt." ), ) ranked = _score_answers(answers) top3 = ranked[:3] dere = top3[0][0] top_3_deres = ", ".join(name for name, _ in top3) top1_score = top3[0][1] top2_score = top3[1][1] if len(top3) > 1 else top1_score margin = top1_score - top2_score letter_counts = Counter(answers.values()) method_counts = Counter(methods.values()) lines = [ "Dere scoring summary", "====================", "Parsed scored answers: {}/100".format(parsed_count), "Answer letters: A={} | B={} | C={} | D={}".format( letter_counts.get("A", 0), letter_counts.get("B", 0), letter_counts.get("C", 0), letter_counts.get("D", 0), ), "Parse methods: {}".format( ", ".join( "{}={}".format(name, count) for name, count in sorted(method_counts.items()) ) or "none" ), "", "TOP 3", ] for index, (name, score) in enumerate(top3, start=1): lines.append( "{}. {} | score {:+d} | normalized fit {:.2f}%".format( index, name, score, _fit_percent(score, parsed_count), ) ) lines.extend( [ "", "Top1-Top2 margin: {:+d}".format(margin), "", "TOP 10", ] ) for index, (name, score) in enumerate(ranked[:10], start=1): lines.append( "{:>2}. {} | {:+d} | {:.2f}%".format( index, name, score, _fit_percent(score, parsed_count), ) ) if unresolved: missing = sorted(unresolved) preview = ", ".join("Q{}".format(q) for q in missing[:30]) if len(missing) > 30: preview += ", ... (+{} more)".format(len(missing) - 30) lines.extend( [ "", "WARNING: {} scored question(s) were not parsed.".format( len(unresolved) ), "Unparsed: {}".format(preview), ] ) lines.extend( [ "", "Normalized fit is a ranking diagnostic, not a probability.", "Scoring evidence used: {} questions x +/-4 points each.".format( parsed_count ), ] ) scoring_summary = "\n".join(lines) return (dere, top_3_deres, scoring_summary) try: import folder_paths except ImportError: # Allows tests outside ComfyUI. folder_paths = None import hashlib import os import secrets import shutil import tempfile import threading import urllib.error import urllib.parse import urllib.request from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Mapping, MutableMapping, Sequence, Tuple NODE_VERSION = "1.0.0" HF_REPO_ID = "saliacoel/chars" HF_REPO_TYPE = "model" HF_REVISION = "main" OPENAI_BATCH_ENDPOINT = "/v1/responses" TEXT_PURPOSE_PREFIX = "salia-character-responses-text-batch" TEXT_STATE_DIRECTORY_NAME = "Salia_Character_Text_Responses_Batch_To_HF" TEXT_STATE_DIRECTORIES = ( TEXT_STATE_DIRECTORY_NAME + "_1Image_OptionalText", TEXT_STATE_DIRECTORY_NAME + "_1Text", ) CUSTOM_SCORER_STATE_DIRECTORY_NAME = "Salia_Custom_Batch_Scorers" _SAFE_BATCH_ID = re.compile(r"^batch_[A-Za-z0-9_-]{1,200}$") _SAFE_FILE_ID = re.compile(r"^file-[A-Za-z0-9_-]{1,200}$") _STATE_READ_LOCK = threading.RLock() def _utc_now() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") def _clean_secret(value: str, kind: str) -> str: cleaned = str(value or "").strip() if cleaned.lower() in {"", "none", "xxx", "sk_xxx", "sk-xxx", "hf_xxx"}: raise ValueError(f"A real {kind} key is required.") return cleaned def _output_root() -> Path: if folder_paths is not None: return Path(folder_paths.get_output_directory()) return Path.cwd() / "output" def _write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_suffix(path.suffix + ".part") temporary.write_text( json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) os.replace(temporary, path) def _read_json(path: Path) -> Dict[str, Any]: value = json.loads(path.read_text(encoding="utf-8-sig")) if not isinstance(value, dict): raise ValueError(f"Expected a JSON object in {path}.") return value def _write_bytes_atomically(path: Path, data: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(path.name + f".{os.getpid()}.{secrets.token_hex(8)}.part") try: temporary.write_bytes(data) os.replace(temporary, path) finally: try: temporary.unlink() except FileNotFoundError: pass def _load_hf_tools(): try: from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download except ImportError as exception: raise RuntimeError( "huggingface_hub is missing. Install it with ComfyUI's Python and restart ComfyUI." ) from exception return CommitOperationAdd, HfApi, hf_hub_download def _repo_commit(api: Any, hf_key: str = "") -> str: info = api.repo_info( repo_id=HF_REPO_ID, repo_type=HF_REPO_TYPE, revision=HF_REVISION, token=hf_key or None, ) commit = str(getattr(info, "sha", "") or "").strip() if not commit: raise ValueError(f"Could not resolve the current commit of {HF_REPO_ID}.") return commit def _safe_relative_asset(value: str, default_extension: str, label: str) -> str: normalized = str(value or "").strip().replace("\\", "/").strip("/") if not normalized: raise ValueError(f"{label} cannot be empty.") parts = normalized.split("/") if any( part in {"", ".", ".."} or any(ord(character) < 32 or ord(character) == 127 for character in part) for part in parts ): raise ValueError(f"Unsafe relative {label} value: {value!r}") if len(normalized) > 500: raise ValueError(f"{label} is too long.") if not Path(normalized).suffix: normalized += default_extension return normalized def _normalize_result_file(value: str) -> str: target = _safe_relative_asset(value, ".txt", "Result_File") if "/" in target: raise ValueError("Result_File must be one filename, not a nested path.") if Path(target).suffix.lower() != ".txt": raise ValueError("Result_File must be a TXT filename or a name without extension.") return target def _remote_targets_identical( api: Any, results: Sequence[Mapping[str, Any]], revision: str, hf_key: str, hf_hub_download: Any, ) -> bool: for result in results: target_path = str(result["targetPath"]) if not api.file_exists( repo_id=HF_REPO_ID, filename=target_path, repo_type=HF_REPO_TYPE, revision=revision, token=hf_key, ): return False local = hf_hub_download( repo_id=HF_REPO_ID, filename=target_path, repo_type=HF_REPO_TYPE, revision=revision, token=hf_key, force_download=True, ) actual = hashlib.sha256(Path(local).read_bytes()).hexdigest() if actual != str(result["sha256"]).lower(): return False return True def _publish_text_results( results: Sequence[Mapping[str, Any]], target_name: str, hf_key: str, *, commit_context: str, ) -> Tuple[str, str]: CommitOperationAdd, HfApi, hf_hub_download = _load_hf_tools() api = HfApi(token=hf_key) target_commit = _repo_commit(api, hf_key) if _remote_targets_identical(api, results, target_commit, hf_key, hf_hub_download): return "already_published", target_commit operations = [ CommitOperationAdd( path_in_repo=str(result["targetPath"]), path_or_fileobj=str(result["localPath"]), ) for result in results ] commit = api.create_commit( repo_id=HF_REPO_ID, repo_type=HF_REPO_TYPE, revision=HF_REVISION, operations=operations, commit_message=(f"Write {target_name} for {len(results)} characters {commit_context}"), parent_commit=target_commit, token=hf_key, ) oid = str(getattr(commit, "oid", "") or "").strip() if not oid: raise RuntimeError("Hugging Face commit completed without an oid.") return "published", oid def _openai_error(exception: urllib.error.HTTPError) -> str: try: body = exception.read().decode("utf-8", errors="replace") parsed = json.loads(body) error = parsed.get("error") if isinstance(parsed, dict) else None if isinstance(error, dict): return str(error.get("message") or body)[:1200] return body[:1200] except Exception: return str(exception) def _openai_json( method: str, path: str, openai_key: str, payload: Mapping[str, Any] | None = None, timeout: int = 240, ) -> Dict[str, Any]: data = None headers = { "Authorization": "Bearer " + openai_key, "User-Agent": f"Salia-Custom-Batch-Scorer/{NODE_VERSION}", } if payload is not None: data = json.dumps(payload, separators=(",", ":")).encode("utf-8") headers["Content-Type"] = "application/json" request = urllib.request.Request( "https://api.openai.com" + path, data=data, headers=headers, method=method, ) try: with urllib.request.urlopen(request, timeout=timeout) as response: parsed = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as exception: raise RuntimeError(f"OpenAI API {method} {path} failed: {_openai_error(exception)}") from exception if not isinstance(parsed, dict): raise RuntimeError(f"OpenAI API {path} returned a non-object response.") return parsed def _download_openai_file(file_id: str, destination: Path, openai_key: str) -> Path: if not _SAFE_FILE_ID.fullmatch(file_id): raise ValueError("OpenAI output file id is invalid.") destination.parent.mkdir(parents=True, exist_ok=True) temporary = destination.with_suffix(destination.suffix + ".part") request = urllib.request.Request( "https://api.openai.com/v1/files/" + urllib.parse.quote(file_id, safe="") + "/content", headers={ "Authorization": "Bearer " + openai_key, "User-Agent": f"Salia-Custom-Batch-Scorer/{NODE_VERSION}", }, method="GET", ) try: with urllib.request.urlopen(request, timeout=3600) as response, temporary.open("wb") as output: shutil.copyfileobj(response, output, length=1024 * 1024) os.replace(temporary, destination) except urllib.error.HTTPError as exception: if temporary.exists(): temporary.unlink() raise RuntimeError("OpenAI Batch output download failed: " + _openai_error(exception)) from exception except Exception: if temporary.exists(): temporary.unlink() raise if not destination.is_file() or destination.stat().st_size == 0: raise ValueError("OpenAI Batch output JSONL is empty.") return destination def _batch_status(batch_id: str, openai_key: str) -> Dict[str, Any]: if not _SAFE_BATCH_ID.fullmatch(batch_id): raise ValueError("Custom batch id is invalid.") return _openai_json("GET", "/v1/batches/" + urllib.parse.quote(batch_id, safe=""), openai_key) def _request_counts(batch: Mapping[str, Any]) -> Tuple[int, int, int]: counts = batch.get("request_counts") if not isinstance(counts, Mapping): return 0, 0, 0 try: return ( int(counts.get("total") or 0), int(counts.get("completed") or 0), int(counts.get("failed") or 0), ) except (TypeError, ValueError): return 0, 0, 0 def _short_error(value: Any) -> str: try: if isinstance(value, str): return value[:800] return json.dumps(value, ensure_ascii=False, separators=(",", ":"))[:800] except Exception: return str(value)[:800] def _response_text_result(body: Mapping[str, Any]) -> str: status = str(body.get("status") or "") if status and status != "completed": raise ValueError(f"Responses result status is {status!r}, not 'completed'.") if body.get("error"): raise ValueError(_short_error(body.get("error"))) top_level = body.get("output_text") if isinstance(top_level, str) and top_level.strip(): return top_level.strip() output = body.get("output") if not isinstance(output, list): raise ValueError("Responses result has no output array.") text_parts: List[str] = [] refusal_parts: List[str] = [] for item in output: if not isinstance(item, Mapping) or str(item.get("type") or "") != "message": continue content = item.get("content") if not isinstance(content, list): continue for part in content: if not isinstance(part, Mapping): continue part_type = str(part.get("type") or "") if part_type == "output_text": text_value = part.get("text") if isinstance(text_value, str) and text_value: text_parts.append(text_value) elif part_type == "refusal": refusal = str(part.get("refusal") or part.get("text") or "").strip() if refusal: refusal_parts.append(refusal) combined = "".join(text_parts).strip() if combined: return combined if refusal_parts: raise ValueError("Model returned a refusal instead of output text: " + " ".join(refusal_parts)[:1200]) raise ValueError("Responses result contains no non-empty output_text.") def _text_request_mapping_by_custom_id(state: Mapping[str, Any]) -> Dict[str, Mapping[str, Any]]: requests = state.get("requests") if not isinstance(requests, list): raise ValueError("Saved Batch state has no request manifest.") mapping: Dict[str, Mapping[str, Any]] = {} for request in requests: if not isinstance(request, Mapping): continue custom_id = str(request.get("batchCustomId") or "") if not custom_id: continue if custom_id in mapping: raise ValueError(f"Saved state contains duplicate custom_id {custom_id!r}.") mapping[custom_id] = request if not mapping: raise ValueError("Saved Batch state contains no request mappings.") return mapping def _write_text_result_file(results_directory: Path, character: str, text: str) -> Tuple[Path, bytes]: results_directory.mkdir(parents=True, exist_ok=True) normalized_text = str(text) if not normalized_text.endswith("\n"): normalized_text += "\n" data = normalized_text.encode("utf-8") local_name = hashlib.sha256(character.encode("utf-8")).hexdigest()[:16] local_path = results_directory / f"{local_name}.txt" _write_bytes_atomically(local_path, data) return local_path, data def _text_state_roots() -> List[Path]: return [_output_root() / name for name in TEXT_STATE_DIRECTORIES] def _find_saved_batch_state(batch_id: str) -> Tuple[Dict[str, Any], Path]: searched: List[str] = [] candidates: List[Tuple[int, Path, Dict[str, Any]]] = [] with _STATE_READ_LOCK: for root in _text_state_roots(): searched.append(str(root.resolve())) if not root.exists(): continue for path in root.rglob("*.json"): try: state = _read_json(path) except Exception: continue if str(state.get("batchId") or "").strip() != batch_id: continue purpose = str(state.get("purpose") or "") if not purpose.startswith(TEXT_PURPOSE_PREFIX): continue requests = state.get("requests") if not isinstance(requests, list) or not requests: continue score = 0 if path.name == "batch_state.json": score += 50 if "/aborted/" in path.as_posix().replace("\\", "/"): score += 40 if str(state.get("runRoot") or ""): score += 20 if str(state.get("statePath") or ""): score += 5 score += min(len(requests), 100) candidates.append((score, path, state)) if not candidates: raise FileNotFoundError( "No local saved text-batch state was found for custom batch id " f"{batch_id!r}. Searched: " + " | ".join(searched) ) candidates.sort(key=lambda item: (item[0], str(item[1])), reverse=True) _, path, state = candidates[0] return dict(state), path def _new_custom_run_root(node_slug: str, batch_id: str) -> Path: parent = _output_root() / CUSTOM_SCORER_STATE_DIRECTORY_NAME / node_slug / "runs" stamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_batch = batch_id if _SAFE_BATCH_ID.fullmatch(batch_id) else "unknown_batch" candidate = parent / f"{stamp}_{safe_batch}" suffix = 1 while candidate.exists(): suffix += 1 candidate = parent / f"{stamp}_{safe_batch}_{suffix}" candidate.mkdir(parents=True, exist_ok=False) return candidate def _process_custom_batch( *, node_slug: str, batch_id: str, result_file: str, openai_key: str, hf_key: str, score_func, ) -> str: normalized_batch_id = str(batch_id or "").strip() if not _SAFE_BATCH_ID.fullmatch(normalized_batch_id): raise ValueError("custom_batch_id must look like 'batch_...'.") normalized_result_file = _normalize_result_file(result_file) openai_token = _clean_secret(openai_key, "OpenAI API") hf_token = _clean_secret(hf_key, "Hugging Face") state, state_path = _find_saved_batch_state(normalized_batch_id) request_by_id = _text_request_mapping_by_custom_id(state) batch = _batch_status(normalized_batch_id, openai_token) if str(batch.get("id") or "") != normalized_batch_id: raise ValueError("OpenAI returned a different Batch identity.") if str(batch.get("endpoint") or "") != OPENAI_BATCH_ENDPOINT: raise ValueError("Saved Batch does not target /v1/responses.") status = str(batch.get("status") or "unknown") total, completed, failed = _request_counts(batch) if status in {"validating", "in_progress", "finalizing", "cancelling"}: return ( f"Waiting. Custom Batch {normalized_batch_id} is {status!r} " f"(total={total}, completed={completed}, failed={failed})." ) if status in {"failed", "expired", "cancelled"}: raise ValueError( f"Custom Batch {normalized_batch_id} ended as {status!r}. Nothing was published." ) if status != "completed": raise ValueError(f"Unknown Batch status {status!r}. Nothing was published.") output_file_id = str(batch.get("output_file_id") or "") if completed and not _SAFE_FILE_ID.fullmatch(output_file_id): raise ValueError( f"Batch {normalized_batch_id} reports {completed} completed requests but has no valid output_file_id." ) run_root = _new_custom_run_root(node_slug, normalized_batch_id) diagnostics_path = run_root / "custom_batch_score_diagnostics.json" failure_report_path = run_root / "custom_batch_score_failures.json" output_jsonl = run_root / "openai_batch_output.jsonl" if completed: _download_openai_file(output_file_id, output_jsonl, openai_token) else: output_jsonl.write_text("", encoding="utf-8") results: List[Dict[str, Any]] = [] failures: List[Dict[str, Any]] = [] diagnostics: List[Dict[str, Any]] = [] seen = set() with output_jsonl.open("r", encoding="utf-8-sig", errors="strict") as handle: for line_number, line in enumerate(handle, start=1): if not line.strip(): continue try: record = json.loads(line) except Exception as exception: raise ValueError( f"Batch output line {line_number} is invalid JSON: {_short_error(exception)}" ) from exception if not isinstance(record, Mapping): raise ValueError(f"Batch output line {line_number} is not an object.") custom_id = str(record.get("custom_id") or "") mapping = request_by_id.get(custom_id) if mapping is None: raise ValueError(f"Batch output contains unexpected custom_id {custom_id!r}.") if custom_id in seen: raise ValueError(f"Batch output contains duplicate custom_id {custom_id!r}.") seen.add(custom_id) diagnostic: Dict[str, Any] = { "lineNumber": line_number, "batchCustomId": custom_id, "character": str(mapping.get("character") or ""), "targetPath": str(mapping.get("targetPath") or ""), "statusCode": 0, "responseStatus": "", "error": "", } try: response = record.get("response") if not isinstance(response, Mapping): raise ValueError(_short_error(record.get("error"))) status_code = int(response.get("status_code") or 0) diagnostic["statusCode"] = status_code if status_code < 200 or status_code >= 300: raise ValueError( f"HTTP {status_code}: " + _short_error(response.get("body") or record.get("error")) ) body = response.get("body") if not isinstance(body, Mapping): raise ValueError("Successful Batch line has no Responses body.") diagnostic["responseStatus"] = str(body.get("status") or "") raw_text = _response_text_result(body) scored_text = score_func(raw_text) character = str(mapping["character"]) local_path, data = _write_text_result_file(run_root / "scored_text", character, scored_text) diagnostic["textByteCount"] = len(data) diagnostic["textPreview"] = scored_text[:1000] results.append( { "batchCustomId": custom_id, "character": character, "targetPath": f"{character}/{normalized_result_file}", "localPath": str(local_path.resolve()), "sha256": hashlib.sha256(data).hexdigest(), "byteCount": len(data), } ) except Exception as exception: reason = _short_error(str(exception)) diagnostic["error"] = reason failures.append( { "batchCustomId": custom_id, "character": str(mapping.get("character") or ""), "targetPath": f"{str(mapping.get('character') or '')}/{normalized_result_file}", "reason": reason, "failureKind": "scoring_or_generation", } ) diagnostics.append(diagnostic) missing = sorted(set(request_by_id) - seen) for custom_id in missing: mapping = request_by_id[custom_id] failure = { "batchCustomId": custom_id, "character": str(mapping.get("character") or ""), "targetPath": f"{str(mapping.get('character') or '')}/{normalized_result_file}", "reason": "OpenAI returned no successful output line for this request.", "failureKind": "request_error", } failures.append(failure) diagnostics.append( { "lineNumber": None, "batchCustomId": custom_id, "character": failure["character"], "targetPath": failure["targetPath"], "statusCode": 0, "responseStatus": "", "error": failure["reason"], "diagnosticKind": "batch_request_error", } ) results.sort(key=lambda result: str(result["character"]).casefold()) failures.sort(key=lambda failure: str(failure["character"]).casefold()) _write_json( diagnostics_path, { "formatVersion": 1, "batchId": normalized_batch_id, "statePath": str(state_path.resolve()), "rawBatchOutputJsonlPath": str(output_jsonl.resolve()), "returnedResponseCount": len(seen), "successfulScoreCount": len(results), "failureCount": len(failures), "entries": diagnostics, }, ) if failures: _write_json( failure_report_path, { "formatVersion": 1, "batchId": normalized_batch_id, "statePath": str(state_path.resolve()), "failureCount": len(failures), "failures": failures, }, ) action = "no_results" commit_oid = "" if results: action, commit_oid = _publish_text_results( results, normalized_result_file, hf_token, commit_context=f"via custom Batch scorer {normalized_batch_id}", ) if failures: return ( f"Partial Done. custom Batch {normalized_batch_id} processed. " f"Publish action={action!r}. Published/scored {len(results)} character TXT files as {normalized_result_file!r}. " f"Failures={len(failures)}. Diagnostics: {diagnostics_path.resolve()} | Failure report: {failure_report_path.resolve()}" ) return ( f"All Done. custom Batch {normalized_batch_id} processed. " f"Publish action={action!r}. Published/scored {len(results)} character TXT files as {normalized_result_file!r}. " f"Diagnostics: {diagnostics_path.resolve()} | Commit: {commit_oid}" ) def _score_for_hf(vision_result: str) -> str: dere, top_3_deres, scoring_summary = DereVisionScorer().score_dere(vision_result) if not dere: raise ValueError(scoring_summary or "Dere scorer returned no result.") return ( "Dere: {}\n" "Top 3: {}\n\n" "{}\n" ).format(dere, top_3_deres, scoring_summary) # ============================================================================= # FIXING TEST MODE: optional NEW cohort multipliers, with NO extra/tie-break scores # ============================================================================= _DERE_RENAMES_FIXING = {'Sunao Cool': 'Sunaocool', 'Sunao Heat': 'Sunaoheat', 'Sunao Surreal': 'Sunaosurreal', 'S Dere / Sadodere': 'Sadodere', 'Yandere — Sick': 'Yansick', 'Yandere — Yankii': 'YankiiYandere', 'Megadere — Mega': 'Megadere', 'Mayadere — Japanese': 'Mayadere', 'Megadere — Goddess': 'Megamide', 'Mayadere — Western': 'Westmayadere', 'Kamidere — Bite': 'Kamikami', 'Kamidere — Deity': 'Kamidere', 'M Dere': 'Masodere', 'Tsun-Aho': 'Tsunaho', 'Tsun-Ama': 'Tsunama', 'Tsun-Pure': 'Tsunpure', 'Gou-dere': 'Goudere', 'Sunao Chill': 'Sunaochill', 'Tsun-utsu': 'Tsunutsu'} _DERE_TO_AVG_MULTIPLIER_FIXING = {'Himedere': 0.943226, 'Ambidere': 0.918176, 'Bakadere': 0.964209, 'Biridere': 1.197835, 'Biyadere': 0.93106, 'Bocchandere': 1.007229, 'Bokodere': 1.158822, 'Borodere': 1.191923, 'Bosudere': 0.894561, 'Bureidere': 0.952454, 'Butsudere': 0.841989, 'Chindere': 0.955586, 'Dandere': 1.41492, 'Danyan': 0.926542, 'Deredere': 0.908661, 'Dereutsu': 0.908943, 'Dorodere': 1.029681, 'Erodere': 0.935712, 'Erohaji': 1.299714, 'Fuandere': 1.328516, 'Fushidere': 1.013867, 'Gundere': 0.823542, 'Gurodere': 0.920399, 'Hamedere': 0.811333, 'Hikadere': 1.200137, 'Hinedere': 0.924739, 'Hokodere': 0.834814, 'Kahodere': 0.870576, 'Kakkodere': 0.935341, 'Kichidere': 1.016932, 'Kidere': 0.963218, 'Kiredere': 1.132622, 'Kundere': 0.832525, 'Kurodere': 0.979905, 'Kuudere': 0.950722, 'Kuutsun': 0.97301, 'Masodere': 0.970982, 'Megamide': 0.976595, 'Megaun': 0.974824, 'Narudere': 0.8981, 'Nemuidere': 1.298231, 'Nisedere': 1.006706, 'Nyandere': 0.9014, 'Onidere': 1.021052, 'Pyondere': 0.910858, 'Roshidere': 0.829068, 'Sadodere': 0.900583, 'Sashidere': 0.883635, 'Sattodere': 0.821962, 'Shijidere': 0.876167, 'Shittodere': 1.120021, 'Sunaochill': 1.295406, 'Taidere': 1.478803, 'Teasedere': 0.888733, 'Thugdere': 0.992674, 'Toubodere': 1.00978, 'Toukadere': 0.858785, 'Tsunaho': 1.097955, 'Tsunama': 1.117567, 'Tsunbaka': 1.096329, 'Tsundere': 1.153682, 'Tsundero': 0.987008, 'Tsundora': 0.962384, 'Tsunpure': 1.149089, 'Tsunshun': 1.213801, 'Tsunutsu': 1.140001, 'Tsuyodere': 1.00727, 'Usodere': 1.045222, 'Utadere': 0.88179, 'Utsudan': 1.326589, 'Utsudere': 1.299547, 'Uzadere': 0.899334, 'Westmayadere': 1.040059, 'Yancool': 0.85428, 'Yansick': 1.065965, 'Zondere': 0.990691, 'Amadere': 0.863264, 'Byoukidere': 0.809163, 'Dandoro': 0.971538, 'Darudere': 1.432758, 'Doromuga': 0.961023, 'Gandere': 1.154484, 'Gesudere': 0.937201, 'Goudere': 0.936738, 'Hajidere': 1.378644, 'Inudere': 0.833058, 'Jendere': 0.921319, 'Kamidere': 1.021282, 'Kamikami': 1.145439, 'Kamikan': 0.919426, 'Kanedere': 1.045112, 'Kekkondere': 0.848293, 'Kiridere': 0.987166, 'Kondere': 0.925612, 'Kumadere': 0.851288, 'Kuzudere': 1.036878, 'Mayadere': 1.134827, 'Megadere': 0.881445, 'Nipadere': 0.910701, 'Nisekami': 0.860631, 'Norodere': 1.400411, 'Ojoudere': 1.000232, 'Osadere': 0.908937, 'Oujidere': 1.021354, 'Oujodere': 0.961048, 'Rindere': 0.926787, 'Shindere': 0.969181, 'Smugdere': 0.92349, 'Sunaocool': 0.903576, 'Sunaoheat': 0.934234, 'Sunaosurreal': 1.028935, 'Tekidere': 1.051304, 'Teredere': 1.321377, 'Tomedere': 0.913198, 'Tsungire': 1.204133, 'Tsunneko': 1.153867, 'Tsuntere': 1.429546, 'Undere': 0.977391, 'Yanheat': 1.046777, 'YankiiYandere': 1.03601, 'Yoidere': 0.870548, 'Tsunpuri': 0.942959} def _canonical_dere_name_fixing(name): return _DERE_RENAMES_FIXING.get(str(name), str(name)) def _score_answers_with_mult_fixing(answers): """Original matrix points + user's NEW multipliers. Nothing else.""" raw_scores = {candidate: 0 for candidate in _CANDIDATES} for q, letter in answers.items(): row = _MATRIX.get("Q{}".format(q), {}).get(letter) if not row: continue for candidate in _CANDIDATES: raw_scores[candidate] += int(row[candidate]) parsed_count = len(answers) max_abs = 4 * parsed_count adjusted_scores = {} adjusted_fits = {} for candidate, raw_score in raw_scores.items(): canonical = _canonical_dere_name_fixing(candidate) if canonical not in _DERE_TO_AVG_MULTIPLIER_FIXING: raise RuntimeError("Missing NEW Dere multiplier for {}".format(canonical)) base_fit = _fit_percent(raw_score, parsed_count) adjusted_fit = base_fit * float(_DERE_TO_AVG_MULTIPLIER_FIXING[canonical]) adjusted_score = int(round((adjusted_fit / 100.0) * (2.0 * max_abs) - max_abs)) adjusted_scores[candidate] = adjusted_score adjusted_fits[candidate] = adjusted_fit # Same weighted ranking rule used to produce the supplied NEW multiplier result. return sorted( adjusted_scores.items(), key=lambda item: (-adjusted_fits[item[0]], _canonical_dere_name_fixing(item[0]).casefold()), ) def _score_for_hf_with_mult_fixing(vision_result: str) -> str: answers, methods, unresolved = _parse_answers(vision_result) parsed_count = len(answers) if parsed_count == 0: raise ValueError( "Parsed 0/100 scored answers (Q11-Q110). Expected the numbered Dere questionnaire output." ) ranked = _score_answers_with_mult_fixing(answers) top3 = ranked[:3] dere = _canonical_dere_name_fixing(top3[0][0]) top_3_deres = ", ".join(_canonical_dere_name_fixing(name) for name, _ in top3) letter_counts = Counter(answers.values()) method_counts = Counter(methods.values()) lines = [ "Dere scoring summary", "====================", "MODE: WITH NEW MULTIPLIERS; original matrix only; no extra score; no tie-breaker score", "Parsed scored answers: {}/100".format(parsed_count), "Answer letters: A={} | B={} | C={} | D={}".format( letter_counts.get("A", 0), letter_counts.get("B", 0), letter_counts.get("C", 0), letter_counts.get("D", 0), ), "Parse methods: {}".format( ", ".join("{}={}".format(name, count) for name, count in sorted(method_counts.items())) or "none" ), "", "TOP 3", ] for index, (name, score) in enumerate(top3, start=1): lines.append("{}. {} | score {:+d}".format(index, _canonical_dere_name_fixing(name), score)) lines.extend(["", "TOP 10"]) for index, (name, score) in enumerate(ranked[:10], start=1): lines.append("{:>2}. {} | {:+d}".format(index, _canonical_dere_name_fixing(name), score)) if unresolved: missing = sorted(unresolved) preview = ", ".join("Q{}".format(q) for q in missing[:30]) if len(missing) > 30: preview += ", ... (+{} more)".format(len(missing) - 30) lines.extend(["", "WARNING: {} scored question(s) were not parsed.".format(len(unresolved)), "Unparsed: {}".format(preview)]) return ( "Dere: {}\n" "Top 3: {}\n\n" "{}\n" ).format(dere, top_3_deres, "\n".join(lines)) class HF_DereVisionScorer_CustomBatch_fixing_without_mult: RETURN_TYPES = ("STRING",) RETURN_NAMES = ("status_out",) FUNCTION = "run" CATEGORY = "Salia/HuggingFace/Scorers" OUTPUT_NODE = True @classmethod def INPUT_TYPES(cls): return { "required": { "custom_batch_id": ("STRING", {"default": "batch_6a98cb8c3f8c8190be53e868d3c269c9", "multiline": False}), "Result_File": ("STRING", {"default": "dere_score", "multiline": False}), "openAi_key": ("STRING", {"default": "sk-xxx", "multiline": False}), "hf_key": ("STRING", {"default": "hf_xxx", "multiline": False}), } } @classmethod def IS_CHANGED(cls, **kwargs): return float("nan") def run(self, custom_batch_id, Result_File, openAi_key, hf_key): try: # IMPORTANT: exact original _score_for_hf from the original no-mult CustomBatch file. status = _process_custom_batch( node_slug="dere_custom_batch_fixing_without_mult", batch_id=custom_batch_id, result_file=Result_File, openai_key=openAi_key, hf_key=hf_key, score_func=_score_for_hf, ) return (status,) except Exception as exception: return ( "ERROR: Dere CustomBatch fixing WITHOUT_MULT failed; nothing was intentionally published. " + type(exception).__name__ + ": " + str(exception), ) class HF_DereVisionScorer_CustomBatch_fixing_with_mult: RETURN_TYPES = ("STRING",) RETURN_NAMES = ("status_out",) FUNCTION = "run" CATEGORY = "Salia/HuggingFace/Scorers" OUTPUT_NODE = True @classmethod def INPUT_TYPES(cls): return { "required": { "custom_batch_id": ("STRING", {"default": "batch_6a98cb8c3f8c8190be53e868d3c269c9", "multiline": False}), "Result_File": ("STRING", {"default": "dere_score", "multiline": False}), "openAi_key": ("STRING", {"default": "sk-xxx", "multiline": False}), "hf_key": ("STRING", {"default": "hf_xxx", "multiline": False}), } } @classmethod def IS_CHANGED(cls, **kwargs): return float("nan") def run(self, custom_batch_id, Result_File, openAi_key, hf_key): try: status = _process_custom_batch( node_slug="dere_custom_batch_fixing_with_mult", batch_id=custom_batch_id, result_file=Result_File, openai_key=openAi_key, hf_key=hf_key, score_func=_score_for_hf_with_mult_fixing, ) return (status,) except Exception as exception: return ( "ERROR: Dere CustomBatch fixing WITH_MULT failed; nothing was intentionally published. " + type(exception).__name__ + ": " + str(exception), ) NODE_CLASS_MAPPINGS = { "HF_DereVisionScorer_CustomBatch_fixing_without_mult": HF_DereVisionScorer_CustomBatch_fixing_without_mult, "HF_DereVisionScorer_CustomBatch_fixing_with_mult": HF_DereVisionScorer_CustomBatch_fixing_with_mult, } NODE_DISPLAY_NAME_MAPPINGS = { "HF_DereVisionScorer_CustomBatch_fixing_without_mult": "HF Dere CustomBatch Fixing _without_mult", "HF_DereVisionScorer_CustomBatch_fixing_with_mult": "HF Dere CustomBatch Fixing _with_mult", }