From c0b3bdb55ede4cf8260a85facca92fc4bbe3813b Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sun, 3 Nov 2024 13:46:47 +0100 Subject: [PATCH] add downloadable lower thirds images to be used in voctomix --- pretalx_broadcast_tools/apps.py | 1 + .../assets/titilium-web-regular.LICENSE | 94 +++++ .../assets/titilium-web-regular.ttf | Bin 0 -> 57392 bytes pretalx_broadcast_tools/forms.py | 12 +- .../locale/de_DE/LC_MESSAGES/django.po | 95 ++--- .../locale/fr_FR/LC_MESSAGES/django.po | 87 +++-- .../commands/export_voctomix_lower_thirds.py | 336 ++++++++++++++++++ pretalx_broadcast_tools/tasks.py | 78 ++++ .../pretalx_broadcast_tools/orga.html | 3 +- pretalx_broadcast_tools/urls.py | 6 + .../views/voctomix_export.py | 22 ++ 11 files changed, 651 insertions(+), 83 deletions(-) create mode 100644 pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE create mode 100644 pretalx_broadcast_tools/assets/titilium-web-regular.ttf create mode 100644 pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py create mode 100644 pretalx_broadcast_tools/tasks.py create mode 100644 pretalx_broadcast_tools/views/voctomix_export.py diff --git a/pretalx_broadcast_tools/apps.py b/pretalx_broadcast_tools/apps.py index 4834890..45ab95d 100644 --- a/pretalx_broadcast_tools/apps.py +++ b/pretalx_broadcast_tools/apps.py @@ -22,3 +22,4 @@ class PluginApp(AppConfig): def ready(self): from . import signals # NOQA + from . import tasks # NOQA diff --git a/pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE b/pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE new file mode 100644 index 0000000..9a5431d --- /dev/null +++ b/pretalx_broadcast_tools/assets/titilium-web-regular.LICENSE @@ -0,0 +1,94 @@ +Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students +of MA course of Visual design. Some rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/pretalx_broadcast_tools/assets/titilium-web-regular.ttf b/pretalx_broadcast_tools/assets/titilium-web-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e0e2dc888cfeaa9b7ecd53ffa85d911afd6d9542 GIT binary patch literal 57392 zcmce<2V7Lw+V;KH9$@IbiAWU?L8K#K0RbDLSg^$;qGE5cBr(OrB%10;auSo1-g6Su zG`*PKo5z@_QKKTFh$t99K)~7Wf9;t8OiZ5lJm35KX1rLtti9TO-DR!4XDg%-!b{vJ zOk%~z!lDtvM?NfsbmwT`$kOqXbk~112@%>)h!Qb!QrYmhPXcR%&^RssF@Dm()P*Om zpTPE$9GEp{Y59tNucfsJVY-K7&(1GjwSwc`Y&(!T&tI~3-lWL#cZFC;c~?0vs3@Q7 zknrudoL|8C%mo}Ue_(o?{X+KpE?BzyYLAf7!-a64DTLF!CClfOR~#MPM+nDs>d0JL ze)S4@RE#8lU-CyRD_>f1$%$24scH%5&#YL!YV~uwXD<`N_X{Ch%2%wcSh2wAt_|ev z1m<_8zG17wCN39J*BJ#h7OLm!qp73vA>$aw8I?j|ww*DqU^~y)S9`)?spUHLSC7zp zJ0uua*-omYtDKu%HV?k~a<9Q>g`4SwI`hNbYv_*Zqdyykt6U?ml&j?`d6m3cu9erwYvpx4a)b&udB5B& zACM2qf60gBee$33Zh4QqmppEK-NjIPbeWhX=7_7sjdG^EwCid~z0=u~!dE!i>QwFO zXB<&&P(wvO#453x?Q8gIa*15Y5sfpxe1*7P++VI8@Rv)t zmQv(WxlAsXE94b&<;9Lst|t6MD3z;HTzizl_{w}aTo%A|$#yRpBKycNT3}T!HR}Gx zxT@3v1_*kcM>`|LI`Ohhky&!693|H=Lb}#l`&v6_`Xp>a*h^ts!@dmL8`czdF2X+| zBqA&#Dk3%_IU*;bFk(f-+KAgDUXF~4jE&NwoT9vM(>8#SzsxbcD-e0tO{ z*It8uRR2u>RR382NZ+o1pueE!>+zNwEX&Vts@hTYV%2k1_g39lHLq&+k6{!}@$a{A$Qhp#=n>hQ|Liw`e2Jo9kg;rPR`2R?KCkL&lw zDAS4mUp{^!R*V;$#ebxqjF7o<8vMQk_C6zDl^@D#*`oE)rfMs+N41^WQB#y@f@zLv zjp=sNqo!9)2ONevOn2Di@P@-~hdM{$*v~P~ajav7;|-2?JHF)jx#LkM>D1q8tkbnl zo19*7+U~U9sm1JO?rY987n!G+7n!dzZ!mvsKH==KD z=kln_`!3b4uC9@;<6M`!-sAd=>!+^!TpQe++y=Q7xn1SgDZK=yj*pJ6`*} zI=!R3r+8oI{fhTbJ{~>^KBIjW`)u-g%I7VgAAFAby890Go#*=x-}il+{UZEw{I2x- zm*4w-hy5M=BmMLJul3*Ve?DM%z~+GcfeC@v1il`4B*;6cAn3}VJAxh$dOK)Guw(Gh z;JLwf2EQHL&?}@@a<3)5Huw6lS8Yf@NNUK8kZVI84f(CNOYgGYH}(Fhw;mcFdP(SQ zp>Kx%*2kkyPM`UG9`EyYmKGF8sCdqY=Z*)AV|&Nu$F7QfHumQ@8J8NjD(S)Gx}fI|Iz;M^go>7mr#OW}gptXZu9`wba1B2SLow7r-7i8a+{blyC9Ffy2XIReU zobsGaIS=RjJLip@uXCz$&gXjNM&wS+y~WT+7oqH}bZ}v>Q@hEwEk%g7z_zUkPi=;6 z+a!i+p|))Y(O)*&wjITl*i&mcPNJWjY}@vb5hC5T?a7u93$XR8#R`!l2J&5n-&iRY zvcH<`B(P8{AzjYV`J5jwMu@RuGFL6+Tm*Z|@nWmln#9&TwyqN8e%%@-4l?bVD)#Q$=9REeq<)rvJ@shCPlv&8^n zhziDWjZy!~i}oX#m>^Z;ic-cTLS%?6`lXH~b8G?qUCn5!USG*s)#qf8Wt?3~`xjHb z$}^9ns_xm8l|(5D86u4YkDyO$`I}FUIrLaDX(>HYvYSg9A%=5A zjl7a=KBX#2*w2-*Kb!q!)T!jHm>2;wS0fWulH#)Bwc>IF0ewDy)p)PrI>lf6nJJX9 zilgP!qsp#eZy}Oh#z5{cis#K*Hs+5&TPw7IEs55k4$P&`uK&zM}*NEX- zvIqu6AXM8b`iq^SKst*w5F?)znH?mC=hKTne8DU4@{IFix|^SB2Al2y$eK{79n!A4W#kF6D$zf+B2d|w1_epDSWkT z(!-+6G>UW2g8Ml?!IrA)!zkZVcxbnXQf-6?HeD&gv?SrDg$P$ISR}~NV!V8l_lNpa_#=20NsV`VW zV2?Anwm=IM(ey1+ixnyIJrQe+xf=IBz{LE)kb{yz4~Q216msw&{R#Z8-AX>Y43r#t zB3XFN5m^N?%DMY-xznJ;ndON|nVi zbOZgAThS38;U#B_J_bLOj1*7)F5O@~Z|F37rF7bmqg}VrFGI(Brph+i9^wGL@K-$Ie)Mw<9A7IkYU1NTub}IwCBVnq{@yYfehnVvPdLVw%@_FKa*71 zXN7z)5@afli7g@s+uv7PD+(Nz(l&emzGI@a`v_%Uv8~v*ULp>GYf9?$;3;99L0fmb z{;Cx%W`%0*@4yV&iFF)j;R5&7ih>6`_hOC8hxxys@D~9h5X%@$$stH9RP>?DaJ*Zj zh{EFa6*1U`IMEMJ(_bWrM0|G=k)!h7sf>C$qn?TP8YHrbuX4p;F@!aXVImJ-JDgq= ziXvjLk=XaqVhl0ISiJB!;=)pxFi}h*`Y0oAnkuG=>0*YsM9dVIip#|1VwNbEns`Rs zA~uK*#RKAm*eL!X?i7!S7sa#U4kUduD?5#%No*1iird8J;*j`VJkCg-70u}I)8ZBJ zt@v8JN_;R!+=F-i9v}UU_)h#s{2+c5J47u$>SsLhPvSLv`WbPr*du-syNOup#A$K6 zSR@t_e=Q{_{0Uqh((iBV%Qp>?h-8f0-cK#a@{x2goEjP$siJk}A_!!N?F*;;77IwPX;h z89B_*2g@OHs2nEqtm`jgtJo$BWsw{qN3xzWn)QxiIhKf}gEhixaZHYr<7KIwAScR6 zaQ*pk;mSE{md;yJakbl=?rqnCxyx6V&zV!PY_;p0 zu1%-HIpq}XG}oFGQcwA6bCIpW3R{Im)(R_(#I>mFYS)UcO>>d085P#lsmNMlg^{?8 z>~4tL{O)bnkzED2&hOfEA310F(xv6r#=6h%vEw$X`&zdJ-P;bMW|yyYSim2r(W@6O znOots&`8XqZ9Q3N>&a+qPZnB-ZnUk<3vEL;rn^M9Mcv!(#XXwfzPQJZ)7bJkYgSh{ zEin@Ju{}s8CFSB>kD(S2Eu9j9^D?8}VARJ3fqsbblD^LSgI zmfQL?-rA?-M&dDk!J1|B%U7;hx}tzL*M!Qe!QI-439y?AGt@W<7 zCX=jJtuhj~N!{b(wyJyEdGef!xeJ#pDR*9N+i;p}y?nKiI80U&TCF5hW|hzytAxsI zy;)=HO_{YfYmCIPY~{jb^BvcyzaC{h`{J>t=bpLDCdoB6NlxulUfaFx zG{f4*YmCHoMi&)buj$^*SFNySDDT=d=NB1=%;gnE>NbAWlJZpxtlP`Gx7{c87-08R zJ$EMe+*#dY$7$;PmE|b?&2hn98iRV)T3|or zK93gIcT@}P+twCz?HMhw?-?zy?>H2#S-IR9VJ@jywQ7-Dk5Tr%Vok-W)eDy|o4fp~ zWl3|&SGZOzn~Mmon@jDRzQ(}JRrZr{pR3M#R$M)2N%>Oybsj6qS5_=rQZa9}anjgZ zsfI-Ddd)`b7FSTc^|ZHbzbk`>I#6Lh?P=_Fom7#O1g^wBd4F-ANX1~pSaViz&(D8x z#Y%M#NgYwwDQLxy6M3mzn%@hQq`AMV?lF0<^SF!{sxQ%7v)&2L=|6#!`cA!FZ`C{W z7QKP3z4{(~xBk0+Mz5!Yzy0WKlykN_3nl!{_lW+R&Yem1q0}=R>9{bT^<2-t%tTf0 zT5w)J+VjkxxAjI^{rjJfb8VY~e%^kLUNzFsI@-``Jx2eUtSwe~|N3KZzskW3Lg?)m z=A}pH^m7*;yV%}^*QqhK=F&SZmizzq80FYn|KF9PMosm`{xL@9B+}YzA5Z%7jr=)d~tXUKuX^rsljpY?yMR_Ocmcl4KO+4Jl_uh;7b^<8=+$N#pc%+UB4Wu0e6 z{I?$@+0m12Ju_Qtv5qeCZiNMnu(87~_wKy(>~G|zT5SIqqxA=tP;zUR3=LU!v(0`T z{b@w23`SdzF&>ZTpX;CSea?2|ICtZ=>tE?x^cPtX5Zt5N!nmE&ztvypE{EfcboZyb z|9>7;&e1Qu*`E8t9M*B}I(JGxfv%qhJzB}tZRl#Vev+-<7$-x=?95Qt>c{l`+)?bZ z3;KKdTijWE54>+Jn-YJ&W!RR9$MrFJGs7h&>YTNm4F3~1s zXS%IS&z>0E($y?j=>OrG4y@s?Y+qnM{*Hm_O8ZCWc@3`JZQuXz)1KP?-(TH*g`o?b z`m?mAL4UdXIQuPpn|h89$9jxN&%E|+{T;QXR~mouL@NF7K2{FuUs6h&{^Nz$VtEeO z?c;@K{=BF30{MQ<-v<4o3(v#hkKshSy?w|MU#FPz=a2GPXvZh~J*~fY;dwOuZR)@x zS&uVbSh|b1(I}_ZDCf){+jC*5l+d6b{^K$G{{I`fD~{{vkp7k`7cbFF+mGqoU-(<^ z(4`sn){wu4*Xcg`|8)zAbmD20y|U_;50Q`~>r7oA-Fjan(RLt_Ris$8t@wjvp91yz z2VL3KnKr)72A{ihwp-VG=2Y8;-DuOFp(Uq!S~$+M7*?&5Gj^ZdVxU%8u3%vl5)cda_yz4q*j& z6syW(SmBLig?9!k$7+o?g_YH%tRG*;H=Q-p2U$mci1pjCtfOvaW%za00B5sW7b+I8 z1{yAYU==i5{K$G{p*YSOau1{Ht7f0pK3w?ML?5hQ_57lf?dwe5NaWM= z>q%>^-ze?@s4vgPb{$i_^P~^fuRneEr|!@$NY3^|1pT(clk`HE7^j|4ZLuPX^g?LH zzB(WM2L!U;4HI?rM9*^$jDJt40aQ;yDmc-ji@}Vnoav*IcJD5TuKudJRGF%bKY!HK zcBnB@<7zF_kC?7PySJjy4kh7>ef;V7g&*r( zB_8~xzxF#zYRiRe-FKK2L)25&-WObKz2l_rHwnf%!r+{;BZ{Xyg}@yq`<*6%H4(<$ zrV9%A(@yZ8j`Tcmp?2EOsApvDnxI;)zUp~cm#(ksi5fHY^)fE=!Lr!Dfk;$owEEh$ zMi@Paq!;QdK)mgnK)(_=qU@0slIwdygF71XHc{Rm;lMccgd_D>5lVU?oY*&`jn0gy z72!tOeU0^67g}eBs!y$P+2O{WzMfE>cQ??Fv53DYJdu?beN@s;?1H+|$LNo$OO@%4 zm-zEXU2TUNpFnb3OG|*4A;uj@>wUZ`=n&O(gGx_0xZw}lzuykN-;?;>06Z80HCVD6AQCkVm_SX`gjjxP< zH9kE)B(^j*u`v~iH;>12TU4XnkGu@Lxh}$W*J#L>Q z=D9uYcAMKZiFx74Zgbs6BzpH5p0KCSAO+WVT^Fe|k!yM);n_Y;cBuN|D?>l)!mFY8 z$M;0K^EdJ7p*O{cgr4{_oS%;k40%5KWUq-zQoYjcP(4*8t6GfGC_&}4!x#(3%6et5 ziEv~JPb0$={`O5<5qL+yFXs2n!Dd&ptJA+6KGRO2-$CfBilLNcQ8ASAH_G<8!ti0} zMxmI6eyL~RMi`ONNbWcU3eq-=28P9WWeOb|pAu^36(u^bR2jW40{g_A~ zO$13`AV>x&AeD1zr0Jv?q?yc@27zplL-anF+30%8y$Re5ZUGy>MsO>*jk<2k;rMcnmyFollVeQ_BAwd8x(zz+oIXJruR1$u*M5Ch^sJV*coz(Bo4rsz#F4dm)) zyc_bQmsd-^+>fIsn#RadZc<3skS23R;1dBR9lg1 zD^hLcj=LGDwj$M5q}qy9tB`6HQmsO&RY!twqf6{LeKbS#?{h#c|`){oF0rLS$Yqm6d7 z(T+CS(MCJkkmM2C(ned_XiFPyQ99Q~TiR$#8*Neg(ncHFXhR!qXp`Y^IuZ<|Jt_JT zbl?c>YLf*V8^^IyFcD1S_zcPqf%ny70BZ_Em}jqN`zCNRxCLwg8^NvmL7oWmwwb%U7^;0`=N z1kV^ofn;HpsXP;z$FV~8$Fn~HOafClHWf_g{3T$fzFl4lE~C6zdYvpMokKbwEY!b} zix?|E#>o*G1tY0gn{<+pP6pBmL>g+OJsD*$o`Q2^lmi*vK%QrF5ZgGngI^31=i~Q1 zS7{-X7=t zHdr36|BBB2iq7pv=MJEA2hg`)(YO8R+kW(IKl*k6eLE;G)8CMn>)T{G>1@(<`YZCE z`WE>h{5R8L>wI3#oTBLAD;(R(D6C_|9oeH7`gysTXFwkW4;gcW68_rqk*5THy+SD} z--B3JWk=vW9QWXC6nTdcP318IP)|d4U*|z=6-BOL%vrmS+DqCjJiAJBqZ|hq=0K@# zlK@-T8~}!?Lu(-426sec6s- zI}Y>%@gM;tf&uuFB+`K(8Ki(TkO4BWWm!CposE^sp>5b&7`q2M-b~9*)3Ot^s9nq; z-zDUqNh{GRY!&n{2<9rckiosY=0>%Ms zD%Q@KI{IPltR=om`WnD`;1gW2>8{vxSG33#Epj!q=nQEiXa+5yjdI#SC$L~8sTn)& z$aCl>o{3jh$djj&9r2&8zyn>0Lf!*lJpLXo9x=2c2kzy3E?TaP1PdXL9dg7FbSsx1xWKbInKWe*zAXzYfz76fIM0+dI-qZBH!I(7+zy@S-Cn}rsIrKaiA2OKVL<~XO zhG7%(@DsBbo%L*Ad?s*`p6|OLVs9cM3Fo&xkzkq0^O2Gs$kvx+~CzFRzE#4v zO89mRzEz1!sdqhnxe441ZUGy>MsO>VyN&B^2X}xw!Chbz_y@Qf+ym|f{{;7e`@v@L z0C*5Q1Rkc2N5G@tG4MEjdxGOn(w?X2-_zh3@GN)^JP-a2UZDLif-T@B@G^LXYhDGf zfvsR0$6hCWgY-@C7I+)H1KtDgbM6DM9efBrqR$_benR>wef*4iJ}3Qx^h?sONWZ3S z-*D}>q~DQ#Px=GtkEAcP9qB1j?huQ5(gr*%&)ne`szoFFP4KFj^ep==q^+cFq~}Q6 zNjpe8N!9aL7E+x#hd}2gsYYrdbs%*lbs{yBI_rm-IaL$29%lAbP2_r5dXRdOdXajQ z&xh2P)Q{AkG=MY^O$s6nChbKULfV@&lr)@(E`sey(kRks{iy8Abupx|q;aJENaIQS zlO~WRk`5qEA{~fkCR0v|{t@=83XT5=jXxkWIG;(HMLLKyn>3fGa`$B}x})HR(tXOLb3SIlT z?dwE~+R&imXvjH!k72@gjMu+I2H!FM-!b;zG49{7zTOut8^ASLSodu5<{E2TwP?0l z-#UiY9YgDmp>@a5x?^bFG0HrQhSb8&V=(g=tUQLqPcWLwvbQoCwTwh9BhkbtG%*5d zUFR4*KSnQG>BTWxUrWntSsl#AGP_a#PPW>xDwUM2B(syUc2ZU)WvS8G$u*T+Q%P^! zvF~|U>seeY@yJg60)iPey*Y)wJAsX>!H)6!HK11R3Tfem_DRKx&e)?`>`|@F9@X0H zQ7!i9B=)EtdsM|p9>*S4Gn!|xM>W`^25}knuBWb>z|G(mumNlYY8~M=+HfCv?+2T~ z1K>gM5O|8VJPn=!&w}T`^Wfj$qV|MneJhddHYD^qZT)LIQiUCNMlTxi5hLEb&!%)%)JZKc?3-4n=Kfrn)I$4E2R-ud4=wS`|*MROdpmz;0?-Y8c z*mnxOJBHrX*!1o!dUq0?t4H6e(Y0#ytQtK#218GE>DhGhUqTiT$9Z!+(Y48kq7CZ-@2mb~ykpD%n1-t}a z2HRlr8>DZ7x4_%r9q=CbjBCCCUx9Df{|@{Bc7UDKy9@ln_8#yn*bjaKhiF?Rr~=i1 zr}2<-GZJn_y3I(o86AEX9e%ef78*>1J{06}u7Kl{*`C7oR4|R$N5wxg(eX>cLi*~8 zp0_iu4UB6Gqt(K=s=1yT*Tam6TIZ@a^kNA)JBjrUGQLg1htwDOC~2rw;sKPErGMBZ z85QXc!CP5Ns$|^T8247jy^V2iHP(kFl1^rS3N4t0wk)O3e;p6jA(y>qb{leOLoRK| zr46~XA(uAf(uQ1GkxMIbX+bXH}J=Gq9*+~GHOLet;nbq8MUHq?Pyv%TGoz+ zwIj0+k=cjHY%g|qFE;#lZ20fk@ZXL2bufHaE4f2S^SFLE*A$SSu`=S=DI8-c5M%B8GPzyUMDM!r! z4p7d1YHg*~X39HEdFQD0ckXOmjn{kyZ|X!#&v8wwtsiP$)I^;PYCcIztfihL$5C?p zMv2EMQLRjzprkWK|J(6Feq7Um-SlIX(S_2y;E^}2_aXJAS6DjuTLXWO&^omS-2!*d z+W6XH<7)?YLh-c)o*rV2@+4fX!aG&rovQFoRd}Z=yi*lDIRsZ*F3QJOXpeQpIG9!) z{2Ojo!OJQ*S#=RUw!p`hzu@DAGsKh3l&y2yLD-KPM#1Y({0t|`QnSSr?*F8LbUXnb z9X_6gk5zE7N-7?z8DkmS-TZ5Te=YED5B%FBXTileU_Mw#B(;b!QuakL?i5lwf|L#+ zr5dE9BBxFyr6L{0wnpS`Ut?2pS2kX)*(fh?ntW#%t+UMjt!!!|hgxq^@;YUc{Q+C4 zJ=xSi{Bf~0EGtW_>sL#dy??;${R5(!3-3!`U)gcF^_fQl2H zL@~|OT}uR0PxMmzxA!j|rmRQ6qu?>{IOV-S883n@;3e=fcmuo%-U4rfcffn#Bg+2- zsCyvau&?fed`GJ8h5SIO?uYCk{RQj+zk>bXH*m50B+mE;b+5#gRNXH@ucW$X;z|1F zm87qU4w~W0*F*@-MDndf@vTJgtwiswMDDFb?P|qAMG5urX}e4%;z%P^_gnHP)4IC! zH4#BGQFs+mK|N7;6_G(bQFs;6K|TEH#J+W6-?kHds~C1W(LpV%58GLJ*xnU~)*`Xv zLd~_+d|J(=t#PQDC-2A8@XiI&(b$v{Y(XiEnE+UMW?o!_CLds4T!Z(iL4q~Jp@)#7 znin62n@W;uUVKVihP_(Ph~ETm2Dg9>U?aE{sJqaAHRpW_hCU6R0ndWx!1LhW;04<8 zBG>|60xyGCc&f!3hc*$1*1&@OuwXwd*bfW#!-D;=U_bNOL&Twnh(iw%haNKKv(~w6 zEz!miEJrOK?FiPR7LTUxm>q{L`(ev**s>qC9EUCYVasvYvL6q30E=<}*6fEh`(e#~ zShFA2?Elkz)f$HeV@Vu{Lu;f49DozB&LQ{P=8*esbIAQLO3fknGk>fh4y_>$t+BdDd0;>FD)xJdl#g zYs?O|Vt=fw?x8lD>Za@|ch_7PJ71pekH!m)hC9>oESKOLmtz5M)qh}(qaFEwkNm$z z&L2`@9wj%+vmBvouturvc4%XMh_Ey+_hE}RsR}D;8 zu~#jvZH3MCu=zBzs?*G>PBW`I&8+G)v#QfDwhqSD!Pq(&TSrgo=*b{@;z&>W(vxNw zTL)w7U~C{LFtZM3*2By? zm{|`q>tSXc%&dc%buhCIX4b*X)5KqAm?fPiPCmm-=`^wO8D>kTiI>kXnz4*#5ThB( zNSa{hX?$ND>^zOVtAm}V8QD76c^Y=s!MIi!*9zm#z_>Fo?hK4O1LMxXxHB-W2F9t` z&1q~|9c*faO*OEo1~%2eqB>YqhlGwHp=0>P{rJWGav==6NB>3Mix1&xUL)RCe(xBg zm(1v?`#AxOUOg?2rp4;+YzXrIU9k%}w;<;>o?oWj#4g_>?<2%6-@~}Qq7R-k3`7HU z*E$w2*&jKmyCkWktngzM+ibJ*N;pvoCo18@c{p(Zt9Tr%cpPq2Vik|Wk4miKal;7AK>Zi6RnSj1+yQf=tPb+l(a+ZVq(UI~}Z!lh=o)C`xJ;ZiePYKBYA|HBNu z5}sAUvr2eY3C}7SsVv(_g)vgqSjXdVtrD(P!nI1cRteWC;aVkJtA%T|aIF@u)xxzl zxTc=zskY71tI;+!ORvT{Dh{57gOzabEF7$agJz#AA##fhylMR2K=5F@OyY)O$^ApcPaB;@E}(BA;#1R z=C;HCR-~%#-mAN^&BmE9IIf;A_z^xIhog<~??brvE_`d_itn(bYCirEmh@GwE`k$* zXiq$)97J}nA%mwG^Bl%J7A;C(%(pP!evIW-#_$8i?>~&$D~!);jLj?bXDjXh9352i zj9enR(bP2EV7$8j*Gw(`)MBC*XG&K0Ypwd|%oQ!fo@!k|%}jnLx0+=&k>d=#Y2wKV zHB0fLRo;~CL+XovVC@s_JkHhXu1EtBKs8{-jD(zFe<#fE zL{gnb^qGP`PXp=93p2qWkPE{GGhY}&I+RpJl#B~L{78?e@e~o`DHz!bBU@qQYcTRP z82K8EY?YhI{~-C*I-#0RbQo;3&NS5>(fF`v{2~~D<3fKT^QF1ZG9u>n)OQoO8QcOkfQ{f*e%W;!b>0W=2b;kIKt1R2 z5O|7qJPn=!&w}T`^Wfj$1?qYcYymHUmx0wMsHd+&8OKxjggTg8k58z_C)DE;>cuzf{$qHC*V`s^%>=VPWlDum!w~jeocG6A>X&8-;sV#`UB~Yq&rA=G8VhQ zFKq7tzk>bXH*kXyosjk@CkMJ zggSge9X_E2t*FN*w4fRF_=Fa;qaL5of`-&%&+70Ab=b5zd_oI8p#h)Jf_-a{y+}hy zdy|HeMxZ@#9J^PKU#Q3S)#DfH@eB3%g?ju#qvUzOKSa}q(6+;9+aa{=Fn*y0jjP8m zwBQ#Sh=0xz|1{zo8u1N{XyRcs@erC=k8h~QH#A{eo6y8gG_ey+d>2i87fpN@pU^4q zg?p~BO+7{QKR?^TQ)pOYk6=S?MmH?X+U3<4OSX{%!uTbuAD*ZnqW*M z`l8}3PZ)C!N%TPyE=a;l*&yCHrtCmpOlckMgj zzWqrOwVH7X%iIF@Ti|{R+;4&VEpWeub~Ry{Yv5fcyz7K_EpV;{zA3I;_^eqI{-gVu z#q-EUJ++}?&rU324OXxPnY6GTqn=+pj||jP8t0LNdP?IwvQSTHv@n0JK?a>zL$$iz z$(U3#Cd$(M)e}2^d|IdHlRBP^iTznkbr0z%eQKvqN8xYxGnOgLOwxdA+ceS{oLBds z8hIO<;V~g6cL3P+5b$9y^Hs~cK(3f>A>!^V|UuIJMGw= zcI-|&cBdV?Q-|HD!|v2!cj~Y^b=aLc>`ona=OTBXi8r>QzuWNnuhX8tzFYK1o70Z$ z+mZclY)&2W-;K?wg9E#)vQz9{cXqA9LCn1!q%L^)||r59LL5S$G#lLwjBQ(o6-Si+p#IaZ#8*p$QAljHEX0}gk<-wtd^9UR_`EwSE-<*6+=+)lh`UG>|C-B7D; zO?Vy^_cRcpWiuxljXq5$790oD_Q9|^<_!+E)!YWybqaPhz^+DE)c~Uuo6f?VeK2Pq z%-IKX_Q9NeFlQg-?t}pic%BA4PXnH(0ngKb9Z^pb1;7GtSfJvi2E0xKUZ(-C(}34$ z!0R+1>3v9f9}?b&WE+rb0}^dOnhi+v6w*9}G#l_B4S0|SJV*l`L_O!zh!h)&t$H?zv#2Xm# zv$B9ZW$fe8vDghfaq2<@l82tFI|kRFsTV#eZjDVeo?JU~Zj5c2UN%OytT`v{|s2PB|ciV|Js+H5fiU1mk z02+w^8i@cJyDYJaXH_h#;#l>RyVdKfyL8Iyt2=ed>#MtU%In)#BQECkFMJ~YB>u$u z)V2%S)`_-tqHUdMTPNDqWb^DzHqYK<^XyGF&)$i)b)s#Z$hQ;ub|T+Syb%xHhzD=POSBsvLdC}_Hs;BDOeCp_Wuoa#42T2q zAOQ>j1F=zRo|~e7%ZRAoynM@OoWm#WHDZ==oL6&N{0pO^e)n>Ukx~BgTO($fL8{`G z%XuDRHWonL2dfsoyoZM0VPF%!NB@1$f3@mmhIdWqr}_=3`kl1;-MA}f9QiAG!|4C! zZKvybN9E1NdrCL*rpnuRbLbtsr}Qq~OZpFfr*RLS^q;(`RK0uh0e-*r5O1Y?g!fTC z#&6x95TEki(l2>S>9@R{^he%I`V(&?-7WU=p3DQhFY_>O&O9oP@n+3h{ecDu{-y-Wx zVZ?-e)h}#g{1c=(HX|!8D=;W4NoJ&{r3PiCnS;!+apo`?G{ z^KR#o;gR#E{^I5Aljd{1Q&CP>d8y^8%n^ND@0J0nL&LRApMAf|EXW?55*iqo8RVe7;(fkM z+j_^y5hF&V9J!4nE@S=NU^^oMy zu$;7+Xib6KBmQB?U-%*X5vF2CC3McD={Jj`8uybKjo6Iz%waOkN%Id3myaw+4+$9+ zJvccy==zhN%sP^nH`DU_OS|*(lupKQ)o!lh7vz5Q$V*0N`liPYlxczAxv9&dh71f2 z9yMIPHsaCUIk~0M@ukY55u4SOnY>qajrNJUQbscb<~Z08loe!7lWT5?Pl;a~zGPAO zqWF}4H%a|^uXW@5jW6jxZk_iH>e?*+m!osC68VWND9k#J4s(JLxxQo*&yc^Q%97o2CPKU1B_%7_dolmpL<7{T!Zv}iML4(7Nb zdBMb$D>)j054c|Y!pM!nM#zu?%W+rj{a)usqAhXA)>(T3tHgT%WNJ{LkK&PcW>A#b zDZoE4HPbs*22D&`a`l!t-?T*5>=<+Q*85Q+C~d#Y&m6R<&#~ ze>U*maVJurXdi|}ZYbFjy-U0Q{1R>Gik!R1Q^dJC?Qz!MMZC<&8md9q=qN842dyBn z7sjN{GULY$%=hL)5 z78x1^cuF&6gh4~6=%|4*D=;lHBRvk)S9+l88Fks-S-rg613lbw2F+hSZFabM$z^xn zwqZm6aYIL!jtkLl4xKn7DL&BE$7~MBA5$^g-#jc(-f)%U@PcJ2UXH3xeqFA)&`YEr znGu=cmu9lt6uH3i$@a+~HXQnCbv^LM$ zESenWCl6Xa_AME=YxTm(^CDeVJytrppzrvAfDB*l{tp( zQ!=AqL9+PaaV6R~_bZpVy7>RN@CrHgm1o@@E#LdOxUSq-vPu0fS|yBf4eiG|`@uL< zoL@$XeAqJjoHlMm#rdy;w73xX6+vsfkcA(XG7dXo$P<$o;2(ib8m7`uZN|n$drFxe z0l$LMhRPRyTc0|3@L-EiR&Hj>NLielvoI@D=3Bl?PF(;mA+)=+?w{Y#18`c+>hL~r}9a0)%dThk_oa3?IR$Tdp<++DF9frC~hu$u26Gm)M zr5n1%|0-a3C_l8zWN6sLiE{YLm6ngtG)*L-1ozO98XCxLdplUkI*;X0VDlAwLUUB zHhrjO*E&W>(oXq6^x}TQJd)-`IOfv_s6eAdIUOzESm56 z-t!+?roQP~;F6dzPrCem@JCBlQsz~6`p=yIj_QSq7Y5^1%*rCC`K9?q$3>d`Op&JG zk6!p_Y^vqg)Rz}Zr-j;^=f}SOy0+4?L0*eg%D76!A|8fU6n=~%Ds5JOqKSqAFgJ?m zu_j|PJ+`c4{`>)n%a$#$v`O*CyM-hB1Se*_^m<n5L_|jW5Vr&dc$*0p;$jmdyK-!6 z>x)?tH>J;?-Zy>AmTlUb8%wUYbQJcMbH7gUKmQBup$z`dx;Krn@`((vf6L(A3o_JF zEvIR37B&>LE32XMHj$S%wxp@N2Fm2)me*t-%Te-K8VfB|s+Zg`(PHT(vVe;|iXT?` z7(InEsagCt3m>nW^Wd5_n=hRkWxDQ?Ri&jXM~s$ziYzY&zbY5M=I@jju)*^E2G8Br znzeD(R#GdTi28x1S)ME@k+a6iY|Gc$o0jin=J~Ozep)J?CcpBWu0?pO;wwPckqig znV&8xS-EZ6S3h0y?kXz&$UzRZJj;lsI#{ofySN7D5|0>sBIPd2%_TCKM%-&JQ;j43 zhSv3#X>=&s#Mh_n&M&^Wv&8e;Dc^dM`5kTa`LWt;BYoV~mhoJtc_EMIeiaz}rfXjh}dAg8b!5%ePlv zEmPNiyx}I9de@znSDxP@mu}l?L76StO}zha4H)oXVb z7Qa?otG&7Gic^Izlol&3%+{A1b*r~m2}dRV2t%P%d>LzNiX8KZQFHpRvQg6_&srPg zYSjMD4G+cK9(9k)w;R^Ww7c$9B`UTLqBZLGoWhSGi&Tv{+V zdhbpDctg5aTD3RlEqOBDQX^doWwhlWBgnf8WWDw=Z_f}iD!@NX3-GrE6{<^d*5E&! z*?{zapy=mm2S+dK>wMeD`}3!-nSFTlyeRXX+O<~<8#u6d%5I_--#Z7T`1Bfo>HA)e z`3#U$(W)kCq0OrzR38u7{PS3Cc~Q~%d$r{%5AR>l(#eBFj94BD8W9@lV^2>m8++}U zwIw?r7&%^6Tf$Z^Sg=|e`lM`HlC@-+22(I=@=)npFTYj#1Ve(r5xgp$ewn6{Pes7W z^ci1Mw400$E4{MMrWu?1Tsi)(O*h}X$zaN&^A8wo8Gn8o*J4AoP_Bhty){L0uJSW2 zdTr~Z-Mgp0x^2>)JrMm@+0XKngri5DlLvU$#u1Ih-@Pu8kMJYt`zEAN=n1YrsLRdUl#3EcLg@qte1^ zY&2PY0)xy^hCfwv8YpKElqK^z4&ZU+i>55o_dw%asX^I>lY$}Z{8t4T2FY*6loa~ zD~qER90)=2*HKP=WLm%r#lwaTF0PP+Mu*=k->{U(H}VH8Dlzjvt`#Nbql&aAKsL~x zzWl#8xPR=R3Y_^v;O7zCFgy` z#ie8B78S=2AC~KwoAy@kvFgd6#*%6%dIEzvCcYqIq@SNE%|4pO^;Z5K;(Ehlt92P~Chy8~E8?ck zt}UP0JhLR@#q^xVa^)@8Tw|%f?z%qHrv5f*lA-lc%tS1VGH*BwvI$bnmEr>Y)SS_W zsEcw&jTt-U@+lQ-hNRsQR^;zDGG;=4?jdbg-=wvclWW}$YY#CSwY=swbLTWepQ7ZK zlxeIc1Q|hGkkVTwp{PF=%O|$JV(Nlh{ykPePdXbZzrO6p z>49chu9goBo{g5eEqV#a%nDHdlabG;F;i#tza+SqY%Uz^H`Y1I!^3hfxd&h?o~Pze zG|%wVJ?7U$&5%@?p<_!^Umr8)@}ZN%99FI#lD>7!{E{IvL$&4dgR#+l!UsJ($`jWd zGc`Ok^PeL;9rJjppcLx4(}~(9^?pL)`YtP$VQ4qYiL&^KUIV8)7DSI5JaXiiC*&Iq zQ!<@~x{X~d&G`>Kq)I5DZQo1vI|HF&H^Yn>eq4?4Fez_D0Y=X&D2^?NEcEqNst`S3 z(cRL~(z$eu60}qjlHXE-iWF=xC%n6)LF9w0k*#Ntja);qNWy2w(v`kgk$y+UJxmLN)SGg8%?w4jPXQDjOS%DL;xn|-Mk562C&BVuK z^A}&Vb$m%0{?<~$AV{O&>BwlGO)f*Q1Uys!9MvT=1 zX}IzJa+T$T47O~N!E#$cf#shC1#+3n9jxCd2Wd^pmih%TrDFZS%tC>gU<-R3m%++n z97Zw7Q+{bq7&;`;oHAUiC^M&KXQ!IW=E>nH+OTW$a(sP<3|l=Y+acd!!i;MYGQ5X) zWF*`;vy}ZT>KLa-iGNvjALjdv8*|9?a_3}Kdp~L)Wvx9+Rb#6)%hn1w7@!)3L3+fT zIAmx7waa<-+AFl-DVeK>4e|BO$-8z?7L}FGyfGoeV~BS~!ZkA{u%E5!C=j1x<%^6c zISNHpYhTzGr5`~~^4i`H#-#iBxDCzhv)L~?!^d^l=R84E7qQhr!^Y1B(EJzU~3(eg=IK|#TU^xQzL)NN2{YQCk)JI{Bx49m}2Gr?WG z4>O1V@vcSg#{;YNdZ#G0m>Qd|)>AWCR}V4=`m3dNt4Z^h*Cs7V9FZ}iPapmyE=d}Z zF)}P{WX7+T6&7ArSfMsY3|KVKc65Z1cQI!QFH_?|bffjs?pL!+VmoW(Nr-3;;E%dZ zfVYmzzU$Q~FK?Oh(rvFgzw=JXd+#~R+Es_K@ipm5 zQa-(OMb3~dqZ4C3&YKWsUYd1n&en1Ly%Rr4n;K$ik4(Ka%+Wt>y_V~hI4dS1=k~ma z!WY~fhk4Bz5FR;nMBEsUi1AN&I9N_(mBjY)Fz1fytGdqrM7mTuYF}G~XJ{->#PibR_jg>!%Vh+f{zFO-S)(|7gFNyUiOPe<1@~4F82@wZ3gq~bid;<0i#(;DwjwuI_3;k* ztbE=+gZH%xFDld4k~_w)8$W(sU~1~b^mN&{Zrr$a<4&eeN=uuR&XrmGKa=NJNpw>) zO|(O?!_W#d*4lb+;zQamh|IWut zciZd=0f+n=a!|WzcAi>{;`xIp`5TtA%M%mqARZqToG4FxFP`}3zQq@KVxME$u5M4v ztW1kzPRb~$btS%rPCyJF`N--5^Mr+qCdkpFv{mOf$-;S0FUUW?iFymUxB37}_DD?G z;KGT=SPbTPe?5-7udFi>f%UKos`CM5$KjP&!QOj?3CTf2$TP(wJ(8> zsyh3gb8i;1k!6?}0!he15|WS*Cd*_c$z;!DV<&78_5h=6D-Do`taKm`RXNTGrs zR={edw*6|YN?+Rww$}Q7Sls=ot@^evE(LPu{XgfNJ9F>MBtd^K7*J>KInR0SbDr}o z|K~YEla@7quW{Qw&0{>Nsh%;4r>(7P-@b_j1q*YzU(|@b@~OU8vUAHEw#c#xjQ_=c zf^D+;wqO0~^8BvgZON9(lrM<^Rc^?z?!arvKK?(3^6#>g2M`2#sJd_m*L!D9CkjeesT z{h$=V2@6OB17|P`B<7jueMpJq5}6JUY;dUzv91F`P|FgHf?5-_&QOwj^=iPP@dk6e zF7^PVl}GL!i2T+fHHiSeFr|4E9Z`<59Dm@xtWyN;kr~J{#qAR#5Vu!uqPV>(BHro| zXnDTd$)J=m?-+)7ggNfL?RCcm3K{9KZ#Hpp89c7RMp1EgN18{u=k-yhQU18tO}l~O zR5j{BP6_#weo8P#i3296d6?(6Hf)_XWvY>$A8*^%3$sKY>i{1MGGZLdl_>%zlMvhx zR+f2Kr)_hbJvT1VT3WOzdPGia5~7&?6`w928(U!?Tb>nDQ2~uFXx<{Z2Tfv}DVOvn ze$3Wv*5R)r%Zh{6fgD8c2nXdaK|Y)3PWd@cM|7S^ zYaC4}iN1pD@foR;)X5cd)B3-K7xcrF1r={nf@1ynVZ+8Jr$C0_wWzhq8R#oeno*X8 zq!bjo{LK&;WDj1c>Um3P*N#z8p7r&Aw&??Q@}zR6{e$)|+m%(?J#0PnO|>_d=OOZ7 zOPVzm1wjLn5{`&Ss4sTfufOh?7m%S)=l1zk+O()gPtG%CE8HVkZaST4q_!_ZR)wS* zG7coEhOC43A6!m(>Onn1B0AW!kWb^YKjYFVo~k zE|G%vEu40e6XPgSPK?7G;Zh>TKs*ly8Em$HKq@X`{r-pvt-I?Xxhus|K`(3?i`Vo$ zT4UE9x$jiIER6RG_?d>lJ+!{hg{vv#%PKu_@ZgD4 z%+<~+P94@RH>WUH@pH}*pS6D~-h$Gj9smbvM;me&uta$%)W4(sSq&}JZuaFl)GXt6 z#>#lsgKI;+M*{O6Bxgan4~?{9KmvsAA*maBJiM3S@mAnH1&%jS=tw~y>BxBU(u3#| z?=N`BHt{|KM~%{@Q;bXcGM=pOz;V?e4hK!~d=R4pC$pJ&9VU5Ax|4VfrzCJzGJHt7 zg67f|L)rp|A0>7P15PoYeZ>9%IYG<-48}VnNX~|b4;LRfQhfMu@sEDQe*Yu+Z^efn zrt_lGd#^HA9Rm$HntYvb?s$?igZ;|=PNZ@z5eW`!PF5mdFuAIobu^b$R#ul4ZrJB3 zsi>-+w_f}2$)%s>RITkP{(EM0;;o}@AGi1BpPja?ANif`cYpDSa@Y5@r>oc%YyY!b zn7dw!Mqq|KM`Uy$Z(P~}b2^|s*O%96fRLGUJD>5hcZe|%MTqo`kq^N#kMVsHD<2&P zg;WL7Ax~f5Fp3Z+^GJ)hD5-KqTl9AtI2KvF^iF+)jmE1qkF<`zNJZp^z#HnqN*QYI zqsK6C#g`%4fd%=qL`sTW4JA*Cp?l~C6ic>9I3_ya)+4aV-7hwIXS3(zsZ(8Dr`FuK z>bH+{w`wnRY*fyyUitoJoNvg1D5DX%hxv<0EO|~gva~4Uhma`85y5cg`FDz~+J4}f zbAC=vuOnlNYtBk%jZK}N-By{ur6{|5RLR|CO2(2YO`T@yRL%)L z*ANu2v*eQL6uiNCKykur;DIVM-cUr?Z!OvEY4;38$DVudr3`{j$vI9**|kxkpF+?W z(yknesQ+hM7U#%~OLFGfu8*&>D>j!B!;3Wvu0*Z^(xGbLfg}B=Lx@XCk5k>RYh7YgZ8=dVyd#{ z$>Q{x%Nv`gEV^T+d)^rJmhPn)#jo&O;H}!+qV@6C-Q)8BcUF-XY*15Z*S~e(iq>~zGh?9lx{V3Sjp1Zu>AblZ}oYQdZHX@F3*tyxfo_~^(B3`D!G8A44EC)7cRIVyz) z2$LK-E7w5L=mB`-+fSh;3dbf2G0P0GZxrwIu+)FX<5PML6gs`l^Au@q%7W@h0LTR! zsejK3zBv$K&Hxjq4CHvQUUaR5fX--kPi&Ze<>fp?@h-{98D>sz(BYoRL^1X4g{unn32uc z|Lt3EShsR(1?$&K zSuwG8J#U(=txy&lHLBPb7r|#?lk!NJF^fw3!JJ7QM{mlMO=X)ndLw8o3(v418p}A;aA2 zcaQ+X+E-!ck+DB4H>>m*($IV%o6lt=BtG-?w?# z%A^}duUmN6riQ6Monm9lBD762KgrP!P;QH7ej!`j*9n#TRmq++=YKCrMPSMSD_bQ# z$UGuNeupzs*RzfI`Bi zhZrwgT3f~;wU7LYC1BXOT+CFt^0R#9ggXu~vK}ilQF~SDpMYUE>TjLroxxF#4)JH6 zX=nMf&v9bO$crNbErS1qazgAM8M|omJ6#k7r}ID2ZsmX{o(Hg%Uz%S#vA$}OwPErS z9QR9*4sgx+^9;b64n?rDYqfW7iM*gb_TNYXs3HRnsU}JQ=QGeEwW*(;Iw0?X$ZHrdl>?n066M(!ENb+GV z7b#}D)H*VvDr!P&qy6*~wD=a~3`dj`^3TbUe|bh*TH;MGgA11FH6Q_F?X_}h>*%b6 zgrQ)8N`0Cjs6MSAWmv*MAVEdo(BYZ$Tm-W8A&}^#ctWDlwco&J4*^M6{v~3q94+Y6 zl4gJcbX!6>*J;6+K^s3k{bnxV2Ja%ZhWEsA^N4dg2uCsiz2f)`P4Yt`;1P=ImqY}O z>K}k|#Q7|AXGprm6v3p|>*utY`YBv{>eSyn2)F6T49)G&J3-N#+5u?Z zZGn_efxmw*Gz^JJCnSWbA#nDEs)x{t4=9WHQji^39ijZ0)6A_vWdp~CSUIkN>Qr(5 zk-H_zVgk{tN>RtehDie;SWle$*Ps|yWO)9UqwZuOz6tsrWP<~;fFPQg`Yc#mf%sd8 z!$ALY&zUf|Y3}~rJ{0b4?zA8rF5U&QJWF||Bp^?cVKe4PcG4**QY7Q~2`D_n6GQo( z{L~BOttreJ!ddIX>89s^_JS%p+YH$oi{YFxQun6mWD{iGtRHH^JNScw{=yg!Cd5GA zrRfw4MRQS6PZ0pfy?6GiwIPV#S5%Z!)7x9)Tfv54zLNSdQ2s@S&eaU;Yn{?FLva;W zw=JYMKerLxl{^IjSH6Lcqw=EW$HWZ5TgI4YZ}U$OCO+vLv!4T^^!tJJ#sL zc!c53pctzEM5g*bLv%x=eIrXy>;z9WlFgKZHkIc1kln4_U8zcfHKH}25Z>~n?rRA5 z;k+Z%YDH~fb2;2wmyZxRLa8?li}*@c8A2!=eocXx%#d*nR67$SGbF2*3N06c-<4d^ zfkTzvuaq2Q(MA%&4jGmK>Z_!oAF9kslZkgWCb-;+eJ;*J^7DXR$7%$6eG=tVz8QX9 zW*@E{#-1dO)31qUe@>pZlnKjG_kurT2jP#=pbc4z_K{)thpJ0cX6dQ`aa_Tx*CekR zzd5f=+TxmV|Aht>@-?}}GX45RaBc%Q2MpJMlErP6P-69BM@T+}K~d-&IwLs*r{J4P z6SrxunKSb`z9v8Z%q^ix6f}wo#z8p&Qy`@5{G`L#5Q-Pto^urS=hC94kY5Dv0K8zp zW+PJb&8kC6@;aQ3YpoDbQ_h{c^6#%%Cc<^2(hcj1<0p(Ym`tb{Un! zj7Mp5x3#&yZ77V<7AZ|23-=RR*cj+(m3lD{)$%=0_PEjC?6k-qQd%E{#+HJy{m$wc@+CKMMxF&y|%DqSZ96=3StP;yPg#h36%^n zQ2!p{%3pF+D&pEa4vlYNj>QNj(4YiSMQ2-#l>uF=B3sPYsQ6uW`}D4+4yPU(KxHJ5 zC>{|KKt4x7uPh5wU_bF^X$GT8#!STqrN*Z`rE4l?8SZsjevbDJEF^K(Y;1IOI+9Ar zT_+Qxzq2{RU|)E>c=G?EW+{QNROV;PN?oMe+C2L6U1&ptwKipn0mhY~G=Z!hz8@?< zQa2cRNEn(<7+zQ*w>d@v-o%g1(4^f{BIHn(L6I{QJt0Ys6k^Ggc+5 zbEUsZl~s)4e2yY6_$#aAAiTJ$t5D_){3TZP%67STrbEY#LykR`bCGbfdUdlEdJPrM zYfks^3g@M8XAsx;br`EfmGe;&C2llD}R{ zd-vQqmTIcn5;oVL5Qhk-Y7$v3O@RkdF|9p`NP}{wf4B0v#08W>W^z=z*-_87;M?7v zB?ui*y_ec8;{{k7ufbMnTnDLXDj3iBC+X#AUX9&N#PEu2#gd`DD{IH(sF8T0+tY(@ z)2Q9d$mXD~BHB%G#|kI0^GUo5J-&wTuGKfwZb5oahR*v>hF5mCFArDA%Ew_~Dv zyGJ>3P8%z#)6kR4!u|?52Hp#+M*xi8f+G?&9UEI1zG4{ zPz&IUUjNVA7mRPqYw+~}xEXB>#-$Hap*&)EM*u>_SlM5}H}mN$>YvJZ#V4rT-O(|# zbwTT-Md_Y`yt>qOyPaLiC|J8qyF71pX<*|BK4g`e?yfB}BT7*|t)gM8OHAW**5Flu;x*o{+eKlvxa?R4_K}CBvx| zNNTNqfC;bqBF-;4MOWsH~GoEX~3)rZiayq_lkK_nsAaET7rJ`tR&>Fm!`&_%f zy03g!ApPo{S$AaIm30TVLcd_|D_8uZ2d;p01CP}jsb8_H{QgV3%6|}o^_AZ*xF;XK zp>M!{b*1tgPBFYxh-UD&_G6AsV$mYvTyaeJw21yA+)=3Xdkhu z{TEAnmZz-g?PgJrvRfXD?s&iB^QcHACu)oK`|pSx;1}w6$nt2!Uh?$l3yP-YOm+u1 zEjbKiEU(K?3S^I2GR z<5og{%qcF|%&Z$HRxIbZk6C@Q&(EAZrOyNDC#_Kdok|@kfaJ8oIMjPmppgL3vIz(Y zp;L(7mm=|}PwvoOC%l`fIR#s${f%Xv6R~r6jyN~o(<3}EkY$}LWyJJa*|nl{z&>ijv#`2&q(DY^@8H4oC^F?X+hO@gG`F83 zIiSucMc1tcUiEU#O5L5%3j}X3jnJZO&~6}n4-C)|}Tdd6_>MV0{AANBvuJdHFK6TO2sDFL=kn_GqR?~*WGVEIXq7>YW^l$5;%l*=h^m&6 z=A!j_DrTQO(?~^Zn5lf77qvgorUL(^%kRN!dDjz|h&K-KuBS-2NM@=%rfs#yf6#4ezCztPWbRNMuW^7rpBRN(Q73xy%V z5y|WA;p-wxk|-@U-o__fvPpLHIWDx7!S@bY8+h(p4PfeK@;C6fh(yAi!1J)jLQ$QKQA~7dnh2Q$ zP_`L{s*l@yK@sfKK#IuT(-9Cp%6bifX)}qx@kOL{?!E>(DN7K7yVQnDAKOIlpN#ft zFw*Fkx_zow3Oo(k9dxn;Z6=s0*iO9FAmLjLz~8NoXaTou)0&_V56%x|Ts zZC=Mb-O!dRxCBd^WCm|J)Z3sDxt(DIW=+*??O>`m>$W!a6m#;PVz9LRw8k?-ObiW- zbMy{l4@A|-Tv=WGdVjOAQ<+iqsUplZqfYNT)`(o(+lXM%p;wYPoC&?cGkT%go+Ovb zxLe8Su@gNzu^LS?EnK&bnQp_7V)+3J-n2T{oa-3)Aj zUdbYiD81d;A7t#?*Y%8Dk`xJ?n~c>Nuw`O-erQ>nfewy#({a8O@lg^`yGOM&h_?A@ zAk~f^Uq#ImD915W)P!@Wog%ug=@B4f|22|2Ll3qqWY8M=uyMx(Jf&)mANr7bP;qeO z%2)>N21D-`?ZZ~?oX7?+uaIG?&n@%Trny15VeZE}j5D9+nHVwe8ARO>BSW#PoR_jd zrg`s2lNveeDG{UP{pR$Xg>hDCpcp1TEYEhsx{t~8UCgh)InwQX;c^TE2{%i-#@GGk zkc{EvB9HoWRi1k6$z|xT*t0m zt|dJYN_}`q9XncIzVL=fr7mo3XFYs)UcDS<;h04mVWWa-j(NG2L)XA;zB^SfJ5FsJ zMG&_Ac;N*?6RT4z=BM^AKhVQ5Lf_?x~t(+(@ftuRWGqEN&>o0#DjwW1ipY-8yaQiIY3 zWf?%%VKP(#EkM~9L0W(V(1o!A==u&fS^{jpE%FQzbZy~lq^K&v;`wi3p+XSJ{tBS( zZqZG_%r((jL6y{<9b&`)8W-qQ2S)o;6d%ayP1VEC<$O!cwVldx-|FpD3~u%iM{}Ck z>EAQR`L$KQ-a@J8#|%n7U-fJlAX@Qz#PoL z4O9rl6P%@CR|2Wfy_=vqeK6eTRon+HVDKuwD?1kqRKJ5q{Z37`zU#S1 z!?(Kvooi8IRO)9X(AuJZU$8P|6T99(3%{BFKgF;(DQyL8hxR*Gq20tZgG@nNQa4Lz z=O~gVeXqJgU}kh0>)Ur2MwhI+q)lUwQN@u7Kf^3;%ioo9RtW7){z!(K{99J&hPK8h zVfltn)DN$Y6rYr^k1UUiTU)HadObeAr(Svf!Ua52hEGn(Pmr%E!zb(`{t4ZcI1f^R zd0cpAUQ|Myy`X?JI7VZw{24+Q;Il0JuvR=eUk;1`@VzqjG3!-c(9ap(v5U{Jgl75p z0)F9JcJCgRXIU-2MKdAI86#Snp{b}{&Q^EiHajvYIGyz|+HetNFI;aM!ivdXf8 zEoa;Lih%}Lb$JC#qZu^;8{$8BQ%g4MM zo8GsOH!Re972+F>mR4qAkCJWW65ZNEOtD<$mH*F50oZ#aoi4@Q!16 zpEmX?Q&ijyNU;iWyA!$y%Yt58&HC!<^)-(rSBx56K1zADv-2rW=bniZH%yp7Z(t<5 zh`vL1oJ@GjrY3Jc_BD267k#bV@I(%7ak_#h=wze{F1%oIAx@TTvr)Qx)F@~4FSduL zrOaDVRG1L%%A26v7$5OOMSJ|hZ%3=8QP+{Irm|Ntb2{D`;qP!3@lGAG11WVJkF+~c zBNj0)KWS-o$-3(L^zp;PoS8LDy>47uit@wZ%Svkw_e~k5lt|f=#N$E_oG)*c0MsNz}IxtwM0s3>pX6Ar`p+vX{e0 zpuSzB-zg~mM{z;TirkdkWsAqAj8)U8*TfC4U36WwEurE!h1s#Oxld(}iHjYB^^C%L zG`=3vK@m7<$lf=!AHmt?!m@0?EJ&JITH{<_-7q$PcvxZfqy@=&nXIH(nGsvQwCdRO z-YB&!^1ig;bEvPQ&AXc^mW$%u5{{4}tfAlSZsV(EmLl&%xF_yaE>jc@&UKxm#(J;H z27};NF23;<#)ehs2yVmom8tdK;(%Uz;I?-+ZkM}ASqK+#k?si+BH$dl`{Tt>%^%SL z8GQmHKr^J#VV1_neA2ksCyYeH{kZ4uRgy(lXj!}`SQZP+qKG$#m0ZP2xFk|V;sBC3 z+b4+y@qS616DWwZN4!6a8vCqt8J4KyJyxWWER%W z>`AJ#-L$@Tcmu+nie(P-D1FL$@DMQ_+RxPE7u}vwNkQVAm1~>xJGbk>Hg{`Fsk^|f zG&!ALef4ivaQ?jZm$zPdhbQ zhU_#pwk5S35dM7;5Gj}g9j2{P1+(alYgWFuZuP>pW=B{@UwY3-M_AhO-}dfoYv0?Z zy#Bbh<*Bu=thEo@vC$qIQ=62otxub>b23%Wm0|45%KbW85?7U3aRg_u%UboJZHHbP zwVmZxDs%deDx>>9qxGhF=fQjY7oFMRhzj60j}NfhU+!#Sms*dgldqmsCpR@+JxSjl zk8dl=Ke_J58^i@241Z>nMX~YhHEk1`=zG~#EuSvMQ2yCu*PeE1N0XbEu$$OyJngpp zCwh#g(wRiJ*tywb|Jg*h28DSyC@vLuYx1{7#u!Kr+w|S&cRRn@w3TgY>c67)UR|Xq zcrwkhQAuLwpo_CCPK+aO{F43pp-yrD@KG~yj^lMv0c{a;(4jC~iNsw^jx=`Qwn?>x z)ec8hQSGGL##cEU<#{zVnPVIZOHN3%zg<(~utyJ9qoV1*HlCO|tF*klbk@`pC1urx z%zA!Yg%YOJT9YEqpSQ*&MPbhu3!E{ncBhv(MO0L=CbqY*5?)-I9a5hd*c? zTbdYKSDv<4bPq2Z{a_CIi5vb88$M}l4El*@#nhJn+|*l~;f~+;(Cx!UmKgR79A7$~ z=_l8Ped((Iucn%+O%FO>`p{ilamS$&Tt#+Z~?}XnpJ${jf)tuI${~v`fpzE5}oDlsW(h0h z05FQ6P*}lO?B_kOV7W)?8n9RE*pR(iA80V%v1aJi3d@c~DK19nG44Awa^Jy2z5_l9 zD=>V*eFyo8-Lgkm#iaKdr!YIepKc1g#iO86M3G1vqHp|B-Zx%(($F`)#CzECE~FRv z69;L%P`7Db7veU}M=XC)6X_`{EF!2dd7b8s@(xa1rl}^%$5=eSgp+RUIf8nC7&_^o zMmc_g-`JzZ_;2hP=RIP%EWU&FKqtvNd4A`+lPB4GkX2%C=u+$8cLuyTz?}i}hP*QX X)(&e`.tar.gz." + ), + label=_("Generate voctomix lower thirds"), + required=False, + ) broadcast_tools_room_info_lower_content = ChoiceField( choices=( diff --git a/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po b/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po index d5dd4be..b7b5017 100644 --- a/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po +++ b/pretalx_broadcast_tools/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-31 21:10+0100\n" +"POT-Creation-Date: 2024-11-03 13:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -74,16 +74,31 @@ msgstr "\"Derzeit kein Vortrag\"-Informationen" #: forms.py:18 msgid "" "Will only be shown if there's a talk running. You may use the place holders " -"mentioned below." +"mentioned below. The info line will be shown on the bottom right side of the " +"lower third. Setting this to an empty string will hide the line entirely." msgstr "" "Wird nur angezeigt, wenn derzeit ein Vortrag läuft. Du kannst die oben " -"genannten Platzhalter benutzen." +"genannten Platzhalter benutzen. Die Info-Zeile wird unten rechts in den " +"Bauchbinden angezeigt. Wenn das Feld leer ist, wird die Zeile automatisch " +"ausgeblendet." -#: forms.py:21 +#: forms.py:23 msgid "Info line" msgstr "Info-Zeile" -#: forms.py:37 +#: forms.py:29 +msgid "" +"If checked, pretalx will periodically generate voctomix-compatible lower " +"thirds images and make them available as .tar.gz." +msgstr "" +"Wenn aktiviert, wird pretalx automatisch voctomix-kompatible bauchbinden " +"generieren und diese als .tar.gz zur Verfügung stellen." + +#: forms.py:32 +msgid "Generate voctomix lower thirds" +msgstr "Erzeuge voctomix-Bauchbinden" + +#: forms.py:47 msgid "" "If a talk is running, the room info page will always show the talk title and " "the list of speakers. The content below is configurable here." @@ -92,11 +107,11 @@ msgstr "" "und die Liste der Vortragenden an. Der Inhalt unterhalb dessen ist hier " "konfigurierbar." -#: forms.py:41 +#: forms.py:51 msgid "lower content" msgstr "Unterer Inhalt" -#: forms.py:46 +#: forms.py:56 msgid "" "If no talk is running in the room, show the time and title of the next talk " "in the room." @@ -104,11 +119,11 @@ msgstr "" "Wenn derzeit kein Vortrag läuft, soll die Startzeit und der Titel des " "nächsten Vortrags angezeigt werden." -#: forms.py:49 +#: forms.py:59 msgid "Show next talk" msgstr "Zeige nächsten Vortrag" -#: forms.py:55 +#: forms.py:65 msgid "" "If checked, the value of the 'internal notes' field in a submission will get " "added to the pdf export." @@ -116,22 +131,22 @@ msgstr "" "Wenn aktiviert, wird der Inhalt des Feldes 'Interne Notizen' im PDF-Export " "angezeigt." -#: forms.py:58 +#: forms.py:68 msgid "Show internal notes in pdf export" msgstr "Zeige interne Notizen im PDF-Export" -#: forms.py:63 +#: forms.py:73 msgid "" "If checked, 'do not record' talks will not generate a page in the pdf export." msgstr "" "Wenn aktiviert, werden Vorträge mit 'Zeichnet meinen Vortrag nicht auf'-Flag " "keine Seite im PDF-Export generieren." -#: forms.py:66 +#: forms.py:76 msgid "Ignore 'do not record' talks when generating pdf" msgstr "Ignoriere 'Zeichnet meinen Vortrag nicht auf'-Vorträge im PDF-Export" -#: forms.py:71 +#: forms.py:81 msgid "" "Comma-Separated list of question ids to include in pdf export. If empty, no " "questions will get added." @@ -139,11 +154,11 @@ msgstr "" "Komma-separierte Liste von Fragen, die im PDF-Export eingebunden werden. " "Falls dieses Feld leer ist, werden keine Fragen im PDF angezeigt." -#: forms.py:74 +#: forms.py:84 msgid "Questions to include" msgstr "Eingebundene Fragen" -#: forms.py:79 +#: forms.py:89 msgid "" "Additional content to print onto the PDF export. Will get printed as-is. You " "may use the place holders mentioned below." @@ -151,7 +166,7 @@ msgstr "" "Zusätzlicher Inhalt, der auf dem PDF-Export angezeigt wird. Wird wie hier " "eingegeben angezeigt. Du kannst die oben genannten Platzhalter benutzen." -#: forms.py:83 +#: forms.py:93 msgid "Additional text" msgstr "Zusätzlicher Text" @@ -167,36 +182,40 @@ msgstr "" msgid "broadcasting tools" msgstr "Broadcasting-Tools" -#: templates/pretalx_broadcast_tools/orga.html:14 +#: templates/pretalx_broadcast_tools/orga.html:13 msgid "room" msgstr "Raum" -#: templates/pretalx_broadcast_tools/orga.html:15 +#: templates/pretalx_broadcast_tools/orga.html:14 msgid "Feature" msgstr "Funktion" -#: templates/pretalx_broadcast_tools/orga.html:22 +#: templates/pretalx_broadcast_tools/orga.html:21 #, fuzzy #| msgid "Lower thirds" msgid "Lower Thirds" msgstr "Bauchbinden" -#: templates/pretalx_broadcast_tools/orga.html:23 +#: templates/pretalx_broadcast_tools/orga.html:22 #, fuzzy #| msgid "Room info" msgid "Room Info" msgstr "Raum-Information" -#: templates/pretalx_broadcast_tools/orga.html:30 +#: templates/pretalx_broadcast_tools/orga.html:27 +msgid "Download voctomix-compatible lower thirds images" +msgstr "Lade voctomix-kompatible Bauchbinden-Bilder herunter" + +#: templates/pretalx_broadcast_tools/orga.html:29 msgid "Placeholders" msgstr "Platzhalter" -#: templates/pretalx_broadcast_tools/orga.html:31 +#: templates/pretalx_broadcast_tools/orga.html:30 msgid "" "pretalx will automatically replace some placeholders in your custom content:" msgstr "pretalx ersetzt automatisch einige Platzhalter in deinen Texten:" -#: templates/pretalx_broadcast_tools/orga.html:34 +#: templates/pretalx_broadcast_tools/orga.html:33 msgid "" "talk code (MUX9U3 for example) - most useful in combination " "with pretalx-proposal-redirects or something like that" @@ -204,54 +223,46 @@ msgstr "" "Vortrags-Slug (zum Beispiel MUX9U3) - am hilfreichsten in " "Kombination mit pretalx-proposal-redirects oder ähnlichen Plugins" -#: templates/pretalx_broadcast_tools/orga.html:37 +#: templates/pretalx_broadcast_tools/orga.html:36 msgid "The event slug" msgstr "Der Event-Slug" -#: templates/pretalx_broadcast_tools/orga.html:40 +#: templates/pretalx_broadcast_tools/orga.html:39 msgid "URL to the talk feedback page." msgstr "Adresse der Vortrags-Feedback-Seite" -#: templates/pretalx_broadcast_tools/orga.html:43 +#: templates/pretalx_broadcast_tools/orga.html:42 msgid "The talk slug" msgstr "Der Vortrags-Slug" -#: templates/pretalx_broadcast_tools/orga.html:46 +#: templates/pretalx_broadcast_tools/orga.html:45 msgid "URL to the talk detail page." msgstr "Adresse der Vortrags-Seite" -#: templates/pretalx_broadcast_tools/orga.html:48 +#: templates/pretalx_broadcast_tools/orga.html:47 msgid "or" msgstr "oder" -#: templates/pretalx_broadcast_tools/orga.html:49 +#: templates/pretalx_broadcast_tools/orga.html:48 msgid "Track name in plain text or coloured using the track colour." msgstr "Track-Name in einfachem Text oder in der Track-Farbe eingefärbt." -#: templates/pretalx_broadcast_tools/orga.html:52 +#: templates/pretalx_broadcast_tools/orga.html:51 msgid "Settings" msgstr "Einstellungen" -#: templates/pretalx_broadcast_tools/orga.html:55 +#: templates/pretalx_broadcast_tools/orga.html:54 msgid "Lower thirds" msgstr "Bauchbinden" -#: templates/pretalx_broadcast_tools/orga.html:59 -msgid "" -"The info line will be shown on the bottom right side of your lower third. If " -"you set it to an empty string, it will automatically hide itself." -msgstr "" -"Die Info-Zeile wird unten rechts in den Bauchbinden angezeigt. Wenn das Feld " -"leer ist, wird die Zeile automatisch ausgeblendet." - -#: templates/pretalx_broadcast_tools/orga.html:63 +#: templates/pretalx_broadcast_tools/orga.html:62 msgid "Room info" msgstr "Raum-Information" -#: templates/pretalx_broadcast_tools/orga.html:70 +#: templates/pretalx_broadcast_tools/orga.html:69 msgid "PDF export" msgstr "PDF-Export" -#: templates/pretalx_broadcast_tools/orga.html:86 +#: templates/pretalx_broadcast_tools/orga.html:85 msgid "Save" msgstr "Speichern" diff --git a/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po b/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po index 04efdec..0b98d27 100644 --- a/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po +++ b/pretalx_broadcast_tools/locale/fr_FR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-31 21:10+0100\n" +"POT-Creation-Date: 2024-11-03 13:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -71,69 +71,80 @@ msgstr "" #: forms.py:18 msgid "" "Will only be shown if there's a talk running. You may use the place holders " -"mentioned below." +"mentioned below. The info line will be shown on the bottom right side of the " +"lower third. Setting this to an empty string will hide the line entirely." msgstr "" -#: forms.py:21 +#: forms.py:23 msgid "Info line" msgstr "" -#: forms.py:37 +#: forms.py:29 +msgid "" +"If checked, pretalx will periodically generate voctomix-compatible lower " +"thirds images and make them available as .tar.gz." +msgstr "" + +#: forms.py:32 +msgid "Generate voctomix lower thirds" +msgstr "" + +#: forms.py:47 msgid "" "If a talk is running, the room info page will always show the talk title and " "the list of speakers. The content below is configurable here." msgstr "" -#: forms.py:41 +#: forms.py:51 msgid "lower content" msgstr "" -#: forms.py:46 +#: forms.py:56 msgid "" "If no talk is running in the room, show the time and title of the next talk " "in the room." msgstr "" -#: forms.py:49 +#: forms.py:59 msgid "Show next talk" msgstr "" -#: forms.py:55 +#: forms.py:65 msgid "" "If checked, the value of the 'internal notes' field in a submission will get " "added to the pdf export." msgstr "" -#: forms.py:58 +#: forms.py:68 msgid "Show internal notes in pdf export" msgstr "" -#: forms.py:63 +#: forms.py:73 msgid "" "If checked, 'do not record' talks will not generate a page in the pdf export." msgstr "" -#: forms.py:66 +#: forms.py:76 msgid "Ignore 'do not record' talks when generating pdf" msgstr "" -#: forms.py:71 +#: forms.py:81 msgid "" "Comma-Separated list of question ids to include in pdf export. If empty, no " "questions will get added." msgstr "" -#: forms.py:74 +#: forms.py:84 msgid "Questions to include" msgstr "" -#: forms.py:79 +#: forms.py:89 msgid "" "Additional content to print onto the PDF export. Will get printed as-is. You " "may use the place holders mentioned below." msgstr "" -#: forms.py:83 +#: forms.py:93 msgid "Additional text" msgstr "" @@ -149,83 +160,81 @@ msgstr "" msgid "broadcasting tools" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:14 +#: templates/pretalx_broadcast_tools/orga.html:13 msgid "room" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:15 +#: templates/pretalx_broadcast_tools/orga.html:14 msgid "Feature" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:22 +#: templates/pretalx_broadcast_tools/orga.html:21 msgid "Lower Thirds" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:23 +#: templates/pretalx_broadcast_tools/orga.html:22 msgid "Room Info" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:30 +#: templates/pretalx_broadcast_tools/orga.html:27 +msgid "Download voctomix-compatible lower thirds images" +msgstr "" + +#: templates/pretalx_broadcast_tools/orga.html:29 msgid "Placeholders" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:31 +#: templates/pretalx_broadcast_tools/orga.html:30 msgid "" "pretalx will automatically replace some placeholders in your custom content:" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:34 +#: templates/pretalx_broadcast_tools/orga.html:33 msgid "" "talk code (MUX9U3 for example) - most useful in combination " "with pretalx-proposal-redirects or something like that" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:37 +#: templates/pretalx_broadcast_tools/orga.html:36 msgid "The event slug" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:40 +#: templates/pretalx_broadcast_tools/orga.html:39 msgid "URL to the talk feedback page." msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:43 +#: templates/pretalx_broadcast_tools/orga.html:42 msgid "The talk slug" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:46 +#: templates/pretalx_broadcast_tools/orga.html:45 msgid "URL to the talk detail page." msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:48 +#: templates/pretalx_broadcast_tools/orga.html:47 msgid "or" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:49 +#: templates/pretalx_broadcast_tools/orga.html:48 msgid "Track name in plain text or coloured using the track colour." msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:52 +#: templates/pretalx_broadcast_tools/orga.html:51 msgid "Settings" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:55 +#: templates/pretalx_broadcast_tools/orga.html:54 msgid "Lower thirds" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:59 -msgid "" -"The info line will be shown on the bottom right side of your lower third. If " -"you set it to an empty string, it will automatically hide itself." -msgstr "" - -#: templates/pretalx_broadcast_tools/orga.html:63 +#: templates/pretalx_broadcast_tools/orga.html:62 msgid "Room info" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:70 +#: templates/pretalx_broadcast_tools/orga.html:69 msgid "PDF export" msgstr "" -#: templates/pretalx_broadcast_tools/orga.html:86 +#: templates/pretalx_broadcast_tools/orga.html:85 msgid "Save" msgstr "" diff --git a/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py b/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py new file mode 100644 index 0000000..87f1bd5 --- /dev/null +++ b/pretalx_broadcast_tools/management/commands/export_voctomix_lower_thirds.py @@ -0,0 +1,336 @@ +import logging +import tarfile +from pathlib import Path + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django_scopes import scope, scopes_disabled +from PIL import Image, ImageDraw, ImageFont +from pretalx.agenda.management.commands.export_schedule_html import delete_directory +from pretalx.event.models import Event + +from pretalx_broadcast_tools.utils.placeholders import placeholders + +IMG_WIDTH = 1920 # px +IMG_HEIGHT = 1080 # px + +FONT_SIZE_TITLE = 30 # px +FONT_SIZE_SPEAKER = 25 # px +FONT_SIZE_INFOLINE = 18 # px +FONT_FILE = ( + Path(__file__).resolve().parent.parent.parent + / "assets" + / "titilium-web-regular.ttf" +) + +BOX_PADDING = 10 # px + +BOX_WIDTH = int(IMG_WIDTH * 0.8) +BOX_BOTTOM = int(IMG_HEIGHT - (IMG_HEIGHT * 0.1)) +BOX_LEFT = int((IMG_WIDTH - BOX_WIDTH) / 2) + + +def get_export_path(event): + return settings.HTMLEXPORT_ROOT / event.slug / "broadcast-tools" + + +def get_export_targz_path(event): + return get_export_path(event).with_suffix(".voctomix.tar.gz") + + +def make_targz(generated_files, targz_path): + tmp_name = targz_path.with_suffix(".tmp") + tmp_name.unlink(missing_ok=True) + with tarfile.open(tmp_name, "w:gz") as tar: + for file in generated_files: + tar.add(file, arcname=file.name) + tmp_name.rename(targz_path) + + +class VoctomixLowerThirdsExporter: + def __init__(self, event, tmp_dir): + self.log = logging.getLogger(event.slug) + self.event = event + self.tmp_dir = tmp_dir + self.exported = set() + + if event.primary_color: + self.primary_colour = self._hex2rgb(event.primary_color) + else: + # pretalx.settings.DEFAULT_EVENT_PRIMARY_COLOR + self.primary__color = (58, 165, 124) + + self.infoline = event.settings.broadcast_tools_lower_thirds_info_string or "" + + self.font_title = ImageFont.truetype( + FONT_FILE, + size=FONT_SIZE_TITLE, + encoding="unic", + ) + self.font_speaker = ImageFont.truetype( + FONT_FILE, + size=FONT_SIZE_SPEAKER, + encoding="unic", + ) + self.font_infoline = ImageFont.truetype( + FONT_FILE, + size=FONT_SIZE_INFOLINE, + encoding="unic", + ) + + def _hex2rgb(self, hex_value): + hex_value = hex_value.lstrip("#") + # black insists this should have spaces around the :, but flake8 + # complains about spaces around the :, soooooo .... + return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4)) # NOQA + + def _fit_text(self, input_text, font, max_width): + words = [i.strip() for i in input_text.split()] + lines = [] + line = [] + for word in words: + new_line = " ".join([*line, word]) + _, _, w, _ = font.getbbox(new_line) + if w > max_width: + # append old line to list of lines, then start new line with + # current word + lines.append(" ".join(line)) + line = [word] + elif word.endswith(":"): + lines.append(new_line) + line = [] + else: + line.append(word) + + if line: + lines.append(" ".join(line)) + return lines + + def _get_infoline(self, talk): + infoline = self.infoline.localize(self.event.locale).format( + **placeholders( + self.event, + talk, + ) + ) + _, _, w, _ = self.font_infoline.getbbox(infoline) + return w, infoline + + def export_speaker(self, talk, speaker): + img = Image.new( + mode="RGBA", + size=(IMG_WIDTH, IMG_HEIGHT), + color=(0, 0, 0, 0), + ) + + speaker_text = self._fit_text( + speaker.get_display_name(), + self.font_speaker, + BOX_WIDTH, + ) + infoline_width, infoline_text = self._get_infoline(talk) + + y_pos = BOX_BOTTOM - BOX_PADDING + if speaker_text: + y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER + if infoline_text: + y_pos -= FONT_SIZE_INFOLINE + + draw = ImageDraw.Draw(img) + draw.rectangle( + [ + (BOX_LEFT - BOX_PADDING, y_pos), + (BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING), + ], + fill=self.primary_colour, + ) + if talk.submission.track and talk.submission.track.color: + draw.rectangle( + [ + (BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING), + ( + BOX_LEFT + BOX_WIDTH + BOX_PADDING, + BOX_BOTTOM + (BOX_PADDING * 2), + ), + ], + fill=self._hex2rgb(talk.submission.track.color), + ) + + for line in speaker_text: + draw.text( + (BOX_LEFT, y_pos), + line, + font=self.font_speaker, + fill=(255, 255, 255), + ) + y_pos += FONT_SIZE_SPEAKER + + if infoline_text: + draw.text( + (BOX_LEFT + BOX_WIDTH - infoline_width, y_pos), + infoline_text, + font=self.font_infoline, + fill=(255, 255, 255), + ) + + filename = self.tmp_dir / f"event_{talk.submission_id}_person_{speaker.id}.png" + img.save(filename) + self.log.debug( + f"Generated single-speaker image for {speaker.get_display_name()!r} " + "of talk {talk.submission.title!r}, saved as {filename}" + ) + return filename + + def export_talk(self, talk): + img = Image.new( + mode="RGBA", + size=(IMG_WIDTH, IMG_HEIGHT), + color=(0, 0, 0, 0), + ) + + title_text = self._fit_text(talk.submission.title, self.font_title, BOX_WIDTH) + speaker_text = self._fit_text( + ", ".join( + [person.get_display_name() for person in talk.submission.speakers.all()] + ), + self.font_speaker, + BOX_WIDTH, + ) + infoline_width, infoline_text = self._get_infoline(talk) + + y_pos = BOX_BOTTOM - BOX_PADDING + if title_text: + y_pos -= len(title_text) * FONT_SIZE_TITLE + if speaker_text: + y_pos -= len(speaker_text) * FONT_SIZE_SPEAKER + if title_text and speaker_text: + y_pos -= BOX_PADDING + if infoline_text: + y_pos -= FONT_SIZE_INFOLINE + + draw = ImageDraw.Draw(img) + draw.rectangle( + [ + (BOX_LEFT - BOX_PADDING, y_pos), + (BOX_LEFT + BOX_WIDTH + BOX_PADDING, BOX_BOTTOM + BOX_PADDING), + ], + fill=self.primary_colour, + ) + if talk.submission.track and talk.submission.track.color: + draw.rectangle( + [ + (BOX_LEFT - BOX_PADDING, BOX_BOTTOM + BOX_PADDING), + ( + BOX_LEFT + BOX_WIDTH + BOX_PADDING, + BOX_BOTTOM + (BOX_PADDING * 2), + ), + ], + fill=self._hex2rgb(talk.submission.track.color), + ) + + for line in title_text: + draw.text( + (BOX_LEFT, y_pos), + line, + font=self.font_title, + fill=(255, 255, 255), + ) + y_pos += FONT_SIZE_TITLE + + if title_text and speaker_text: + y_pos += BOX_PADDING + + for line in speaker_text: + draw.text( + (BOX_LEFT, y_pos), + line, + font=self.font_speaker, + fill=(255, 255, 255), + ) + y_pos += FONT_SIZE_SPEAKER + + if infoline_text: + draw.text( + (BOX_LEFT + BOX_WIDTH - infoline_width, y_pos), + infoline_text, + font=self.font_infoline, + fill=(255, 255, 255), + ) + + filename = self.tmp_dir / f"event_{talk.submission_id}_persons.png" + img.save(filename) + self.log.debug( + f"Generated image for talk {talk.submission.title!r}, saved as {filename}" + ) + return filename + + def export(self): + generated_files = set() + if not self.event.current_schedule: + raise CommandError( + f"event {self.event.slug} ({self.event.name}) does not have a schedule to be exported!" + ) + + self.log.info( + f"Generating voctomix-compatible lower thirds for event {self.event.name}" + ) + + for talk in self.event.current_schedule.talks.filter( + is_visible=True + ).select_related("submission"): + if talk.id in self.exported: + # account for talks that are scheduled multiple times + self.log.warning( + f"Talk {talk.submission.title!r} was already generated, skipping. " + "(Possibly scheduled multiple times?)" + ) + continue + + self.log.info(f"Generating image(s) for talk {talk.submission.title!r}") + generated_files.add(self.export_talk(talk)) + for speaker in talk.submission.speakers.all(): + generated_files.add(self.export_speaker(talk, speaker)) + self.exported.add(talk.id) + + return generated_files + + +class Command(BaseCommand): + def add_arguments(self, parser): + super().add_arguments(parser) + parser.add_argument("event", type=str) + parser.add_argument("--no-delete-source-files", action="store_true") + + def handle(self, *args, **options): + event_slug = options.get("event") + + with scopes_disabled(): + try: + event = Event.objects.get(slug__iexact=event_slug) + except Event.DoesNotExist: + raise CommandError(f"could not find event with slug {event_slug!r}") + + with scope(event=event): + logging.info(f"Exporting {event.name}") + + export_dir = get_export_path(event) + targz_path = get_export_targz_path(event) + + delete_directory(export_dir) + # for the first export of the conference, the "broadcast-tools" + # directory does not exist + export_dir.mkdir(parents=True) + + try: + exporter = VoctomixLowerThirdsExporter(event, export_dir) + generated_files = exporter.export() + make_targz(generated_files, targz_path) + except Exception: + logging.exception(f"Export of {event.name} failed") + else: + logging.info( + f"Export of {event.name} succeeded, export available at {targz_path}" + ) + finally: + if not options.get("no_delete_source_files"): + delete_directory(export_dir) diff --git a/pretalx_broadcast_tools/tasks.py b/pretalx_broadcast_tools/tasks.py new file mode 100644 index 0000000..c09d14b --- /dev/null +++ b/pretalx_broadcast_tools/tasks.py @@ -0,0 +1,78 @@ +import logging +from datetime import timedelta + +from django.dispatch import receiver +from django.utils.timezone import now +from django_scopes import scope, scopes_disabled +from pretalx.celery_app import app +from pretalx.common.signals import periodic_task +from pretalx.event.models import Event + +LOG = logging.getLogger(__name__) + + +@app.task(name="pretalx_broadcast_tools.export_voctomix_lower_thirds") +def export_voctomix_lower_thirds(*, event_id): + from django.core.management import call_command + + with scopes_disabled(): + event = Event.objects.filter(pk=event_id).first() + if not event: + LOG.error(f"Could not find event {event_id=} for export") + return + + with scope(event=event): + if not event.current_schedule: + LOG.error(f"event {event.slug} does not have schedule, can't export") + return + + call_command( + "export_voctomix_lower_thirds", + event.slug, + ) + + +@app.task(name="pretalx_broadcast_tools.periodic_voctomix_export") +def task_periodic_voctomix_export(*, event_slug): + from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import ( + get_export_targz_path, + ) + + with scopes_disabled(): + event = Event.objects.filter(slug=event_slug).first() + + with scope(event=event): + if ( + not event.settings.broadcast_tools_lower_thirds_export_voctomix + or not event.current_schedule + ): + return + + targz_path = get_export_targz_path(event) + needs_rebuild = False + last_rebuild = event.cache.get("broadcast_tools_last_voctomix_export") + _now = now() + if not targz_path.exists(): + needs_rebuild = True + if not last_rebuild or _now - last_rebuild >= timedelta(hours=1): + needs_rebuild = True + if event.cache.get("broadcast_tools_force_new_voctomix_export"): + needs_rebuild = True + + if needs_rebuild: + event.cache.delete("broadcast_tools_force_new_voctomix_export") + event.cache.set("broadcast_tools_last_voctomix_export", _now, None) + export_voctomix_lower_thirds.apply_async( + kwargs={"event_id": event.id}, ignore_result=True + ) + + +@receiver(periodic_task) +def periodic_event_services(sender, **kwargs): + for event in Event.objects.all(): + with scope(event=event): + if (event.date_to + timedelta(days=2)) < now().date(): + continue + task_periodic_voctomix_export.apply_async( + kwargs={"event_slug": event.slug}, ignore_result=True + ) diff --git a/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html b/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html index 1804bbf..a184c51 100644 --- a/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html +++ b/pretalx_broadcast_tools/templates/pretalx_broadcast_tools/orga.html @@ -24,6 +24,7 @@ {% endfor %} +

