From 842d1ec274afcb3c4d667de6715f150e2e6f0ef6 Mon Sep 17 00:00:00 2001 From: Rufus Date: Wed, 3 Jun 2026 19:25:03 +0200 Subject: [PATCH] Initial commit of converter --- __pycache__/apply_template.cpython-314.pyc | Bin 0 -> 45501 bytes apply_template.py | 182 +++++++++++++++------ run_batch.sh | 19 ++- 3 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 __pycache__/apply_template.cpython-314.pyc diff --git a/__pycache__/apply_template.cpython-314.pyc b/__pycache__/apply_template.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c120f0f829cb5217677c6d855a9cacd631075f24 GIT binary patch literal 45501 zcmc(|33yxAeJ6UckstuB0C!Tn+{8uPL~5lLTjC;B_c{|$ph*~38n@BBzI<=K`KZbor|x@w z@Ap41@6D5=eTjs$@#hC zoKx4S^Xu5J-mhoB2ET#*8vRE0Yx0}eZ-PI8{U-Vo*{|7e#;?9Jsl(#86j5G=PHTtN zZ)IU)r>!H|pIpQ_P0j>oqSNe5a$1~Lr_Gt{OmU_<)12wfjQT`>$}v;VI=PDKZ!zbz zZ_tQ=7!V! z8P4?xSFo_%S&48J3p7-R&*xYtEc1%FV|B&cCh46#x|kulQubHn4je(}S=?Ul z@P&TAXT&?~_Iv8Q7x-GfWo-1UNAL~Z`pzgn=;7PC+noE3^3JxNgU(*w&7T|-M%?}} zeqc=SjtOr6(AhEG*|eqMz>zwOWsi4wXu!=6yLn_d>K__LXzb+Bz>s^GuS9ged(h2q z;0K3%1H4!7 z%j<+nI#K(CG&bt-{kl2vwPU=`Jv=n%7R#wUE9QM*Y;cS};r0)lM&}4)10J87_tU!o zdVWao4EV>;@~Tyft*sR^sH>Nn;;UcL4qh&(ph|M?VyxBitDb@#f&{}|7rZ12QfB&j5We0k6-Y3V!Cs|ou~bNZ*zUU zZ{W0N#OcOs!RztG4Ct3% zIg90enIHfNjGT`p^bYxlJsa`s4-AjdFFiL(&-6>rP118yEJ=(%deRrOw*-9tv5`am z3&WoMp0l3egQK39vA4Blb8*M@+r9*t zi+2Uwle*J7r>+lQMCa7^NtlS^dMM=zSFbPSstiA;II%>j<6{Ye#~%)4z7h{3V;Qh*$_m9@7HfpJc4TaVH*iYB^Z^oNX7O1U?6-Bo(A z_Lh^v*a)BsR!fymC_-!>Rerrt20xbMbqhXEKbE;rjb~O@I|Ad}9c$X6HUG9XKeR7m zEejjVg!OnIe?S^oiZurWu|y|!zCfK4Gx*)71Rf#ut^xH#B}?(dDp|rR$$F7_J@f%6 zmUX2;xBean@0F${`gyI1J*l-sH0F%((T&FY-3ZXM>5(Ut_?`Jqo!oin(78f}cKWz< zHB_eLY#{VpdK=eh!~`-1n=r4>4tWH>hp#x-d}3_yLIux|?NiSEK%Ru`{BFXc{3zzQ z59{4i7t=MzbkE0hFIMSeCLhqVKc??KRAmrqP)01lD|k*0osXG@#|GTPKAHjRd0reY z-5FsEo>@OsA~4QFLzuyJ!h>58!P_#wza}X9sfI7>x|1*k_j(qpzk|b z7OStMc*`I2l`>||X{bR*$&QVeQCw?D-hPh=%GiK)ziD7FLMTCGHg>Cx+B1mcqnHH8 z%({|$RU;mlmT(dv+F#0FpbDxlM30H(B^m6UML}$_o{;za9bQRL8Q*4=l zvEcMlJXOThT-*w85I#nym{O7ShLO!BfC2#1jun-+}-VD{TG7j1=e zw!&${jXl$zH#(-9!|UoIw)*k5s5SZG*2%4NRHsNreZ*QHHrA_C<+nD?0a+P!V1>vU=j+q8d4-F44 zn0MDYpzh{C#2oFso=R2s2%UGYzOZeF;4;jJfU1K&CF&Y5?!zaI>5dSb5PiHmI$ zZCCtr)~>Lz3y8XlP1xUAzuh>Su>BFpnkqrvyBZ&|7P9UmD8-zX$kaf}m>I-*zc4oD z7pIX?okjv-fSAc20Fk*G1U-Xj%4AB^rQhS=iLA#G1>b6YC-cv`*xQFElLAG<$Yq?X zms7884Ux=^3Pw)j+5z$jAIF8NI!!Ld6}gzyT)YOCVL8B~NeH??L;A38 zg?G5LNtwO?hkL}y>H;{dN(ff8jSd9B+%d9Go}TXJ6T?GZMz#yho{fVs=>-$36r}u@kYa%JN6NaB!Qzo0Dw)BhpC-;ZSLir1} z@_UI~`lcl_XG!y&qi7kr$cP^X8pSzn7$R=_(Hz9{h_1}&bY0dx z4&IH;H+Ns(y+pyc2H$D=vmy3YZ5q-rr_AV+XskG+`{ZV0HOn*r=IJ$#W9(q5%MfoJ z_bD`_O0P00Fmw$CkC=-t?Mwt@S~F)QKIJTdU2w8Umn1hx{ngY+S#NTCyhfK%q=36d zk>0pQdIbq6=}j(^l>T=gwbGWg?z(2383PNbrA1w9gajlhGl704V--<@4~w8jAS@su zggQ$LiKwOb5V;Mw=~H`W_n!SCHN`#-FzI+DzJS|?sA&j7TVxDrH&U{X7D!ibT}RH4 zc*@y9BJP1eSMd0%>L7TMXnR7>Zc44K;_0DhQR%@k2(=)Zy&&aKXCd-ltekKRjTH94 zt1`usJfo0}LmKZPK*UW2nj=w`00j(Zh-enf9y61s`Ivh4Bgk$wEz#7B zS3V!jEx49*CFe%*^b>E?Msgdb5~B9pd3$-(v2M{(KIbT(b}u+8@1=8@WlI^HEh}uy z6qg_>r8s%H-3K{~svhI~Iw<7y$1nN~PMzQA)cZ|N1IbrnNstwvZxe(uA=u1*coK1v z?B9*?CAkU)ToTlefZPU|jp5PgC0Y-*9I1N~)NTRF`WvLl&n+oCraPU>|4^ZZ)WM6g?+Ow`{ z>hU8mCIlu>H>?hNBH@-#TvDWe;h{7lW|)nGth54Ahfz@mf%lCCgaMD`gxh!P`Y3QL zi_aGFAg#O_9t(4bAi@Q8?bh&6G4P|ezK1o0FC(|EpBfq%^VL~m7R-Mr5ojFeuXflSbmPt*B!UR29 zil10=e?NM|=Og(SCJTv*O2)-$QBL0rR&;BfR#toiF?>bvenIk;N)xCwc-PBs)?Ke# zqTsg=u{Yvkk{1x-a}v34yZ;YpA>_o$Hx-w8bt)U%jPe|IKmFz_Fu<*TEvH;k}F9{ ze`?KnU~whLeSq1WAWENUhSOX?Z*kjRgAU8aC`SEpowo19m2jL%ti|}pk*>-X_$xG4 zLrV?9EC5IkgvQ9wD5H%?E>B1jxNsQnMNn9P-xagL<}-Tq{{}O`v}~6Q)Kfcs;bT+5==CF z!3Lf`0BtJF4=Pp^Spn=qpj{x04@5(B7+*dZvuUdltX0!%-#;R8Fn#CFmV@2BZQZ^7 zhkExodw1^+)+;%X016}lU-3{R%1*8TBlHB&(p+F8Jf1)FP^*c$WaMq{t#1*E;x}s2 zLn1_YN+6i{fWW8{sVbSVLBbe)t|4y=c^&X#hT*YOG1H*uMBo&YgF%A?YQ!5e1;B0! zF_YjPJ>`iRM%?E`I@2Q3nPw&{5j>*;F_etXjF}a2GL|^B8_&ipm^f$qS=1OQch2@h z9xVv?>Z3I+(xECXYG z2|EwXJ9?w}1;5vUuc4Rb?X}9s1Mi=icRb0y-N`PSekzi^VKIB-T=vFDcGHwGYR|sh ze5v`>o#E0g3-&Eas)OMto%4=EEZMzWF1O~BbzE-Abmih&wToK@D*X6@{qZ05zSn!V zu;fP5^qHCB8TYr!-`F)GlO@AJO%Vx+2VIw4wGHD9{Mq6lO=*-tQ-*z<2oPl?*7)`rZ!lk9FWQ9yV z5IOK~MKTFrL}n71B>X!X{FTI!@Fl$cF?~^JAiJsf9tZDw+s*Fl5PBl`Z6AB1Ru&1E zBTgKZwcYq(VE>Ti6SsZXJ9)O=T?XuKw;5j2m2$JP90nuk#VIZW$D)X`co zF#-i#R|qX!z4i$zHK0;_j5eZiWbRSp0{mwV{28J=jQ@b7G$Y}~e-Igznk zYulmLHok4YBuF?7d@12h*qsb{fjpTmSjIg9NfK^-Zy07%ZVT*}1i(^C0mNJ3Z0kPg z+Ov0m+aVqXHx)pUK)VVG-3uEZX7M87yI7L)MvL{7ms%XdR|0XO2;>Ja3e90KWqD!2 zWIVSWroyT0;3l5$1@;4kz_gp2fe5T0+P2qTkWgcIl%uxzjdBEDMi$I9a(j+N2=3+4EIct9N~ z3hKCqIZL1=a(DU>5Y6b9XaYFuUYek4Fao%1 zia@Q1oMA35OPb&^sziGccR()^>ww+No%&nG)}jwYrvfXWig$iHEz_`ZN_$sjXA{vE zsSJRNV7Z%EC8Y)8JBXC~GUQhDIOLG}3uA9EW^!LBsd?7^wNjenq!h~q%Y!j-5bGn2 z)Ynmy)yE|1@II$Si-2YH3>0D%S*1s_*X<~YG1tHLe8W6d*j%-LH1pId&qDcE!)ESb zZ073eZE`yKSHoty*X*5S?TmeBzFSNQ)lZojNl+pE|e7(u?Lm8ZaCS*#ux$| z(Zx%DTwgV`vgPpvS~Yzs@6iiy<6o)dxIMkR&WI`N+pWY%o>>u z3??1#8{E)W_gJ4Vn0ow~PS=tBEv*NR;Z2`UVhnr4hWI5WP}B>E%&eH1oL4hnQSPf~ z?*4frI-^^|V3y-K9)qb=N6sO*jpy58S3zjAS(T>pGI6A(0!D^l`_DuK=)o$8HHi}y zdD1T2(h%2r10qXL#7T23H&}( zqGa_Rhj3tu5XODRk_Oxm`jXkYkBDcL5tX&4jHD1}2boC`Jy8;uo>&&=F7opIxsDMN zHiejo<^xIRL{se(#!nI$+xJT{r3?0kXvK!GqYSv8*sQ5mg(&EH|H*mB!4Dm|_fo-f zed^%S_&WvV)29~->Mxn6bWwZGb*gnKfy?2;rL7Cutv`1BjpJ^9;k9jlux)z(Y}sP|*4z18-)a2IZQt7# zZgqu^^eyfA!+pADnn@o*#}@RQ{0}Om1O= z#NJ&71JSbb8QnM9LP^p5vKzjcZEw6dpT8+uT64Rg=EI!Adzt8! zr2@{D_VUg<$d%D@GrAA5t7aM__PUAIJGRt|os*s6ED&g}dE4QSF*;U8nlKGaca=>z zNAvAGc^Akdcp{KKES7v=kF%@I2|>?Mkmxa+sKy!bU<1ma@G3?71M9+}L<(lvto z0nhMoe?OGb{|;Z6Rr2GaNdF~@P9FFj&i4$wUwj$V&^^7uWL~myNtxlSil{j+)UjkT zWG0LsSh8>l8K3GQ$-i$vXo*5gy*l<1tgZACp`~i}4ip%B_doOqi7lnlmZ>#}vIa)w z?vq8>cwD6hnGT39zG_2jH>w(=)HCfHvtbg=7m`F-5lmzS9@#BZ*%Of=4Aa|HRQ4rU zWj+F9q~1u=+3%5ebu-C3mMHkvAp3a-96p{zqEmxhIH{?|`!BHNdYmjs;@Y981gFY> z%P1A(+3VosiE{_qTj?|@6|K9RQlmXDCE{g`^a|0cqzC^j$&6c&-W1PHMN|Gm_NS1S zyQ!Zga>)a$mWSl$3VDVp0X`h-0p%!UvfA2s<*1|@apWME2^UDttM3ANCX&EF)u!N1 zr^oFZIyDM}Dk<(r&McuaFyPSFdCmleVCf2lEE9mLQs_Dn&+b9q!d~*w9mrEaizG2; zD}~zNVLt#YW6f*=3V%#55(iF`c+)5Bpg8U1?PKr9h6npmTr6?abB+Q`^SWL_6KuW4 zYyx&Qc|{XipYUxYV^fl_6ijPUQpfj2lQS==PW9X^Zw=E=>L1Uhhdhz7s$=9Bo zI``_+kraNy5VfYfyyHWgBbx5GoO3A$+VUq}uZ^TvP9(sd_T}C5B_n6K-aIjC%~-G& zMYD=N$f}&!_tVs@$#c=Ptjm^578G^(_2%hw^J(?bwCvEvui1Vt5s)~gd?}B!*~hz8 z-V2kSIU{@w?{8?ge{u*T168=VmPNb$HCOOwDLt6as8Py|x%h@&#}(D4yzEM`F&9%-7ql_6 zs!C95Reg1UUem~iY%E|+a+g^lZ{pMBAj9M~IOL^)k0m}mOL!=ZHo1<3LN&;%j`}a z3`6VaO-$c87}$k=UWD+B5@T_#afncu`VIsClU#GW2rvWdu< zE!qEuK>Q_3d#>7dlO_bVyQ?nU6^pKb2h0DY{63~!AIhN();<7wm z=`|eQNYs#|H#e-HR}5Y0Smq#Mv3SWkhB+)=ppOD7MFWC;9)BU z$c9ERX2sIz7mmB}ymb3`XVhY!x0FQ9=~GXBHRa``sMQfRI)3p7Se#OLHt4@cWq$eaZdD z;yq>h_sXn$noRFC>fkS%<_a4UWR?5PFqq1OsAJcohz3!BZdBsL~`$D%8|N8XL>n?=`)mMHkvQ}1m0Vdjt4 zy|*sBs zAnQTdgi~S-B*x(aL{XCcZ6&Q0I@+Ly;5a z8_%(8K4xMx8kSr&#eJ8&HcF5vc(D6wz*Chd%&})o5JFI}fuI18mWr5S2@oG4_n4W= zW}EQ|NG)R~GG%LhDIf6w1K}r#$EM8%1c0YfvO}h8mMfNU>7Gc=-bhLdn2gl)@s5vD zv#@GY(l4HxJT*1=l@UCEpUA$H9m<(*jHIt$Os|_uubc7D9*U%I2Ww%^y1ez$*3cGw zw6C8qgQ1u@y#Uf9@TuKzB5=3{fu@qtm#gb?sM7f`tb`hx% zAW;JW*y1Nig=WJ6L50=8zzBf>HzE-j*r(Ig+++S7oBiU!$%9Jrn82wuoeGVL_p zHyXup>ybxPj)C7zQWzVeO#yua?eYSZ%gh;nMI`MqJ*V&D#g*$#@R~IAx3Vq5n+RxR z#$S@E`;cqkwP>_*iU`ANMQoeK=t0qx0HQTXdYQlLOO~7DN>LzRsw;H`DFL#^m5P?8 zJ(V6XupMpaBhu`6)4b`fbnHa~svuECz~D{yrXBvz3>rZXNbj(DDFdWSs$7!#E4KhO zx+vvm_d1}z%yik-h>sev^0U0zt}K^f8&HkJYoHvL-Ic9@kU3=T>djQNESW1>_c&5G z;%W8B_*;R^Ij)?zf4_?U!o2k6dF|d@S8fkD`<2d_ z2u>bjN)DtQdRTVeg5G>@A@EKiHkBp13g{dL?>g5y2@hc|K$o@7WuZPFbg-ww6~y5~ zsk@3XDoK=MquyJD)Mi&M%^H`br%*0e{dKLA+XF#akt@k%r?`DsaiZ%%ZY!PB;>tz7 zc306RX>}tl)k_o6o8N_*N3-NCtqgI30Wj(7%#-1Y6f*y>zY&u}*O!4p3l)71sjG*MWV z!vBT_1`Ei_u-wPj?k2yI2yf-FswzEZrebNEj_|jL&KN{Sk84y`&*B3J0<_d8L@5%9 zY}gK*n9+xGuNX~@Ghf&V9Kw6_ae}-_cvU=OY*hp!{FFX@3@>J+qGNW2*RnSjzIAiD8%ODBBqXBUafilMq(SPCd<%`}$8@5DhUknz2(0HVP!E%)Joa#sLgtEG-Qu(B6s7EW`{UUrG^Usiu4T0HektP-%=iz$6qVD zQZ%1iecxcnPxvsmddd;CrV@q??OL=}eQMy6k|&IFxz$Ss&Xj{w0P+()v2mGoGbbaC z&Ep3?&IT**h-PMwci%~~ziJxq1WJaHsrl92VsZ0aaq~j)PHah^=n6Tasd=HHh180feY5^QIT$T2 zpGx^KEfZOl?E2Kg+43fLN3)9;v#aKy?JA$mh-7b?=!iCKTWn~VYiN0Q-(ti5+YS3e zrqJoo({ovsVdv8m`|f0|pFS7Ks-M^wrOodJbGCxesp)gGiQ&R+3%2c$A@dDTr8_bGv-Vk2IAhC1BI1T~YvxmGW)t7uKHt!al0)fV?Vjk2=2Xt)&RAzR&K{a= z3uiUIybl`6%$!it)brttQY0+f0J%&k8>NEZyqG+hJauZ`ye?W&HIXuPZq8gpl9r^& zq;N*VY{lDK-`)5A;d%2@(QJNtG_TpQRyV7Zt zKMzUENw(^NfgNxt$yomiSej(4N{iLJG`T3$fyO=yW@RdM2+I_y9-_gih8_a-=Y)(_ z^JXXTZrrV@;1ra$!ZcUPQF~AaY77ABg$-CdaY3B(G0fgXl%A+5y#%Ev#dL zNMP5auaLSzqA|>I(tieW{pmlBJ0#MwSJ}V|AY;h-XxZ#xAkMmSHC73MPX) ze-@kjcy?}^(1px}{p1~h7cqXkeo5D7c;BMEU7JR+iu%z&4yK4=dnJ880?EiNGm`Lgx$pOFMO zBLU3s#a)xTa01S@*YhLR^6}PaqV;xS_D7kyaqK^(jQ~@|kL?B1#sz!XOwrq|3ssMQ zTvBnf_ImAX>09o%P2V{^`*gVB@r9B-p@e90#m)Td`HRJk;o`mLukN25TxQ<=VlFH1o`EyxJj}dgnldP@(X3{mvm$?;8Hcc?;_!5f*!{~fC z9&wkc{ZHVfSE zcmP0Vn!vCdomz1k7T`nbE11ppkuZn39>RSq91J!MP-RJ=N(NJ>M8?SEWWY{5F|q3f z%0J%z0EqZ7YUBu*9_O?S*s3i^RQCj8*PE2baUI*?2G$p7C1E)`w_0uobTHjDA3onC z?FeN6&`iR_0Cia#2vRpQjF*AkGRQ{;2{O!pPK<3pt`iKX6SF@En#gFcET*$C5GPzi zj#b70VbEJ&W!sBmrcOpFn_I-qZoXg!*#a_yf6;~v2Mko{1DQzDDgh@HggwhreOSgY zeN!RkGl(i0)pt+`B2vkUewsqp$s@4N4CeqvtyEbCJAaqnG5`t|Q3SO(9uPa!eK`Gv z&d`JyQ=&3_42H^7xI}gn@$SZ+WG#In*pWS3ogxVR*qTWMHY^g3yuJem*vSC&jy+r4 zo%K;>VH`-RY|DycEdMd>)KWG+W#?W#|9j`B%`;_-+4Z-x>t{EGcbu7TI`d%;A1&ao zJ#poUX@8`kW~wb(P&{4y^$t2%jy>OL+JeJ+n%~vEwL27;e)8+*iRzv&s()9Q>VT&R#V=j_E%u|p;#g3ucXjISa8R1cuho}-5o_K;R|kBDJdGlo%>Y&uQL za!*k6tT!oA&RDX{r{nxBuxV-12x4ZR7vEUN0)B6Rj^~2@iEL!LML=y}kXax&z*HWq zNjU#c%mm+)h)INJmqC|(kNL2}yXyE_^OAvH-Ob9q+WNI;mrV2kZXzS|HUHQ0mJ-<` zq@0ea!LL5Ql*Ark=w`WRrJxATjbg7cAAm>%=O;6$yk3KbF2m|0^_<#^%XSZ_63Dn) zsF!VVR{%9L5>qHDwH`9iRRClvLlCt7nRDWhBQ0%t8b~Cb`f|;Dpto6`&+77U?J=&8 zteatF-PX)gL3fYL)3{1KZE~-uzr~zpKY}tc8X2WgQ>V4kE49<4H|2Ah?QF)lXi7mb zGOF<=Xwt+-Z4akO)TD`Dj>2sy<(M^yPO%$d`wRX6+(#VAs45QywU6aSPX6^;!E^ko zX>2a*N%Sj-8gPlZOdv;~0dui54o941>`gNmR4^_&~bRiCJ<{J5=yBumhE=3PAh;@KDt&V-MT!Q-R z%y4C@Y5O2U5S=T*<-iw-T*thF)<<-Q+el9DvdPkSSzoh8Id;tmX329@Em^*nqh9aL z!K&XXm!|$YbL3QDF#;f`N#C(e{Vs>(2`=f4&oSgrR^6G*=#4IqM!;IYq8y=y%pFpYu90 zX2NP`b!m}np#Ju?%Hf`NIi&sqPb+oR)VW%$vn$1AcS1z*TPz{jRkXZ4zuoc<{5k8x z6>#IX+CsGtF%lT1!k!Mb|K*Sduja|)unqH1T1k*YVy69DjV|T^&1+Y(E91A@+kGT} zV@_qGF_!>g806x;#jfIIdSzPMd=ClhRyb#VU$^`U9ES$OKz~}$XmDk_inVWLIDqvB z&_S(*?!E*PSz-@XI1SVRR`r%5mVHGOJi1DIfX`Ptd8}L|@xQM}0{K)~SJoOZ4-^XP zmojfT%nakweXcUOMDKdndX!quO2GVJkSblSpR1(jupCx@p=plWHdPKS_nT5mMV~Sn zazA-1H4w2vj)`#vHuqM!s^XTcXq&y&uId$d0$9>j*>hB_ryO!s%eiA5fup5y+~BgH zw9?<8v?|vIS2gQ7KxMWTvR5A)9BYM2vC%2^*0^e1m4uNb0Tjent_|^8?JAb*@2z#! zO50}8cvo$_*mMb&-2dJ>S6w_DhYPaQIQ6dj9+JhZaLy9BRHdI22*Wr_T=mR0pdK?Z z&g^g@Kc)NzSHmxt_aN2^uvbIe+Loxbh1EubcjKDas8+7Kx6#!Y7ZR*<)yTDk^+EhN zMXrsSI2G|Ar>jve3Ax0deKl%~HeDAe1h&Q$w!Y{1ijK!4@M(p!)LG^%--`I;TqVv# znrVG&rAv4yT>?vIX4$~&S&RQvqy^6P8m{$oa(eYwrY(2q`f>F{z|gPQUU{1UIaa64 zAOL0SU=tuvQ_p~!s~mD|a;;Z;zYDOM`VIOa{F6&YpBsBTatifVhB5eVTK&6*s*6Wk zSExEG_??6-M79sFzjL5$cNdLEb^6V&HTv# znaJSc59z2hTzn=<+Gvjioo7ZiGKx4EhAlb0a!j1)FKxHL@np?>Fjo}@ulN)I)J@}ob4A5d;&5V*yrr-%SgPhrMX6be zDd^rhAJcQ+axND7cB1(~Tj*SOz16*DcEz%a`#RGwNDwDt|~CGFU9Ab3hN5AKqng}$)! z2Q*>4-}n657n9LskPLrTT$A?V5M6jfX9NX{c)rCw;vM6A54NDcaH7#EHUVX`D|WV> zc<%WiT1f}wb<*MFIF?m2>7jj=&qSaO;VZE*6qgVVx~rJ2wh%#5QON=&94Gt?UT@I! zRBwCjHrzasN~hZodT?$k-CH;|8Z*;s8bqZ96JiD&@Ob_ouKtd62clo9VO3Ksy|t}< z&r|#906*;O?mw`nhl#sm3CuFf$4=p8w!b(r*4Hm>eiL^BkZea|BK{M4Bz{lSY!hw3 zHi(Lwos8^`JutVTW=6P0p_j;emApS7?;GTiMulmnnMsqlqxB$t_&>;tk~ayDt{EGl zbB|8agK=QYdjU7{c|2YUiTWWjT@%gOnkbH$YQT)$zC#~=KwcYp?~(Vn})n*}+>Y7DfHhH8uP%Yl1?OGU`SRz@Z z5it2{iV-94LnY^j>|Dp{38)cA0M@8U5EK+u{xf?U%3dU1(8dl5z!?&0L_5<>PhMs{Y zma!QHZlaKInlkz)%E$~=Nk5DU-0Z0S5#bl~<^LuRYydvXI}OAQS62EkL?2S93+N*( z@?h^K{6F;4CcmW9NraDBJhH+Qw+{4^fm{l+YhcEB?Af9|i&k{VGiD5;Rx#5_vMKbD zz-kRcIzFxw^?a$)CI@-fov;Tz$&@?)n8>Uou=6KxF|96~RySW*H{N*%HgSa!N5%Mo zJK6c+f|^KnE$rs*WMoY}AIT^k?;@*;+(m2AoV94tT727Dd?zJ4oYy$JYqmbjb~A*{ zEm3R!qP1ksTC!*@yKOCFF`B}gTEk6k;hgqJ%D%99-yLhlqP6h0wJmqrB&9oS?pEU9=9Wck{cUSKi_{iwcZJ&yhjWfZQjUhrNAD&(F806NfA!FG zVWhBr)*eaTJl@Vs@n1f8wI$3qL<%;}Hbrb($J>^SBYIQTU3)>ub?uoe&xEUXMham7 zw;Sh7X5@$3NZ}Z+XpR)@h-B=XNW`u|Io`fVVMoN?iFnAXJW{Z2&YU}8{@77)`M{+E zIQP8cjSUe;9W3PRd6#!y+KDqeD&Me1>@|pEb=$^~0*ou$gX3uBb?d%PRqN= zk)7S|pZQ>C&%6!iW>(Ime~`anVt+ITC%b=;Q#sLj*HOCY*fQtXGJ8Jaczh!1u3YD~ zcV7tiKJ`~Gg%5vj-t1PIvisendGix!W!0{?4@WB7=FH_2wy81Y`-Yjmd2{nmt$EZa zR*S~jgY&jlq)1v9Iz3~X9h*0|5kAz14cces^9NNF^ZMuQwz3I82V(DqtdoiaYWPSttD7& z=;0?n$gY6#C+w5N(8sy!r(c{g&u;vdEt0!=$`nn@T}r_QFH7iuNkXji{b6hrGGKU(ko|v|O{Z#1a z^r>)e-E7L+=RRoIw~*F>V%9aI@s6wsGnuI7Up{#0;DV!kqBXj{=H_$PpPMcFZtb^g zBkLcZ=$PuIX%bU(9+IC{!)Zrz)G5LrtnpbqK`bzcm;YeQX z6xq5?nM2u;v|`MG;_Y<0ZDHuhwZ1ET;p&d}*Z+9K-)soG{E;UDk;1bP`?(1-ra^AO z>n6Yh7+RQrfA=r9VYvizJFj3O3lx+M|V~ zbo1Iw+iYW`aLYt@)Ly)3ubH#g%p6{{Z@z8c94)P!Sr@L{7B1dCl@cwiT`b%( zlS*#W_V4cgPrDIsdT{peCkEXnV1RHsY_=QjR@VM$%708zKUJ|$WdKl^VX%xAF-?A? zuy)4x*1p9}9dnyH7Lz;YlRMw71ZbAmdN5eM#cI%47U6?uQ>5s|#AoD?t$Zz-?CfaJW}2QHmThwYKej zYG2IQ+1-9nmr4nm234+wNe|@BhO7smys}Hi)^4J1_2>*m)--cQK>lc1FcF z4S#BR(-O&O!hQ*J>Y}+|&RlTy@M2-z?ZUd57eTttn|Bk*lro7u6AR`d0CwAI#=@Ie zB~~{U*4}bVyTjRz29vn@jLhK7gcArnqU~?@n|jfljF{)u*5Y5$S}bP8f67^PjrYjA zYFwh98;97>OfP#Wu1o?DvICzYnupy*XVS`yw}pY4Mow8D;;N;wVUA3hBm*wR@)kxf z%3AGFR-K%J37{2&oK^G1sbf$EX!3=jlA5z3bk^pqu>@Aia4H92#t|m#aW1!hHQQJ^ zcxVtxKftQxtu{3(tL`+8*l{M%KUH|_Odx^oYT{RlAq!wN?NVTi+O9su`nX9C!7$YI z1+bx3>(fi4kl1Tj-doNDui-IVLaGR7&A~dHm4Txj`7;4_jV_(YG<25Ev2iQ1QOKJ} zJ{9o#0UFYliJwSd$^>+)Nk@fTbXbt5gd`WlrU$yNo3WTyB} ztKY2tHL`|XIjEJ4Uv?MVe`g+0m))&{LHrnZF@rZb_CWUB($JS z#cf(uxAw%4q`@Hp5~?vlx~o-!bR-B|4bnvj(vc>F3}^LpG61}*emW7Y=9|WE_I}I4 z-mF4^P}huW&c_14Qx-HnN$z3fOH@*Dvv^&Lv|xOE9Ze-kV<9&P$Jke5)}wU+!G~p4 zRToUCW)UHLO`vVCp23*$)bQ8|G_)UG?W68qFA0>`X*8581aQMRc8|t`H5#iO^}cIO zpQ^fCcd2eVCz4SeVY~Cx#>(SHFp-U$izWJ1P|`9JX`^Z)jbkN{m+0z^=!^srX`w-M z(^xD)w2JOV3j-%1I}~`t6%M0Ds|{zAhLf-x4X07J{T_K&2bbvQn=P|t-+F=te`sJo z^Swt{1hK)|Q8OY7JS!JZtEbq3a0AGeI|fH0$s`gkyo!7E7~WEDE1V6)6`{CBLk)>3 zCCBDO0mv%|dg(%YD7l1ypKVyceLMg*bwQIj@&-vzj(y@#j$k)5^j@HNJgAsJmtqR1 z;Q6RYkVcAAA^k)EckT9Tt<9f9Bu$t7V`B21RQpu>S6&F4^XYPNY{v?ny!PCc=R(J3 zw#-z{mWT7VePG=dHg0=(jt~vN=13U=Yprc!5ncfcD z55%{v^5&i8uW^|ZmP|8=+sl3I8cMeRa4eS8gKOMzUE#1hun~Fi{L;0HU*KDX3$;RE zw3%IG%7_TUCpAQIK>sQ3_vdkErMs@mBCft9z*RX*x*48jDbS)4*Wd&)7mDbSh=W+- zpaAtlV3Y~<>AGPk7JO{ULR%xLUs*zt&_K1tg=;~Yer!uR1-|G!GGZ&?Rpbruot!>Z zM&LV=Tns5X7H=zEFCIxP9q+i4ye@S1=JVH|k0dvZw=41re!6nrxPdqsCcY@1-ZF2j zUh#O>m_D^Wv|--3{tiNwp{{vj<;RKEFTe2R7sg*uq`s~5=C%)0)9xDWQytSM=Z&== zT5ak|L`9kVmEnRwCkY6n@K%HLpJ>5Bt*+!A2k$i>`%x<))j_^s(Rjbn_}c;p*jucJqo> zH^O<Df(>u7`qgXd zdYX<(>?4IFbB8NkTHBw8;|r2?)#`nv>v#hdHqUgge-BCeMujE%QC+U|ir&%pWVR4fK>rtYNp+?vxQeKeLs)Y~EejSB4% zC$5$q3rpBZ5CGSLl`D>Ubqlx!`C%piwkg#p7MSlp@KV`iZ37D!1@Tn+s+5B3>I5%x z?+&ItQ$AQHMkN~`GGIbQ58!ivwCDfu zwWh=tnOLel$6j0P_%f zm_U7FCWr_jY9O4;*d5W_;|e~B$asuG3`_r6ENN(zc4kt} z=jhp@GE&&KJLZS|f`06|Si(u1^9d%*0HX*E2~MPB{JSoa2&1zm2wy@ZhRa?<00D);AhVdtU zYR#mG>=ay4jY|=2sZ*A(92{?pCRxV!Mw3%C^uhMGH_sdQCF5JaQE_pLgE@anF zS|<#5(o3ed%xsFJH%ufb)IjOb{A+ zI9q+mXimzxV}s&+=H$F>v-IiZy|_Lir{R+_&SWOKWWxES{Yn+i&l@i&4-K;=Z+qsA zt;!4finG{5>91KPHinJG(>i#+_#~T4%l(AYQ(4lr6BRS*^R^98KTo;8+I6x0U+$#l zERWDQyJz0EMUAjzLh{div;=?HnAu*V|LYoSN0#ZYw-)UyH2tX1hVYN7b>wd->PR=e zpKd~UnY`n-MEjHh>?*8KA+bX6t{!4P57RzLs6P@I@BjB-mG&VfiaD1K@(XGV9Pk7n zvFPHlJ=j;OzdmrHA6}YH%9W z9iU)*u=>bpclzQ02ojPo7w>{$oXV%^6_*jT%GY6;kJE7CortF??cv?132UZ#c+Y7f z(iF<|Q-3w(Jbdjju1)sjQ$KggAFG}uHGZby$PD#;EXze>d4lc z81F6UB8B$ox+YRV2QoD!C^VE>Pp5LSB?cJ&+h< z?iI`R(Ci|$Y3d7Vd!=)xYxd46`#Uvjsa#IsvN@MsPAZp=S@pa`0soyVrHAN{6;3m1 zb~zOtZz58teN}RF^;Axbr2_|p{A#&D_JTgx@NjOX%6-kjBw;&s^U5S6MlMsSH5 zmuFCD42QB`0D(=0tEBd&Fx``lUv@32^qrmU9803UV<%DxMA1Re0((=kcm*p4a*&4C zr#XZ1Al|Y==T;tfD2dF8I2**2kuRE=LVXy@okM^`=BZQ0%PE&qBG#gD&>@h(gsk(% zvb#xX7mHplx@r&gzMebXvY1pkpHw-sCu%L2*4?yRx4dDShutKupx$_M%k?ec>K&2N zosryK6Hi1lixx90<}xd0O26rzP5R^End9NgosrC46R?Yv?CHv<{qwe(k8+D(Rs}P+ z?N_$ncyjvrH;za0H^JVmu;kj&D@SjfnaO$M`AA{Ylm!Q{kaeW=rl4smDY|Lb?MsYEuE_4`t`0zYUP9x zr#_PTBuuCVUp+X{rZi~hI|J{QfB)HeTPIzj=9y{z#&Zh=jqD<|#;xD2`gYYjXWq^H z{`vXFeOGtRnCA2A-am?l!V;+S+nqm5e|Ph(ocT?iuf51_1D|PGSkaaZGf#du1y*Bw z-!(3zwL#Hd*z{=?XQ{f^z}b>N-H6aaQYCx%%*SIoyk}{t>nt_>aJvQmUm07?`1zY; z8~LTy&P_@2sk&`Qxt63mp8W|)znPQ^3LG@yrt9CA{mOY4MAYv$vgi}by-6tLq5^?22yc)tt4XL{42b>=zqodw!>Q0o*1_du<4cE|&5or-yUJZ^q=?mNmm+j%n(of3IO-Jv3f zW{r&C03Iel*L+3KFLQJ3LL1K|wQk6-i@8bf2H) zFA!#r=^hIfw#qt;6?2j0gKC*ZnVNzs#bhnPEJb@FO-xa1jb0xMnrmxC{bI0ek7NV_ zx=Wl|d?i!sjL-mx+Kei|ILa5M7QaW?ze;t=ce>BDs-xV2@lfr%Y_RX@M|~KagjHbz z1D&ifoTM$WiE2*qgKz^`GHA&mfIGgDGZ&M~ZYP&T9f$Aa@QXP+=W=!~=@ZjzpCnpS z62{w?@;H-q(U?7F%#NCE6VJhpHkw?zl!!x$KjDm~gryYCcRTx9t@&!ve9gxFEz#uN zny=cl>Cv=;XmU<8dCyW3%iM&_(=^HLxLhKdT%$>@&0PFi{)n%(iD#q9O-m-0u>l$5 zjN**(1FCEQoxvP|Wrh@pfLjZdsmgmTy^kw@GyvVHxkZARXBZ`0Gz{8VC(gNdhO$aZv zT3}hQKo``-GuLn_R$j}mFpFn*CbL{c5c@-np@_TXN~l(D5<{knM-)ph7!T;N|7mww3>UuTWX^8o8(fngj{Lt~ZqJ5dV%^7(jFScn}&9hj}UHyeco;0y( zGxG5-qQ0n$=;GE(VZP;9q`+FMUW!t{^m8gLK+-Uhte5+`gN2q(+&Tp_H<-Wz%(2a| z!+gLOa0@VrWmayA5PpTgy~@sjRm{R57XdjRvqu%JSgk7=5{3(y^q4_EQ4DUuecPk{ ze!^i)Q!Glf8B6ve>KaSO9dIYc+`^z#ORryK)kx+YOY&$;-~I#M2yc)_f^dO}vI$ra z%OtW%#u4|>sPHHF%GT&2J`#M*{mSw2cG!+AS_|f^1)<)EwPd`N^vPwHYcAEy8;h2V zIi_@oh9Szv|1fw1d&%^sh`lmwtHh2DTiV6l7k5wWrrlh@PlnxEe^vihYjId#aZMxo8kY^pd;Uy)yf-w$eR$3 znfr0~0bw(zOG`(eF}$;#$O0p&xYZjp`ONudt% z4wJ{2_h%{OBah~bIJej)+Hcdlx5@hvc|RuaC*=Jrd34aTxUqH*g$|LY>Sms!N3v=V z=Q8oyY#p$f#V&-78KFM&vpb?0gcqluc_#>oz_4ex@LeRqf6hlL8rUo9boY!IIwQ`O z*I7T}lGuL)Qa<8RnQ#3Gm-7>@@F!f>k{RlTu%#nvOblBNGJj9hh&N8=?_)l`!bq}n zDb<{yAJ;EAxWtsjgzVc1*y}Qu(oUW@9W~k%FLf#(lh%|M+7dP9l9xWUD{9OP zXVU~XWl6vGsr^gk_Dp@4OMkFQrz?M8Ht5zru<3P`4=iR~$%8C|u7Mu+>mFDVb?FZr z>AI{>D)qXgsU*x}UDBr+Eqa~f9!LIzUL&qteo%)CTpw6)t>=SvxG3J&MJ*xwbUD~VT(8S=pAr~F><&H0QO!j`owP18$pNuh&4b1-so?NZU literal 0 HcmV?d00001 diff --git a/apply_template.py b/apply_template.py index 2f27cc7..48e20ca 100644 --- a/apply_template.py +++ b/apply_template.py @@ -48,11 +48,24 @@ log = logging.getLogger('r360mx') # MAPEO DE ESTILOS: source -> template # ====================================================================== DEFAULT_STYLE_MAP = { - 'Title1': 'Título 1', - 'Title2': 'Título 2', - 'Title3': 'Título 3', + # Estilos de título principales (RatedPower -> TEMPLATE R360MX) + # IMPORTANTE: el template usa 'Ttulo1/2' SIN acento + 'Title1': 'Ttulo1', + 'Title2': 'Ttulo2', + 'Title3': 'Ttulo3', + 'Title1nfs': 'Title1nfs', + + # Índice / TOC + 'CustomStyleLevelOne': 'TDC1', + 'CustomStyleLevelTwo': 'TDC2', 'Title2Index': 'Title2Index', 'TableContentEnd': 'TableContentEnd', + + # Portada + 'CoverSubtitle20': 'CoverSubtitle20', + + # Captions de figuras/tablas en el cuerpo + 'NameTableImg': 'Descripcin', } @@ -107,6 +120,26 @@ def collect_image_refs(xml_root: etree._Element) -> list[tuple]: return blips +def _find_section_boundaries(body: etree._Element) -> list[int]: + """ + Encuentra los índices de todos los sectPr en el body. + Cada sectPr marca el FINAL de una sección (el contenido de la sección + está entre sectPr anteriores). + Devuelve lista de índices de hijos donde hay sectPr. + """ + boundaries = [] + for i, child in enumerate(body): + if child.tag == q('w:sectPr'): + boundaries.append(i) + elif child.tag == q('w:p'): + pPr = child.find(q('w:pPr')) + if pPr is not None: + sectPr = pPr.find(q('w:sectPr')) + if sectPr is not None: + boundaries.append(i) + return boundaries + + class DocxError(Exception): """Error relacionado con el procesamiento de documentos DOCX.""" pass @@ -124,7 +157,7 @@ class SectionDetector: MARKER_STYLES = { 'indice_fin': 'TableContentEnd', - 'titulo_contenido': 'Título 1', + 'titulo_contenido': 'Ttulo1', # El template usa Ttulo1 (sin acento) } @staticmethod @@ -155,7 +188,7 @@ class SectionDetector: if child.tag == q('w:p'): style_id = get_style_id(child) text = get_para_text(child).strip() - if style_id == 'Título 1' and text: + if style_id in ('Ttulo1', 'Title1', 'Título 1') and text: # Si hay un salto de sección justo antes, ese es el límite for j in range(max(0, i - 3), i): prev_child = children[j] @@ -200,7 +233,7 @@ class SectionDetector: """ children = list(body) found_toc_marker = False - best = 69 # fallback conservador + best = None # 1. Buscar marcador TableContentEnd for i, child in enumerate(children): @@ -220,7 +253,7 @@ class SectionDetector: text = get_para_text(child).strip() # El índice termina justo antes del primer título numerado (1., 2., etc.) - if style_id in ('Title1', 'Título 1') and text: + if style_id in ('Title1', 'Ttulo1', 'Título 1') and text: # Verificar que parece un título de contenido (empieza con número) if re.match(r'^\d+\.?\s', text) or re.match(r'^[IVXLCDM]+\.\s', text): # Si está cerca del principio, ignorar (es el TOC) @@ -235,9 +268,17 @@ class SectionDetector: if pPr is not None: sectPr = pPr.find(q('w:sectPr')) if sectPr is not None: - # Después de un salto de sección suele empezar el contenido log.debug(" Salto de sección en source hijo %d", i) - return i + 1 if i + 1 < len(children) else i + candidate = i + 1 + if candidate < len(children): + return candidate + break + + # Fallback: si no se encontró nada, devolver la mitad del documento + # (asumiendo que el índice ocupa ~la primera mitad) + if best is None: + best = max(len(children) // 2, 10) + log.debug(" Fallback: contenido empieza en hijo %d (mitad del doc)", best) return best @@ -428,9 +469,11 @@ def extract_source_title(source_xml: etree._Element) -> tuple[str, str]: if child.tag == q('w:p'): style_id = get_style_id(child) text = get_para_text(child).strip() - # Buscar primer título principal - if style_id in ('Title1', 'Título 1') and text: - # Dividir título y subtítulo si están en el mismo párrafo + # Buscar primer título principal (no el del índice) + if style_id in ('Title1', 'Ttulo1', 'Título 1') and text: + # Saltarse títulos que parecen del índice (muy cortos o numéricos genéricos) + if re.match(r'^\d+$', text) or text in ('Índice', 'Index', 'Contents', 'Tabla de contenido'): + continue lines = text.split('\n') title = lines[0].strip() subtitle = lines[1].strip() if len(lines) > 1 else "" @@ -491,12 +534,23 @@ def replace_content( changes = remap_styles(src_xml, style_map) log.info(" Estilos reasignados: %d", changes) - # ---- Detectar límites ---- - tmpl_idx_end = SectionDetector.find_end_of_preface(body_tmpl) - tmpl_back = SectionDetector.find_back_cover_start(body_tmpl) + # ---- Detectar límites por secciones ---- + # El template tiene 5 secciones: portada, disclaimer, índice, CONTENIDO, contraportada + # Localizamos los sectPr que marcan el final de cada sección + tmpl_sections = _find_section_boundaries(body_tmpl) + # La sección 4 (índice 3) es la del contenido a reemplazar + if len(tmpl_sections) < 4: + log.warning(" Template tiene %d secciones, se esperaban al menos 4. Usando detección por estilos.", len(tmpl_sections)+1) + tmpl_idx_end = SectionDetector.find_end_of_preface(body_tmpl) + tmpl_back = SectionDetector.find_back_cover_start(body_tmpl) + else: + log.info(" Template: %d secciones detectadas", len(tmpl_sections)+1) + tmpl_idx_end = tmpl_sections[2] # sectPr de sección 3 -> contenido empieza en sección 4 + tmpl_back = tmpl_sections[3] # sectPr de sección 4 -> contraportada empieza en sección 5 + src_start = SectionDetector.find_content_start(body_src) - log.info(" Template: prefacio h. hijo %d, contraportada h. hijo %d", tmpl_idx_end, tmpl_back) + log.info(" Template: sección contenido entre hijos %d y %d", tmpl_idx_end+1, tmpl_back) log.info(" Source: contenido real empieza en hijo %d", src_start) # ---- Extraer título del source ---- @@ -534,18 +588,32 @@ def replace_content( for child in list(body_tmpl): body_tmpl.remove(child) - # Prefacio del template - for child in children_tmpl[:tmpl_idx_end + 1]: - body_tmpl.append(copy.deepcopy(child)) - - # Contenido del source (desde src_start, sin sectPr) - for child in children_src[src_start:]: - if child.tag != q('w:sectPr'): + if len(tmpl_sections) >= 4: + # Método por secciones: reemplazar solo la sección 4 (contenido) + # Secciones 1-3: portada + disclaimer + índice + sec3_end = tmpl_sections[2] # sectPr de sección 3 + sec4_end = tmpl_sections[3] # sectPr de sección 4 + + for child in children_tmpl[:sec3_end + 1]: + body_tmpl.append(copy.deepcopy(child)) + + # Contenido del source (desde src_start, incluimos su sectPr si tiene) + for child in children_src[src_start:]: + # Incluir sectPr del source para mantener propiedades de página + body_tmpl.append(copy.deepcopy(child)) + + # Sección 5: contraportada (después del sectPr de sección 4) + for child in children_tmpl[sec4_end + 1:]: + body_tmpl.append(copy.deepcopy(child)) + else: + # Fallback por estilos + for child in children_tmpl[:tmpl_idx_end + 1]: + body_tmpl.append(copy.deepcopy(child)) + for child in children_src[src_start:]: + if child.tag != q('w:sectPr'): + body_tmpl.append(copy.deepcopy(child)) + for child in children_tmpl[tmpl_back:]: body_tmpl.append(copy.deepcopy(child)) - - # Contraportada del template - for child in children_tmpl[tmpl_back:]: - body_tmpl.append(copy.deepcopy(child)) # ---- Actualizar rIds en document.xml ---- for blip, old_rid in collect_image_refs(tmpl_xml): @@ -567,41 +635,48 @@ def replace_content( except KeyError: log.warning(" Imagen no encontrada en source: %s (ignorada)", old_abs) - # 3. Añadir relaciones de imágenes del source + # 3. Añadir relaciones de imágenes del source (sin duplicados) rel_root = parse_xml(out_data.get('word/_rels/document.xml.rels', z_tmpl.read('word/_rels/document.xml.rels'))) - # Eliminar relaciones existentes que podrían colisionar + # Relaciones existentes en el template existing_rids = set() for rel in list(rel_root): rid = rel.get('Id') if rid: existing_rids.add(rid) - # Añadir nuevas relaciones con rIds únicos + # Construir mapa old_rid -> src_rel element + src_rel_by_rid = {} + for rel in src_rel: + rid = rel.get('Id') + if rid: + src_rel_by_rid[rid] = rel + + # Añadir cada nueva relación una sola vez for old_rid, new_rid in rid_rename_map.items(): if new_rid in existing_rids: - log.debug(" rId %s ya existe en template, se omite", new_rid) continue - for rel in src_rel: - if rel.get('Id') == old_rid: - target = rel.get('Target', '') - old_target_abs = target.replace('../', '') - if not old_target_abs.startswith('word/'): - old_target_abs = f'word/{old_target_abs}' - new_target_abs = image_rename_map.get(old_target_abs, old_target_abs) - # Asegurar que el Target sea relativo correctamente (solo media/imageN.ext) - new_target = new_target_abs.replace('word/', '') if new_target_abs.startswith('word/') else new_target_abs - new_rel = copy.deepcopy(rel) - new_rel.set('Id', new_rid) - new_rel.set('Target', new_target) - rel_root.append(new_rel) - existing_rids.add(new_rid) - break + src_rel_elem = src_rel_by_rid.get(old_rid) + if src_rel_elem is None: + continue + + target = src_rel_elem.get('Target', '') + old_target_abs = target.replace('../', '') + if not old_target_abs.startswith('word/'): + old_target_abs = f'word/{old_target_abs}' + new_target_abs = image_rename_map.get(old_target_abs, old_target_abs) + new_target = new_target_abs.replace('word/', '') if new_target_abs.startswith('word/') else new_target_abs + + new_rel = copy.deepcopy(src_rel_elem) + new_rel.set('Id', new_rid) + new_rel.set('Target', new_target) + rel_root.append(new_rel) + existing_rids.add(new_rid) out_data['word/_rels/document.xml.rels'] = etree.tostring( - rel_root, xml_declaration=True, encoding='UTF-8', standalone=True) + rel_root, xml_declaration=True, encoding='UTF-8') out_data['word/document.xml'] = etree.tostring( - tmpl_xml, xml_declaration=True, encoding='UTF-8', standalone=True) + tmpl_xml, xml_declaration=True, encoding='UTF-8') # ---- Escribir ---- with zipfile.ZipFile(str(output_path), 'w', zipfile.ZIP_DEFLATED) as zout: @@ -623,13 +698,12 @@ def replace_content( def setup_logging(verbose: bool = False): """Configura logging con formato limpio.""" level = logging.DEBUG if verbose else logging.INFO - handler = logging.StreamHandler(sys.stderr) - handler.setFormatter(logging.Formatter('%(message)s')) - log.addHandler(handler) log.setLevel(level) - # Evitar duplicados - if log.handlers.count(handler) > 1: - log.removeHandler(handler) + # Solo añadir handler si no tiene ninguno aún + if not log.handlers: + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter('%(message)s')) + log.addHandler(handler) def validate_docx(path: Path, label: str) -> None: diff --git a/run_batch.sh b/run_batch.sh index 47acebc..a7f64da 100755 --- a/run_batch.sh +++ b/run_batch.sh @@ -2,6 +2,8 @@ # Batch runner para apply_template.py # Procesa los documentos en /tmp/batch_t*.txt +set -euo pipefail + TEMPLATE="/mnt/c/Users/javie/Documents/R360MX/cloud/01. Info General/02. Standards/03. Templates/TPL01-Reports.docx" DIR="/home/javi/.openclaw/workspace/r360mx-docs-converter" LOGFILE="/tmp/r360mx_batch_$(date +%Y%m%d_%H%M%S).log" @@ -13,6 +15,18 @@ echo "" | tee -a "$LOGFILE" TOTAL=0 OK=0 FAIL=0 +TOTAL_BASE=0 + +# Primero contar docs totales +for TANDA in /tmp/batch_t1.txt /tmp/batch_t2.txt /tmp/batch_t3.txt /tmp/batch_t4.txt; do + if [ -f "$TANDA" ]; then + COUNT=$(wc -l < "$TANDA") + TOTAL_BASE=$((TOTAL_BASE + COUNT)) + fi +done + +echo "Total documentos: $TOTAL_BASE" | tee -a "$LOGFILE" +echo "" | tee -a "$LOGFILE" for TANDA in /tmp/batch_t1.txt /tmp/batch_t2.txt /tmp/batch_t3.txt /tmp/batch_t4.txt; do if [ ! -f "$TANDA" ]; then @@ -20,13 +34,14 @@ for TANDA in /tmp/batch_t1.txt /tmp/batch_t2.txt /tmp/batch_t3.txt /tmp/batch_t4 fi NUM=$(wc -l < "$TANDA") - echo "--- Tanda: $(basename $TANDA) ($NUM docs) ---" | tee -a "$LOGFILE" + echo "--- Tanda: $(basename "$TANDA") ($NUM docs) ---" | tee -a "$LOGFILE" while IFS= read -r DOC; do TOTAL=$((TOTAL + 1)) echo -n "[$TOTAL/$TOTAL_BASE] $(basename "$DOC")... " | tee -a "$LOGFILE" - if cd "$DIR" && python3 apply_template.py "$DOC" "$TEMPLATE" >> "$LOGFILE" 2>&1; then + # Ejecutar en subshell para no alterar el directorio actual + if ( cd "$DIR" && python3 apply_template.py "$DOC" "$TEMPLATE" ) >> "$LOGFILE" 2>&1; then echo "✅" | tee -a "$LOGFILE" OK=$((OK + 1)) else