{% translate "Download voctomix-compatible lower thirds images" %}

{% translate "Placeholders" %}

{% translate "pretalx will automatically replace some placeholders in your custom content:" %}

@@ -54,7 +55,7 @@ {{ form.broadcast_tools_lower_thirds_no_talk_info.as_field_group }} {{ form.broadcast_tools_lower_thirds_info_string.as_field_group }} -

{% translate "The info line will be shown on the bottom right side of your lower third. If you set it to an empty string, it will automatically hide itself." %}

+ {{ form.broadcast_tools_lower_thirds_export_voctomix.as_field_group }}
diff --git a/pretalx_broadcast_tools/urls.py b/pretalx_broadcast_tools/urls.py index 554348c..1d68954 100644 --- a/pretalx_broadcast_tools/urls.py +++ b/pretalx_broadcast_tools/urls.py @@ -5,6 +5,7 @@ from .views.orga import BroadcastToolsOrgaView from .views.qr import BroadcastToolsFeedbackQrCodeSvg, BroadcastToolsPublicQrCodeSvg from .views.schedule import BroadcastToolsScheduleView from .views.static_html import BroadcastToolsLowerThirdsView, BroadcastToolsRoomInfoView +from .views.voctomix_export import BroadcastToolsLowerThirdsVoctomixDownloadView urlpatterns = [ path( @@ -26,6 +27,11 @@ urlpatterns = [ BroadcastToolsLowerThirdsView.as_view(), name="lowerthirds", ), + path( + "lower-thirds.voctomix.tar.gz", + BroadcastToolsLowerThirdsVoctomixDownloadView.as_view(), + name="lowerthirds_voctomix_download", + ), path( "feedback-qr/.svg", BroadcastToolsFeedbackQrCodeSvg.as_view(), diff --git a/pretalx_broadcast_tools/views/voctomix_export.py b/pretalx_broadcast_tools/views/voctomix_export.py new file mode 100644 index 0000000..50f8f0f --- /dev/null +++ b/pretalx_broadcast_tools/views/voctomix_export.py @@ -0,0 +1,22 @@ +from django.http import FileResponse, Http404 +from django.views import View +from pretalx.common.text.path import safe_filename +from pretalx.common.views.mixins import EventPermissionRequired + +from pretalx_broadcast_tools.management.commands.export_voctomix_lower_thirds import ( + get_export_targz_path, +) + + +class BroadcastToolsLowerThirdsVoctomixDownloadView(EventPermissionRequired, View): + permission_required = "agenda.view_schedule" + + def get(self, request, *args, **kwargs): + targz_path = get_export_targz_path(self.request.event) + if not targz_path.exists(): + raise Http404() + response = FileResponse(open(targz_path, "rb"), as_attachment=True) + response["Content-Disposition"] = ( + f"attachment; filename={safe_filename(targz_path.name)}" + ) + return response