From 223678f761136b77df5dab797c994d7e8ff0450c Mon Sep 17 00:00:00 2001 From: lq1405 <2769838458@qq.com> Date: Sun, 18 Aug 2024 16:22:19 +0800 Subject: [PATCH] LaiTool V3.0.1-preview.7 --- .gitignore | 2 + package-lock.json | 4 +- package.json | 2 +- resources/scripts/db/book.realm.lock | Bin 1416 -> 1416 bytes resources/scripts/db/software.realm | Bin 131072 -> 196608 bytes resources/scripts/db/software.realm.lock | Bin 1416 -> 1416 bytes resources/scripts/db/tts.realm.lock | Bin 1416 -> 1416 bytes src/define/Tools/file.ts | 17 + src/define/db/model/SoftWare/software.ts | 4 +- .../service/Book/bookBackTaskListService.ts | 4 +- src/define/db/service/Book/bookService.ts | 26 +- .../db/service/Book/bookTaskDetailService.ts | 36 +- src/define/db/service/Book/bookTaskService.ts | 2 +- .../db/service/SoftWare/softwareBasic.ts | 10 +- .../db/service/SoftWare/softwareService.ts | 7 +- src/define/define_string.ts | 15 +- src/define/enum/bookEnum.ts | 15 +- src/define/enum/softwareEnum.ts | 10 +- src/define/enum/translate.ts | 1 - src/define/enum/waterMarkAndSubtitle.ts | 10 + src/main/IPCEvent/{bookIpc.js => bookIpc.ts} | 58 +- src/main/IPCEvent/gptIpc.js | 6 + src/main/IPCEvent/index.js | 2 +- src/main/IPCEvent/mjIpc.js | 6 - src/main/IPCEvent/sdIpc.js | 6 - src/main/IPCEvent/writingIpc.js | 21 +- src/main/Service/Book/BooKBasic.ts | 301 ++++------- src/main/Service/Book/ReverseBook.ts | 284 ++++------ src/main/Service/Book/bookFrame.ts | 59 ++ src/main/Service/Book/bookTask.ts | 221 +++++++- src/main/Service/Book/bookVideo.ts | 24 +- src/main/Service/GPT/gpt.ts | 178 +++++++ src/main/Service/MJ/mj.ts | 32 +- src/main/Service/SD/sd.ts | 33 +- .../Service/ServiceBasic/bookServiceBasic.ts | 256 +++++++++ .../ServiceBasic/softwareServiceBasic.ts | 76 +++ src/main/Service/{ => Subtitle}/subtitle.ts | 503 +++++++++++++----- src/main/Service/Subtitle/subtitleService.ts | 227 ++++++++ .../Service/Translate/TranslateService.ts | 19 +- src/main/Service/d3.ts | 15 + src/main/Service/ffmpegOptions.ts | 11 +- src/main/Service/taskManage.ts | 11 +- src/main/Service/watermark.ts | 17 +- src/main/setting/gptSetting.ts | 24 + src/model/Setting/softwareSetting.d.ts | 17 + src/model/book.d.ts | 16 + src/model/generalResponse.d.ts | 7 +- src/model/subtitle.d.ts | 13 + src/preload/book.js | 70 ++- src/preload/gpt.js | 6 +- src/preload/mj.js | 4 - src/preload/sd.js | 4 - src/preload/write.js | 12 + src/renderer/src/App.vue | 28 +- .../Book/Components/BookListAction.vue | 152 ++++-- .../Book/Components/BookTaskListAction.vue | 10 +- .../Book/Components/DatatableAfterGpt.vue | 1 + .../Components/DatatableGptPromptButton.vue | 88 +-- .../Book/Components/DatatableHeaderPrompt.vue | 17 +- .../Book/Components/DatatablePrompt.vue | 46 +- .../Book/Components/GetVideoFrame.vue | 70 +++ .../Components/ManageBookDetailButton.vue | 162 ++++-- .../Book/Components/ManageBookOldImage.vue | 117 +++- .../ManageBookTaskGenerateInformation.vue | 3 +- .../Book/MJReverse/MJReversePrompt.vue | 17 +- .../Book/MJReverse/ManageBookReverseTable.vue | 2 +- .../src/components/Book/ManageBook.vue | 6 +- .../src/components/Book/ManageBookDetail.vue | 28 +- .../src/components/Book/ManageBookTask.vue | 6 +- .../Components/DataTableGptPromptRow.vue | 15 +- .../Components/DataTableShowGenerateImage.vue | 8 +- .../src/components/Original/DataTable.vue | 43 +- .../src/components/Original/MainPage.vue | 1 - .../src/components/Setting/MJSetting.vue | 16 +- .../components/Setting/SubtitleSetting.vue | 188 ++++++- .../components/Setting/TranslateSetting.vue | 11 +- .../Watermark/GetWaterMaskRectangle.vue | 2 +- src/stores/reverseManage.ts | 3 +- 78 files changed, 2827 insertions(+), 917 deletions(-) rename src/main/IPCEvent/{bookIpc.js => bookIpc.ts} (77%) create mode 100644 src/main/Service/Book/bookFrame.ts create mode 100644 src/main/Service/GPT/gpt.ts create mode 100644 src/main/Service/ServiceBasic/bookServiceBasic.ts create mode 100644 src/main/Service/ServiceBasic/softwareServiceBasic.ts rename src/main/Service/{ => Subtitle}/subtitle.ts (55%) create mode 100644 src/main/Service/Subtitle/subtitleService.ts create mode 100644 src/main/Service/d3.ts create mode 100644 src/model/Setting/softwareSetting.d.ts create mode 100644 src/model/subtitle.d.ts create mode 100644 src/renderer/src/components/Book/Components/GetVideoFrame.vue diff --git a/.gitignore b/.gitignore index 6118292..98a05a8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ resources/config* *Lai_1.exe* .DS_Store *.log* +resources/scripts/db/book.realm.lock +resources/scripts/db/software.realm.lock diff --git a/package-lock.json b/package-lock.json index af50196..ae77d11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "laitool", - "version": "3.0.1-preview.6", + "version": "3.0.1-preview.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laitool", - "version": "3.0.1-preview.6", + "version": "3.0.1-preview.7", "hasInstallScript": true, "dependencies": { "@alicloud/alimt20181012": "^1.2.0", diff --git a/package.json b/package.json index cf912a1..e72d627 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "laitool", - "version": "3.0.1-preview.6", + "version": "3.0.1-preview.7", "description": "An AI tool for image processing, video processing, and other functions.", "main": "./out/main/index.js", "author": "laitool.cn", diff --git a/resources/scripts/db/book.realm.lock b/resources/scripts/db/book.realm.lock index d79fac9949e0602d0e6bbd0a11b0c352406f93c0..014b7e86274e6f9a1170e7df90fe5d7f14df451b 100644 GIT binary patch literal 1416 zcmZQ%d)| E0I~xFZU6uP literal 1416 zcmZQ%fN5&liDu;}cQgb> MLtr!n21N(}0OQ339smFU diff --git a/resources/scripts/db/software.realm b/resources/scripts/db/software.realm index 070048989019271a2bdac83e90999ad32d137abf..4702f0f817f7a27d6e53479ba3a5a845279baf1a 100644 GIT binary patch literal 196608 zcmeF43t(JTo&WDlo_(h+ZPOQ|DQyFEntSJYr^qs;Pr%Y(DHJF+_s-nggyuz(LV!gE z0Vx7nR?(uc1X(1?B6V3L2&JQl#&y-IixyF-U`r5Ps{WNKYX6_#oqLmMI;8_XTGmYa z&F{R=x##`(o!?pUm`{;k=4)AcUSP=?m6eKb5x+5tQr2vB8Y!dGa2%meHW?iq>lf9X zx$;smD$eT9Xm7ou!)jl7mXg)KyLH0`s~!8ItbXOCtF6w?#+D7P{d=tD)=mrOzO4P~ zR=e{Gqut8Ji$5p+(a_7S_KwEZ7DdwInR|4pqLgk?wkkasD9SeHuee*LDE=)YeM)X})w(TjQGcCfB5_`O?*GmNnq0+-Pe&+q!bV z{fK0{WprBSH8zuYekGgQD1{uomoKi5a)}ppnNYrqPw^*wzz>Q*u~MEW0i~!Fe&SB@ zFX5~6^*JB2)A=jrl3)FcuDIqCn24VIEb7~+D7~HJ!g@WLlk#GU-Eb^g8uD)I2W zQ&m+B2CMoX{=lBi5AOZgO?&U%ym#}KJs-SBp|-EMywpjz>rM^jb2o?Y0TQ0n@218L z4V@I0=p~;MX}_Z0=vTJ=!KqG;JMwE(b#?UwsbS)dd>A#fe$o0aZ%kTex;3ga*qe`^$x`Rf$V~*eXoJ;4+T>R{pkKVHqqDVHc{}y9 zm~<0XMTHY&_K$oI<$}3CEhf}ihf;4HO5MFd%!^%G;+j*B9s4rH=g@Mc+@T{}I?}b5 z@Tpg&-9_7q8c2Ymq@BNFPB}e$b#+HuQ)B0P@~gdiVf9Pt_rLb!tuLnUdNFMko$Z^f`PJ=?ZgDmm^%_31SiY~6e0A%()b}Goa!340dEj1t z*E)ayid4e)+DG&V?Za~yDbQWs@D|Z;g|gq3?Wf=GG%QI+zJA(lZQVH7Oq3C#KhNm! z%t|`1Y_lYv+~25Eho@CWNH0C0BOjBkEu90ch;}FOV(;|R(jQUpGvc0l?@;Q! zL#g);rQSP~zT2VHdxuImQm^|<_m{R23Zs+(kHiUm=}T>7(SE7iEEIXiw6pYJ#~PzjIv z_;=~2moskf9~fD(3D`|#lAnw{^4n6IrJl2iS*?@XEF_pD<|Be}2f7-3fHd!5=D_c`m6K;w+tR^ej`JPs@)#-VQ8=H*{ z*6O6ul&w`Ij^1<4ydZ18$+jE2B#Por>Z;QbWFsu?Xfu+7(*~vV?eyCOHCn!p3Put8 zoo}bUI+XhAQ0l8gsjm*DpLQtq)uB>3CC|!TNrUd4dc9)A+7VSFajeuh zf5lw93LTM?{i1i%m6q^j{18g44lQyi>2mZkeuzEkxsesKi5E zIAM-*meQ(xRC!$aiBjaN@vZc=`#$b_()Uwess9vzoqv=66aF3kpZP}=Eht)D)KzqM z(U*%}E*f2|6`x;xH4Q?-@%lBbYR`wBc;(K2+q>;Q_uRH^&*yGsDnXK*pSYvFjCT8A z>4yt~_>=s=z5K3q{{9W9#6vkvC{py=@|wyTey8ZCtT<&`O{QjNjat()a@)w=HJgv? zJ+9B`0>nMvLzb`A+WHyvb&W^%H%-CEsv6-m$go3de3V^ivIp({$I)nb@# zTh152RA-*H_~6M~j>GZH+Zvh49nQr#dXr6WlEBIBRdb>sn|DmE!x!Cx_Xn@ z-005!1|3&)vLb2S)VZ>;WmBg$IPQ5a#t+OBJ(Gj!WU)W*;JIe5i^)U!r(8QxPa3ig zI>dg+x@f3KXGV@;&Ukd ziQ|s`#G&+ajye5_Lr1#qnCLqC(XROzmyTs^Ch?4OX@zSp>7kvKZX4Y>v3s2S z6?58?U)uQG-+j?`^}h6?suIustBen#mND-Z#GL9ejYK1O1qqlQy5!kp39PZD2I&B^=bH=LaHw+#Ij)?@l-{Yv?tvje%CKjYm#QToNV4KMvt zPY6eT*E)ZXB9-qW`^k!fe`1bdc~xLr_3r9@beNU3HMOcU50!k{_cZI)ui%pkThUVC zl%LGoCb_}5{{6k<`UdKqGtbMe!Ar!Ra{3hOC}-y4OifrwH5rZTuV`rOXtUZ`)1bS+ z)hxWadQ*E7E*d&J+d3B3*0Q2sfPH6cYtw>cvKH6by%3zdjm-tTcU;+$TwZq`dsOVG zR4=rRrVi^GSrp07O|PuuwdfJ*y<7jZSYhteBh^!{J|C&QdG105#v3lsUE~PvEXuv! zov^$4G-vN)Pwe?%cUAMH>zV4a7iU%N`S5LfKh*QHCosR)-TQF!Rs8p!8c>qVkX-)< z(Ned+`rh=<4fIz^&o?T)a-p938;5F^r+P3HJG~05E7p_~wT(e_cFcyvL>y}s@8#`Ou*1dbtnddRm ztZQp;y_ALax>#ISlhKG42y4k;AZ#Zifw&%x2f`*KtS3#%Yn>c`th3vVW^0`j|AOW= zwvK5}HwVg7@=FUH-E?W8A?c!gYtpU#6B7^oXlDBRKOC2sGJ0C( zwNd4XHC1EN|J*eueM!s2L{G;l>DwQRD*9bBl+T!R)01bOkWu%Xm|1c|QF__P(dm!Z zj!kU-?ri1!aU&9^D$~=qU96-xUlmPXXC9}#c7W%*3nbq^CYNGV|-5YNE04r1T3@DiWW2)t^{$+VsQ?*PfhPuE$1w zo9bJtToE7r?3)p0cmm{gnM*zkABAc>O8+Meyn39d=vg z4*UBj@34Ob{&>m``%mD-`8(|60z2#`Ao)u9`$BXWUVmUQ?zhdzb_qH8uIWd)-s*0> z?fuq4?MXe#?Me08)6jHf9y2^o z9kM^_=8INnc@*ld!1>f)t=#AjPd?V$iPxV?dtpDx=WNQm^Evu&pMJ*DpT3taTUEysh=ia?aDXcm?hjW}SP8&zjr$^#rptJBllc zlX)0zWy4c`J6)Pn9UODc(-hgE^ZYr6{E>3O?ce1b!zEbDvT-$*1)3QSy5;_tFl-sYogkvx5OG6*B^1OSJ=LC>&!M z7t}*mERu?b4P|bYpFq}Wy|w%2^wkcwy9wE7oOlRtLjO&l-SgPzSrhNQ>ozus&RmjX zpRZl&b63o5m|H-2EcITw?juJGZ}Z%(b9-~bo01bh>6$j+bIa!qrf=%O;Y}R~@0>HwI`g0qvkngzZp}P>UVTou-ubh*C!G@q!j(mA z4kOOt!b{KFGH;t39x?mG-I4k=%G1AVUjIDr`)J}mE$91&>b-D1O89TO5<4^0>Shfn$t`HB6|eZZl5^4Hn-JJNm+>GG-JhadkG z6+^<6aV9sOq2mtW&e6=6ror_Vux!m_KOvk7n1&V!Xtu3uk%$^MHMS12;pMAu`bjHJ z>N+WFFY&L<*^iZcOB|>u@%-E9w`hdohL@ySG4)$tta=o>7}ejU-a3}Iw|(TcPgEmU*zYTj{8fo-+yxXDK8R{_#ayD zBz`au{!n`_RX!F*EVlgZD!uidHd&8P1rU^?4{m@tY|nEGR#2Sh=!Se#=@vVdcck( zV-drSM1p2if)@AEu5!ycPdjt^li_{;gZWp;XJ=hq;QF?ZegDGxo7vfNlm2n{)69o< zR(b7hju{RoqjoSHh{P?*o!#ny8BE3kp_HvA!&WF_MF;k&rTr_jD+nzcp6It!_Ru?L z^+Vgc`@O-ue7`yC1nC!4Gc{1ZEW&N8J!t$ZOEbsH{S{IVr9blghe zU(S5K+d0kkTys|L&OhC0x{{ugwetcjah>+N^M0L=m0vd1S+)EX{x}&UJ%3^*KZ)0? z4+Wk-sfC_r6}XNn_}r_&^?#xJ4h5fk6&SCo=Uay^8a$sF%;z^h$MM|d_@w7<~}r)2BX(7!qLi~K2UKdsRHz5?rEq4B~`kf&T|Z#n0TPQO_|ITzL+ zy6ulP{MX`N{P&^!9#(3D73l2jfd4|2=bg8@8cvVPjiyZ&j2u@r1QwqYSlei{c7b!O zP3=Y#F1cTjfnFf7My9Fjh#(ahsXZj z*4UYBs9vZFBYY>(X{au5ZQ9h_f+3tA<$;0z$C+w$MJI;1|R1@!a7^Wr>w-QKwSSDY6+<3)kxQ7}JD;q5ce zcqWyFUrzr)`z*{|-uZJjxP!vW%O{@4UEcdcS1ZLkL2ml;?(g5G6xV;~V0U@H<6fnB zAIJ^wNZ+xHd4JF4jQj5WM(Raj<2hHDvhny{yIOcFVVIQBc0*g|`h$5ZJ-*5otF!iU ztrq6?&e~*ab6bn77^&7X5H$&;>tX|eg#vS9(!iKNxBfV>la)!NHLZz(UAI1v?=;2~}gN-|8WZvArR zG0~$ukBJ^SkC}U{j$gU$ve165&~-$C>x2C3&qMp6g4dM=t``ele-?QD_1ByS7S`Xn z}K;V_qTI?R_E{)pJJaf0+yrp!~ zk2~!%c$C{`&}pC9FLA$P#n5$IQ!951*Ebo>20eUZYn#z1SCw;l^kLE0f-$apa^Z8K zuIg%SHEXJDc*WBW2`|gZPhg$gO73VfWT07ZaG}Ust$F6Me!KYU>K3>v8C2YnYKT8` zQ%7Tq)q%^yT^Gnds(EoZaksUtZ%h#a!($-a8fb28X&e$whsCucK3gsv)CE^BiN{uM zS`#@dydu18Wl+~+QFB=+n2N)%5RJzpVQaCe>9C7hdPLJ0=LSB)G#XQzhJ>Bx6Ktd9 z%Eo{UfsTiPQN>Ah?d6SawO;yVN$jZ)`j(&?46C7F1P)aA*JF{$5QhU_)R0`fTwk%e z&Rlu^nbt;AUwU5Cs@x=VIXTJwh3VI-AdJ>FP8i?>=W~+tO8MYv<>t-x-)&Hc(53gqH6U+!|?RhjYi5`x&PX@tyM> z?>=wU^0OMwS+b_^LutU7;X@=ATBx*`-_1f#%t zmo5)!i_X-8YpSogiHX554af=G2M?1a$SK`mH=HEMgOv%sdmP14Cpw@5z*~J;-emY^{eQBG9WUuL{k5`oREbgc>-`}+WzEZeo z(wx+8j^X}P?y&xoqGVQZ!M;vW`s%ryW59dZia+iFZ|zi+`m5+9{u#gTNfNbL?F{rD8GQp7p*J*sa9;9PC&5lmz^K4Q)QsgdLZqxAyv!EnoI2 zaz-t8aJ#;Nzi<1L&Cekd;fFp&g_m#NtHkp@zf#ZnapDHQ($xq3onO&k_ba{o{Yn*b zqV~Ok{h$0wA5gjPTz(+CKP34|mMU_=lB`jxT(unG_JE8_w?Vssn2S46i@oSYT~67Q zb6zarh?JRLsMyuB78A9&?E}3Wh)YSCk99!|< zfSKn<(zE4Xvi9P)TR^?)gVuqaAmOTo=0f_J67qE(VP=1WD$nHaLko*n(z^_PnMx8> zPrdRfrAqqUqLT1EN;z#!K1s4se&P?SOSbyS8zAj+7j;1RO2zHacG)+F zd_IYCCLa&iHYMK5^A2vCBHt~yZPwj*Xxr4kOnm^APgB3XLRy|d|8>fW3L$N@{O6d{ zPV4`dbd}SlXsf#_XkWC|u8G;U+JF4P?N#<%y*9f|A8fZGA3@q}W=OlOYt6RZoo?Ih z`%Jd|_RxM4-}5*==H)?S! zRNRPLQA-<^c2G_^=C8uIHZkl#;hCc9Gh+Dj(fru zD(-c@aVs=m``&_iAE*O6LBdrF%>`Pg{uAsaS+>-gBKarg^2=3wCRv*AulO7En^g~n zkZYTDHR#q;<)fb2k>by7|1z#f`$&9dYDGEMZT~W+$_J9g&M({keM?e7wi;qV zH5?Cdku;))4R}~>_8rWa8cxRGnbKA6*Q$|lockOgXsWiFQgxl>t&vg<5DeLDQrK!F zs%n<0YKEn1dP>u5GpL1wAuS#XX<90bS(uw2F)bB}Ytd+23x^F&jhI?WO=>YcsS%cD z$1F`XY)y@;LBr635ldqqCKOCsVYpl)K}(GW!&*FOX+|&^0k#p0n#o{9O9i753t^OY zFmB;eS3{|k8dBpVzzT=r!AK|=iiC8a2BWaPM?*$D8j7Z(p_CB|>8V&KY{C*BkB3Yv z9GyU>U@fw3USjJ=VmM#4C#2&qcJs3 z_HY@~Fry|nq4iiY8Z>~OiVBZf$Vx?{5jz^Q)EFGhv7o8N!W2g;9E`;y!B{dLjHOIy zBpkCMk(fbJ;!z`Jn+9H_l8|w!6rX9ute71O>vl{x?3m5hvVEm%ma3b!%GYYTMWU=A zn{1J=u5tM~6efk_L{!r$VMDhgh8~AsIg&JWZYHoVXOV5T_>XI5Fm77mxJ7ow%xF9u zj>VI9jMAmR!tpqf##1RuR5#;sH5m_^skoT}K`Wj#EP}P*1hwNv+>V#it?KrWw_eRI+3$P7_f>N!l8fBt&APNjnltS`>IP z9#7JmsC;IUaFcqNsZdG_af`$Z zr&4gc+wn*$9-&Imn&VXDkfo<+&twMMr+g)4n9|AlP$ZeMV#yR0IhBf7R3{rdJ4N1G zmabYF|Hupr{&g#C2l@Q471hHQxoeqWsya2&G-Fixm=&jLX;Ovah7~ppD`ccBGiA{g z*qCd!melN&733>IHgaew;INI#*5grPkJ?;yp=r>o*>RheX4_HICU(gl+yKpTGBcRM z>Ti&Sw-W!LAQq>aE*aK)f@%I?^TQDVnH{x}*F7Mpr?C8xaelO-oOG=00~NY&=D9mo z((R=qJ8x1>K331cJ^2>t%Ci?{zO!|D1=U0aEP<3BkZ|YNyD^hFy2wGTf^KGV$aaI$ zhFbJnfXwmNVkYzW9%wleX3^^~p6{z>t-zkl3V0S0v$123rkp*TeXRFZoR_`#i1RR} z_$c$sUk?PG=U478!%hC*c*jQm9?p%F%QEhj$-1JlT;{puO5Zxx5|{9u?^Tr9tT{-J z(zoc)c`(bGW9U40P!0&`a_?8<;j5a%N&~pxT`m{bs(_T^P6|D9Cu?5Tt2_DTeOz!7 zcW!uxkz+Qv-|pqdv2hNNtr3!bs^OBXZt!pJcje`mc!v5oz)zLE7kDK8Y^rzst@g9S z?*HM(X;*-rQ#WPa_7{!Y!}aOl`Xud~c}(0JhmSV(<|l>Hqn+MkEq*Cawx;6mlzEi0 z@tk1h@w-bi&E}}g&u6i@JZG-*kvrxqO+WHyZo1`EWzlmpl%K?>B|f`#MEaFiXDaV~ zO3(c8Sx&Z)%j=Fm%s&?XW%9+li=Qd}dGWZCaLI)w?<={lq_5_ z^2$5R<1X*KAdkDe_qOu5%PW82Pd{d@9CcvSiKEXP z-7@+kqkBjHc(i}ayfN<{bJ>`Wjd^0s?lC1}PaeB!Z0FcJ$38XorLpDX0^{B@?uv1D zjr-ELz2in9V|-1;RTcMCe5K-*im~HEf#n?gPg?c-+q8etX=sNvBUrO}cr~=O%q`(*8*^ zC!aBS!{l2he}3}!C%-YdYRb|nmrnWcl>eOa+>}30nO(V}vbpkKDj%!-QKfI{+^J_z zZJT<>)GtnbVQTTTlct?Btz+8OY1^m0IIV2@{ORXTzkK>9r)Q?`nLctx?TqtgTsh-k zXFNTlf5w=Z!I>A#{O6hX&iv}kS7%lnA36S_V% zf1mKH6DC)kT2)_lW7TJ?zFqbIs;18Rr&-Ban`b>V>$|gFpEaX;QMFxtOZB$uXRH5E zeZuS|vm0lBX!awscg_CO?CKMjpV)Nb?I(WW#OF^`JoyIBdi5yFPgFhmi9_U(s>h0) zRF8kmUC(Mqc|EIXhu)`{|4y6#xZ|qSzu-}plNrn-C)2r)n3unav(c{RgZDx5?iV0X zs-OKV_dDO#y?~>fF9Y&JbK0fkOa5_){V?zSEZRw7-|Nn2ZMNBjziB7?Z8`I;{PO@^ z3)ulp7k9(s*jpXPo}cv6q*o{XX3`sziYAYmJaO{$$+IV)GC4H))X9q{H%xAu+%@^` z$@fqGugTw?{QBerlTVy7f6BR2E}ZhdDIb{f;FL$E?3%KBN=fCI%3$Tf%Jr2SDsQd) zXyxOTU#i?&`M;GDr%s=mn7U$W^VCgKKQZ-FQ@=L#+f)DF)IUz0<&1#mPrG{B_0xK$ zZJYM&v>#9NPaiqGc6x03y6LIuH&4HP`WL1@HT|XOuTHO+Q90wZ8B1qeI-`BY$7kF# z<0~`%d&aM3{C>vqGw04cd*F*;Ip*K`QEg`T`KriD z&)oH;d70n5*Ow{(S4Vz1`LgV2uiM|QobcHTcG!0<-(mm!oE^5ZYKL76O2P3U4wi$* zz>mSp-~bp|x5FL-#)5I+V{5Xwa_tWL%a`o1fB)+pcE=|(_KQI34ddC{y&od;8|^5| zZPSLz+xF(MM_Im!=E*nl=GWB2`R}xP=+=+I<~yv_-;(>md8V^YJ2vi#9(2A}*mrXv z{E+Wa-*(?KzMuPk?VI91!N0`c=>L%a5&tg#pZwKD%Zr+dZZG;m(ep)0@toqbid&06 zTKstNPl}72cca+v)c*GS71@mxw|t!Q(!Bd=2hVez_I;GgMa$mbJ*ZrqbRFgO!y)f^ zc>T_?(jVsZJL)@qUf|YSr=K{=|RFwh`56; zM!y{TD`0?HunD||J=Y8rSpJpkal=-D5=Va~_!8<>m~nMSc>p{NU@1|43qFkcAy9(7 z9Q53S`b*H0zzcwD3W^4OMu7SP@Ojj?fX{%lf$*waiT~ButpR6BSmG8M05NbHdLQ^Y zcmO|-K^wsjP+twb8$5w}J9H)pp#A~$8_**BuK}D`DeFNk>QlfQ*eyaY{4}lLG;l8Z z2Qfbt^(g3d;7-g=0S0CeFy ze~S4}p%+7iH|ZYKFM;)_EzEukP6b@;V&9&yP#*x_L%j*Jnc&-~i$DbPPl8#PeGUCf zs9%FV0-8Y#{TS?Ki5_}7^p{{G>LTnmfEfCF!JokQ(BB7cLVZ8jfqFK04)yP$w}6XL zmw`(NV24$!(1FwP;B>b=y z3S@Aba?&Gi zR>13toAP?Xg6`zLavM}X8U5ApyC(39+3tzllVp&nWALfuPG>pyCpVvpd&219K3E0! zjV1jh43U4^bz1RRgYg8E_b3-J=HI0JyYiomM?Y0cePewg->JTOUxV*9-yOcYefRs` z-nvt9)}7;zt#>4)zxVPPmfgSZ_e;J~MapGtm5ZcuufCu1?P7wIeiklh%3bcTi#w_w zMP0<4ZTI?q7`?sspz&WMcg^Kqyxj4XpSV9(^O1Bp>-@a@CUXY)qP_GV<^3{s=zf`( zuGmqQ9~K*O9_^*;SjjK%{F454-m%=jb}EL`UbwP6x-pe)KZp4{v$Mf!whm*7@ido- zczuee)tihf*CRmliX7*oJbN);Z??8>T;I{z-q^-VXU?zGvzI4r*LNCbleNC1@v4Cs z%QrN&nnu%VtFsfampK8u$_a@0KCC5fCSB3q*l8W!#YCg=@RsW2Lw@MXXwcLA<`w&}!jIs4V?nbngVnIJC>+U)rat}5$aT3`F zB9}z&PKXQy=|`pSlm*LfAQ#pvL1HTR=cjW&hWAO*v#6}eU}xWmxWnheO3-b-$v5b#JwFLeZImxB>}bo^-A0!H>I38L&ZN6FW3nx zt|6^y@(gqXk)^W#R@{CFw;v`fu=Os&dmOi4Bz>U#%h-cmU^ft14!QDHq%9}DMcTlA zQ2uSg0qK`<*H2nr$;x`@hwen)L&dAa3#xvB8|E|$Y7GXhx#dr-q-S7f^MgPx_xcvy+q zImmJWBHLv*Q0s8RoUN)gE8|6EMO1VkPXhFIV$a-f8^{3Ri;y{+C;KJ+70f|5^FEQ& zA+jWNAa`^cp2s~hC3XUl2eb7Tq!a8!F3f;zn6%&7T&rUaxD#|UcT;clD&_yj z;S;0*^nDUD(DkoHO7EWt`_IG;)*dKQGC)y^m5Q$yEB#>ew~CcMu6;N??snP{{f%0srvhRgbMc<8k z(BLaWmPMJetF}z3Jf}?QtuIp&20JPpWy=0Z<;V{zcg6+jkLXqgWO(SVn(oB8gC|r_ z4rtPjtcxnBz_SyY0QH~>c;-|mC4;_=>QV2SSGlwcs=I0lTl~}jNsB-QaxX9e6?4zM z=*3>rBKAGdgsT=mqFxImJaHqm9HdD@4OINrjmx^*g_(q#03w&9&QZ%ja{iCyXJ%mTs;M}bgb9ocB1}bWTbdZK7K-=Uj_3Xt= z!WMnkguys6=o3I_n(#y|aZ7lj7Hs32#c#JiOT}!3=t0-Jvth}%2@=4IQ+M^f6SHP% z^b&SC5Os}PcAHW6fP~Q72mMPLGN6ul)6ktD0s33BlqT=!yWKJpyIgqb5VLKBvDLM& zg6fQ|z0kC%K?2l+G)RD5V=#Bi*NaEY>w(a;OWT%b^JRsr-{Pie8+tEX`94WY1|%3W z)6gnW=b+;2S$%hLmR6BBU8JFlZ)?Dve0v4hT|Vf~3sYz!2lb4%yGCFj>-3pf|CMMP zmwL6R%^%CMv1 zEEui@!wo_D0-2B6!iw`hS-!PJ%s1KIRM-+cgfHy`%vq8h#E13~^cw(Mu@3KK}k7fXCzY4b-!+djt9)7zM5ar-&cutKd#>EnutG;i*~*y%3xVSpE<2OU=PUEtrH^ z2Pgs0gO%Vhz%uwCo~YlTe+Y~RtH4jeJ>Vs9D&fq5eh+*b$h<#-+1H@2fk!|y5T2%4 z(9^*$K@s{G^j`2Ma1*#6%*OmV=*6H6Yy`rebQ$!MK=_f?L+=Aga5z4sbFrHOgx@Fx zPDA~Ba0=J~9tII`0k|6ok5Mah9`N{z4xqjnJPR6u$20UD)K3GprJbMDLzd##RjIkd>rgSB;=M6oJ*wY8zxx+pddUE!#7PJ95d)Nl#?4hoX zw7iGCj5VzLs9<#$;ddQ&6ii<|X)y>F$a%y*AmzcmHv9Opci$(KRYm}(Uf&KkA*p~;rU?^xQR zF9EG^)RoFEUl;p#ZGJJAe_f8Btb6&ZvKw3Dm{#CU_f;zO{8gRMs`x7*R`PGHzlOcM ztwnAA9)C|!!q??r>zA;)kd|KKxZ3Sc`}d9LVmE5PznfFJEwj3dwoX>&w@u#SCrqVg zY`UznNcU}>*k6<>YADW}qD<}{tCTeOH;?G?>+IsH{FATfA0z3nbJXP%`zQ6*wH56e zvBH=3C44(iY@6Rbf6L;Xl&_Z$o;0|L{z=G$%l@5&T6hmZQYlDW#Vmn&VL zp)EZ~Kk)DLo$NDGUliV?9M~`M{9%6Y-LwVLf-jI;rV$VTVi~HiV7!5J@=6ln( z60Drt%b9yU<83#3Ig_Vja?bQ=X3m-ZPKn#4;~n0y9(BVG^mOa3}^#eK?d}L3g!hms0aPX{H!<~J*Wqp!8Wi9 zC<(q5)PcUUNDrty8)MJ_wt!x+81D5qRiuvVxJ1!>R&c7pw&Do#2;8`ugmpdVB$ zBpsk0YzEuFE+7->8c-*7r-5+5eo)m&IG_z|1sTu}DlR1)P!BePZD1FWNo@_N16`mS z^i=XK(@7_Dm8$imxt{NVccJ@Q!nl>Ng+Bq_gYs3(SuV@YgJk}sVBXIhM1E3FOR1+K z=iig-@5u5WBFA53_>27hT)F)svp-i>zbB*Llh2D#;biLPlcg`R^F4X_A}e2HPu{%9nio0qB4b|U%jc6VKUA)Ku1xtP^5jJvyeC6m%$aO!GOm|O~ zyU1>TGnwtV^4bUFw7W9ehstLcIq04Y_FVbv*SqzU{rS62%><8EJm@P$rA)xCy;#!_1(lNcB zKa=#ynqJ2J%lMv80u43Y-rsFqznu7PCcb9@U3K2Iz4WK!k12W4@SJ@!+s}_dzT`XL z9*xVQN(Qsk8BXspcMs~AzUTB#uP@SnjHaEw)q6&NEBA%o&+^>;tcqPMaRso*YJIqUu|CUm>mFYTScek#1jZOh;>hL*!WT=^~5LU+L%%vhFz zAGq9L{d6}x!b|{nL(AbWt`mOgf5!Z2);jD3*DYcFgWEcIez(Fq+z+k#3T_m5ky#)0 z{{q1Cy#GtAkDzjQEO5=KdD=w`Fg+0{-Z_Hr86ix0{KX{=clr z9w!_oCi~!d&cIi!!oR!|{_ezL_?kz;PY(ZeAAHb#i;A4}mODSt)T4X9M^g{I-&2^} z7xGE!GdUtt%!7Vn&bCGQO_<7Fi@e<^yz=tKqP&eL=MD0fqPQa;3X89cb@=A==+}K? zZgA<1>262=fvi3|zac$xBg6T*;d|+1r(n=rUco!kPy8S5`$)GVM>6lb-_)I|{n`BS z=7GoBJ+2bur5!8##BMniRu3q&x8$CBnu%A>Nl$%lWaig9)kI_8N$D4+R3tw4s(ux9*AqnRKPIwATL$2HE3E2l11Vw4}k_k(P%2b8?6?OqIw_{ z=8eTrh`qz8uJBrAOjpfNfQJ_mJ)%dgfElqPfmA#h3dSNPySiaT9$$;I2OEK8gg8`#M?bZc!lRfG_E6!!PKNCO@0(fyHYoK#kS9;oSW4r)&alGclF4W^ z84V=$5R92Ty&7PvQ4d6e@gNV58qrudrRYY)He;q8;Pp?FghwI)lP4SlG0lqGyc25i z6si(V>S`((RIv~9b-W%*bnt}p7$`5OM%7SM*A$Bn;-SiTAZo$<$Ag*4056-G0o6#^ zJi^L8upLy?STY<6lRK%9a7soaF}^Ma*I+VcM6D<r!fCOA9S@MVL}zQ!fNAN`Krl+W`Oo{Hni30=wIb{#&g+!q6FC}Z zE1Nh~Q{%44TCiCKqtG_0iLo9 zQw!p$fX-GtL=|}n)Mf)%vWSGM;>aY&;_OSulBQIum}0Ptjvp(at2&v9 zT$_L%rw;P6sV4i?e4VAlR37EzUDE&$^OBjWO3h5gt$@t~veBf$E3`>d(Rg(=X7NqI zI5`^TY#^X(Ni)D!yg@FLgW_*MeF+%1gx& zg;!a5<(5hvV!NM8r&Hfzaq?c{q1zB|7E|VmDK7y>ViX3C;|9XqO9&X6O+`)WF*~Y} z{wR$zuEuy>HX7jRRGJj;90yFpR0BNV%Nwt}v}@WiCB`8FCk8YG)snK4suHGp+Ci#D zDoC>nMacqreKtiy<1JU}xD_X2-3r8bhL>l5bxTdziW$_|w@=0b8c!$_l4a1OjBp^u zE4MbyC=`oW%7L7Di<8syz;5rfA$1c_oChN!F$~K+lc=e=E)uA$sOj&9p=d$=FcC#qL3WUg5fL}zN_<& zzpf;EW7|M4=mCA8g3ZPrAm(CLUjtuL8GB=>w}7pn0@T3Q+5p772m5+9%_`Y&gN0k! zhkh64Yy&Ihuq{Gu=fF4PI2yJ070X5&~(xSKKW7CXX8 zKzqhwMq2AoOFC+Bv*lm1cB1bV%+H1|ZgzqM5Os~K7QF|Q&U}|3FDWBEWkud!q4|G< zHu+TICu=_V*K;S-LpJO|T{h<27!Nhcwe?~dG?R4$`}(6L?{~4EFL$)-zt4FMj4Soe z!IQ&gDc`B=f=^IBj;wQj;t#9U-F~#?PX zI!Z?<4|ocN?{Wovg&7J#?iBRZQ!fn4o68q#*=OC_NgA#q-#Po)^*+iojlWxP{~_wm z7Q*6ursvD3pCqfPGkTjTDTLyy#kT@BQk%s_Gab$crt!Gxjrg(kI0A=S$iU{XWtusrR|UYLuK~h&dXP_)M*c7jZ)>R$zG?eWn8)q z+6}~9+=;peh+fpfb1(8Bq-;e7OD|OH>S;5g7Poz%cVzx{EbT2w5g%( zH%-ORu^!rP_u>ETwATb-shs%KL05pkw7tqVO1_q4MH8bN`jg)0t^WSDek8qzw96k- zR?kx=)a~(ZyOcXcx$W{VkWyTbDC1#A*?J=7!%&tOK0i6?=O+j4r@sTsW9WIpJNJA``KSIn~Q_i;png-w0g}egzCr3pRmQz-O?JL(c?XLj3@E z7+iw>x8TF59|BAp-`1NN+wr#=|7#=+kjsbt82Uya{Mc7R?*>nx-VPngo4p>hH?Ug- z^_n0g+ens85A* zZRe2t**9V*%q~f22b8(YA#YfG9kUa#x1hIy&!heg^dSIG!@zy*i!r|k{Y!vrvjg|G zZ$te(%(!~%%qf@>J8x5b4gG825zq|s-QV7TTc-5Nz2HyaKJ+(1?+3YWP~3v~#h8_W zjo?i5CxOL)DmL)u1l4MQ58Oa~A9hJF9`zXTan!ei)nF9*bD>kf^{DRvCE^Bzz-gHO z9-IPpfQJD~kb~}f-;TRh%;o`>1c!Y~;$G|;akC0K2`ona5$JcIPlGblmjUS)#w$v6 zq*65M7rrsq2|p9`{Vu)Ur8l_rM&WzH-r;-tg>Qh5%6T6sdUW~8)Tql(=DokfcRBA> z9p(3)wfGP|x8>YJtgEIFa(=STlHVBDY#ZxP`B6m9PwI*M^qMT)dSaFqWy!!_?jasD z&-N@%e{s9L7>xPJQ}!3YvGgvYq*3Z2ONGV%DEz5~yrb#m3wE#@x~cRNr8`Q0Ryv|=LD}lEuClw!zFhWlS^DF(V&)Yn7jK=YtmyY;K9-o6c<7Q~;>zR3rN`_(K7Gbx6Vel56nJjc z#J9^2r*zMmt9;~+`A6dab^9}?-5+JXN9{23>$RiYPikI2>D|krpLEXI!&Q4e^u#N7 z{@dPd|GDS3ZF@d$nFm5(lLKL9?DO--GJJ(iRjcAWdZ)Z5Ap&r_kakVmZkhUHRF^uvw{Yiq;# zG@MTb_O!!QWw@&R6|0KMW8gBMVXd|F9k>tse{J9Lo&G%s?XwQor(^D%WoY^QrTRCm zuhOrKre8T$?hAYOq6?+RO1l41(ow*xyu)MvUvcao&dk4eX38$6w+~bHa$))J?FSSz z@gH_Pybtp>A3QItHWpM1haC-P=HI((9nSIL95;^1Eb#aWoO8IN{9d!>y>IQEcX6J& z_}Gv?qQ_*tm`%^IaK3xsz}-b(E_%6Wbg@=^e(}}C-Nnxo|GaoyNx0;~lJ}L|SJGGV z%aRGDvC?&=*Oh*{^c$sz%H#gI@7KO5{uBI5{Ehw(`5*D`^8d+SU9`NYsp$5iFBCmr zlzZ;Xw6Mhe{@TB7d3f{B>^W!N`^1X|&kK1a&c_zvwF}RG*#0VrU+}Q~rM0NRH^6^` z?||=tAAsk;kHK#664(oV4t@dlf!~1Nf!~8af&;*R;lp+bCJrI29zoQg9Yn z1y+L#!FxdxG=OH%4z2+I4F2_;N9_AR4|oVX0(!xA@MZ8l@ErIFn7rx{dj^;V=7RY^ z15xlUa0XZbR)W=FEw}`jU;}6dpSk}@`$6y+_zL)MuJzocnSOr{M^N_pn}(39DoW+9?D__R4~rPB&cATi&@ZjeQvvbhKuD; z!PzcWLj@PQSPvD{JWYE3i?q0S1uA&W#c!a3Ke+IJgS-RfF2+Cw6J1n71vA0%V77~s zpn?UU7G!=EpDq0pd`U~CKL}$cc$bSMP{Ell&VdRpaB&G#V7h34-t)ul_G4fNc<@Kt z?TtUCjKS+KY`6amj)O<55**9?TEGAG4!h%%8Q6)l{VIwj|LAVGAFT>erubd>pCLqqsWU4E`$ zC?8+c9C+t}q+h;!8?qtm{};Ye_~_;5nwO;?@imoF_3Kb@PAZvN%e zQ}u}T)Xi@#FTYz(xsF#x{9Z$HQz~f$k!Xd?C1efpFr^tv1tN$kvP?t|A;$|5P*E+S z$AbYQic6lb3?U3kq+mruL0$uhhmpCYgpfLv((F_q6-9nh7#k!6nHB=5;$Z{1R>@#o zSE5MPiX-ES=#c&t)@{NxQmD+B6^kH~2nkn8T(wOth=8lGnnEBEqWc4J%SMiqjX17g zid02|3KEgRb`058sTd-j!tt1noTpSO5DZzomq5tTkfE6Iu!`s-Bn(A^d>!(o0%j8N zTY562rVKNM5VDk_MvO30ne0F$l_aDnQmo=BJ&0f?%|rrHie#k}BN?%fgM}b5GmN+; zURc19jfkd5+~Cb;WRPh|C606~Ybk_HX(6Hug%M5`K_D7(l?;t`C1tw(}th;k**72JmHXfiBWfLJRlOaVnHmQ*S#?{q|{ zFor@#1g(^TgtQnn8-Z*@7eWviqU4abMXC_Vrtml`;Tp()Q>B1Yp(s+-3^LQCrrSKv zflMqVOx=?DfcP)Odxet(XCQ1WprxW(l4nI?YDiV0p_H9OE3&QlgSa*_Gl<+VjfCi- zq^%=*jhe1UaW8`9$bx8CL}gLRdQ6R)JP@J>!-y24HH4Fz7C>GW(Gh9@5hT1g9ygJK zW~njNP^l_ZRt@1~vA75V6ZvZfg+XheVuyHDL=6X(h#IjFFc(8k9+Kd~Ase}4Da6{@ zDKajlYIcMiM`jt>5haqK$Y%>7qRup=SFlafG@>e1GpHaGEf|bZY{)y~>xSk14a@r* zmiI>s9+vm_m&p55L-BCXL_Q_*6d5@=Bx4$)#{A+A`$ph?dmEQ7jsxr(<|Dqehs~B!UM^(NzR4#zQKThdAS% zD$XMee~5&{ktO4BP%)(2iy*!*8fN%p>`uG7t+HOb!f2bL66?09!!CyS|1D6Apq(_oO)M7)Lblee*S5JVVb?i*QZ#MDx1QuE%*LUgA{?bOsb zFESZgF!#+YJ4kNwZWh4@Esfa_?`I+R)p>JhER7i!GgPFLhUDEXGo~H(?X95ABp?>!38+{Y*`<6a zA}iy}MpYvm3mV~wX)p=mEiQz(%9~taG9spvJ&cy#`&?!uh!;d*rT}$B-s&>#NJLe2 z=gqEI6v38cyRA}Z^;AUO?$V-mBx%{s8(un}Vl$awb}{gtmrR0V%sS$RiRjQI5;l`j zp1Fz!-8a5MRtj;Qb`)`-O!1KNNmXVVfDp}KjH(_>#u4$!IG>D!nY2Zmx4`02gIZ|d z#f+(8GA)(j6Omeq&{ZbG18;=|BdVVJUYHdO=~fWgv=Qc;yd4%oHm*)C$Qxoz;d$ta z365$GyeG!YmMpV%(^lj1u9#_s2i_J#qAOxqnQSq^l{dzS)Olx&xs&tO7<1{o?~NI8 zJ3jQyF;xp0x$lm}kPa+wkI9@+-XBBCtzkuy1MiT9BPk=I^QakfPkEEfG;QZyGG=>b zTrveYxFJ1=)KolMLG&Rro(eIYV#dT=%XzPiszULb%z@1m(=a5bB7-)Lh+8Tg@_z^3 zE~5>yZejv;@Ec}SdQ-LOHJA)i!ICK?=6dg$MUdr6_k)bs7=1_#QLgc%yluuB!+GCK zPaXQknJSH)Ss&B*q`Y^=%*%Q6Oivy8-80&an zDd4b;%9feGyq`w;(8X_4qNM_d%0j zJnAuE=pFe+!V7_OB@S>sZaUE40kxnd;s-1echJM#n?D=-3-EupgoC@+!R^4iKR*xq z0DA5LC^tb5K$k;hkLOvi6#K8C{}Z?uXy^;MQ~xS%Pr&UnY}gF1|A*KA!|VTHdH=)m z{`1NE&)&nLn-o5wVDRYf_XmSR&QUnmtU1=tHQjRrC;yJ}c}XyPUNZRJXDOm=-bwO2 zb~g>2-$;I{N4Y+#+4_i_SA<(iIg_7D-%0tIdHn9uOtU#E^YdB0LgjZb?P3CY=y`PJ zTR*DJ>^@&j-~L!MbJv&VWq$KsU#9$DPflbkM+A=KkI2>OEptK`__J$VR_Ph{aAqgJ z>|f^C%+uu8fP6~%i40Ns-Qt>Ob>hA_Eq*+Ip1H_H>3!qC0hyo6Z)i9&Z_WK3E*wce z=dooXvvenE=t>ENNHb?bs`qUh_^EwMT_cDA;yTetVoTlycq zW8Jz=tD`fh2HBrcLpcGo7|qr|RIl9o_{*RC+@3AB?Rn_qxY^Lwx!&B=(b!^jbl`?< zx@6Q2^1e!(M^-q7l(P*^W;mfyc~U$SVb{?0)!dr0n(&fH+O^GAr%~I~WHhs9#=|m! z#+HuG_D#u7oOT)=8`rmNYBsI*>V*;X?M6#SlhN7O+L9Y_LuY4O$HLlL+i1D6F<`Vc zE-;P8)TRZ=*5+DtwT`#i%NyIWDgv-1@SMPmwzl<+DH5awL*XHI9agg4>clR1sGYH? zv$c95$+ZZt!E&;ysc}PtBwlhy?AJH8*jBsMl9YT*HW=+jveRl`*4W-5uCB1mO^xeG z<@!zSk|Xjhdfwn*k1sBQ=QLP(fvUO7nsnO?GCpUF;wl+6jWv!Q}DDmbt zNv6TxT~r=@GR+XjN$hK|Kg#ZFk`1^J$AdZ>hJ-bclJy-{lT;P2Oj7NwZP+^%a{UIY z#cC(N)+hP1p|$-=B51x;(%smSYQ19p6%FgnW-gaDTiHUgjiwIrBG#k}rTCT7f7qqo(aPw`WC28;27-)a@rp}fIZPVpV!7IDs&F3$( z7pbAx;^?y263&c+ydo70FJl*FN$3nta^o8Qn~cUqZH=p)e3A?>8e6KbQQFo?yQKd= zx0^mdWLQf(m-atYezo)m^2TK$a;#MYx2=ORtXFg?2g$KsapR$Ktc6cRWLZm}BYaLG z&w4YPAJX@TJZl}&Cz%R!&>-RhRU$6{}b<@0`<>+rJOez zMb5RCpVHm009Bw8^ni>@w?V~>;aBN)skjxjxbw_C`PI3ytE(85#DA~rwohouq4KK} zm`j)v&sHE|iX8eb=q|$8%;v-j%DihTd?$b*l!}t7;xqJQx&12pH<>QPW zZqq1x2c-9%mcM^#dWV+JU#kC0)ANq@Fa0KRF-|*HFfYUyP-jpN=Hv{?`KK?#6|g z-f_{)GxyY{Z!RxOudH5>{^s4|(mzT2Gm&qflKxhBW@hu$(Q-d6{rp!Zr!V@Cd5K%+ z%#?joW!clS(_fpTXD+y7e7f(E^29fG9-prLbaCd(KP*jrdTE97=*1J#PkwQRVy8|~ zK2Q=$^gp^V@zc926IVSrBk`V8b>=|*$?0`JC{bSh`o#1{JE{{mTzhglwkDijR8^Ax z&c5GvYj)p_KsEdzA!rT)J2!F@s7wiq__~j4>FH&2({0zz%6w<*^i1Qp3F&WiY4UxE;x|Sq zU%KQ}<$>@hWk+pQ;7vo&+2pKFJe8P}`0diL@`syh5)b@nX8MIG6|x5``Kd%( ze3|dRzgpr;2dOK3(^dsp>bK);_rTvmA>EOmddgO_r^!I-_F1_`u z(-WWi_2jhgnMcnWqh!7sknfN( zNxaatF!Ryr5s}fI8TZh*^lyJQBAxz5X=caq(-W^=Ha9W5q*Q4cd3@rRA1ltJ=8Q@- z-7rbHrZ1TJQBf@O#uzQJswXOSEOGT2C#U~BYh>c}ODAM*`09kj_5Cw4PnxwN>s!hp zed{-C6W@KID82TE(V6-Ga!O|L?}D;NpMI?=nCbsxwesD>0x5Tq51#nsOPVs`G$k|V zsu79g@=2MmePvAM=514zvmZJ+bNAYaGU@b_618QuiuKKz%G%RTOtk!mnz`wgQxhxx zKYMoqZ&y{`4}YJN=_Vl)8Mw%h8v+`{17{wfmV*dH(Gmx0RA_QW5+M*tkf2pBbs$bP zzSbHXYSgN+{;jdR)>uWZQ;k{`tJYX*jkQReYSh0qmRjELbMCzm5N!Ikmgr|+_+_7A zpS{;!d#z_Z>skBx{kAO%F7f7*Uy_V{>B%$3wtV)X(YB(#2kqx8$L6D5j6HVRa9mTe zyq}MD*epG5Me?uZGnyZL^@*c#k`e#p8y?uKd|>GjsMxb6w&1;5;AGhDM+CxKlo8ciXS)W z-yZJvNGbPkS~+6eGVVX>H*MTM+8q#OY&QVEg)$27Z{&XM`5!FZ-LguEXN-7mpqJ@8%;PT8|I1e>kzef7e~BOcD<* z*O7LAwYz~n(3>apfouo*K(+&YAlny)K9KE;LLbO>pbr$!OvZIZI~ewQa$P&tvM1NI zcW58C-Lv166?9ZrJeh zXT!KYSwgpL8|1HZ+~-)gp=CDzxz0E0crR1lmn+9M`+mS>ebgF{{&c8r>|mYE!>I$! z*zY;d_m$0B+uj-Sk!NwM&58EG&PC(;vD+V!N%nfkHP+jd(~=d%>B&fS zdU6nU8;Mt4Ha*$*tm(mi&zYWVf#l26lRaObp49hEPlkVIM$&9~vSibY zgNscU{6+iJ{05rLv5uDq&2wBM)^-Fe!QG9f*K1)F?9G_l+Qxd_F55c9?_+<> zbFe-*f8w*ceVY@X$9ZDkIELnz#CJPwn-j0&7~5ej>~QSR+l}8eA=WKNeKzX#x_}Gd z*$>XS*ZXb0o7CQoa;;U6(9quRyC$r#Z#$KlVjVNi zGYkv$j7IkDYk-|b$nh5hV99IpxcV0f;3=zFu6!DmiCC`Z$F z6k@yag7GpAO&kC19-r-pee6JdAK3OO|0wSki-`M_C7I>3i8AeA6ZX#Z8QZh+gnO=G z9ow@vJ8@q7Zw`#-t;)t+tq=pV>Vj8lX4$6O-LMauIiJBc z*bVz&$XqTDt6>yIstyAiVLOb$ewc4wSBJH*|F^wA+k03GTVWUMg>b%Bz$)1DSmlA? z3w;dhU>od)eK2G`n1|JHkpDNrr(FpfVLOb$ewe?+xnM19h47L0LbBAkU=?hE9k2%u z!0?E3!aCRnyI~&;Eptv-9b3-qY8Zu`a4Q^yk%n@>M%WHxupj21sT{Btw!$vh3nBcx z0#?D!b&dlEVPw7Iz(&{(W3V6QKf`fgEo_BduoohMbOo$}EwBT24!f4)l+*ljWQ~3N zP1?I|v6z=xj3uX*mw=y_?wyyLpO>4D4yT((#gwm9^EeL*g?_tOD5D_=4unIw4<c&41LQf`-I`c9p&$N_UXA zA36IyhmJh`$kCTBKgi8*ce{;Ft0&dJ*9Zos`{v^xnu{NK_>o56?{``)JX#Lciv0RR z^Xdor^oQiqM-Khn`SWt;QFXPk7qq34-<>Z%nJYh;BR|NGw@anh>7Y~^Lb5*fd#->`S-8--S?tJ!%T=qd8`$P_V%(T52UIRY`T`;*0;&-N63A2~H8ZLtp91GXN3*fUb&oLeb zw|o6ccpuyWFM=>_KZF(tJKe2GuS*W}=r6VX3Fs!tN8nWWI?QxD<}&#Nycb>u?}Q8C zW;o9G8F+`+Ux5$8ef8w;^x3;$He3&<#cyoC3pc{^;RA5256`oGDx3w(?I4Ffa_5Da zlEvO{f@yFYJRaT;pMpbm-+%1=yI}!b1mA!+!8hR)_#%8Brap%an513bTZbtLM&r4-nnBL+YI|4;+`9$qEBVB z;a+ji0()Fy!vkgKsM;3P>=QA(Ew-v)5XT#_okV;eh+|G}<8>Y) zW*5JU_wn0!AKSGPuVbJ1&Bz3r6Wht-#CElI6Tgo*-Ohma(~RSK9pA0=I_%t zfgKT(n(75#W1TN{_}%tL`Tpeba*SzjW2cROAF-r$j_&MVE>wL9;&OOK7>HP={ zE%Z5quzQs-v*;c?6WYtARFrk3n%suu8oC*edCDnP5f%n;M3v3yZAm{1tUlt zWBlJmgU^o1F7bZ++c^68yVx_zuj$W@y5Fsh$xcb;_cF=WepE^(DX3G|mo&9Wm+t%V zzxbUzyO*bVc4?m7k)3BZo^p8DX?A!`?@6<()*8P?S$2sm)rtOmul36P-)76$tH{G0 zK0T9sNhgl|Y>c$`^k;TpO04Vc{m!&x_Z8zkJ@#8L!Ef*&^F$Z#hbMZV&r*MIc^3-x z`|vKPufyYuKA64Ro?gZl$KQSPuY)h>(7#`ne?57h9^~f&eu0NVxenopF8-%H(S7cD z`rHg3@DM)!T^}EB1*aqrw$9F7cLiU-A5Hzh)PI~hW!j2qkDqq+wAW7i(6nz%n?C)t z>8qw+GyQebZ<+qj(`U{&eMT}Zo;7VAbJXCu#OB2#b4P!F%bd|muUy#Nx#_gvLP+Xw zI6ir2`=RkH@mT)$lgA$Y!YQNY&pK-KwP(x?PXFY0=gn-Ml^hq(fR1i`c6Idn_M&(W zGoIZX`{U-q=DRN~#?>Tkwob8`~H`{#l zBaa(A6CBUm`a8tF9U;&6-)NrtfkC-0xF0@_tk30GCaYtRN=V&kN(?gF``!vy><9W2=f6lXiN>VG-@u%aiMQDMR zR>%y$e5aV}mina%)=t#%=<2;h9EpFvQOA?eMV>Bfh5@~RUlb{NO{gppvTNt7C=`mQ z3UH9)5U(SZM5|B?w#cGLTrELH-m7=~R9G+=`!UXQ)50 zH3YZ$qSKYp%3|Tl_dD$l(*J%^#V+2-2a9g06g!o$;C5?Bny{-D8toeL_HKeA61`=$ zpQ~crz#Q)TmJnsGn^(S4q1wVhpCE4P;v+BR1Y8{n@huhzw6vXizEJ7p+x>j4k+kYT z2-|gG)!@X(k%|boRmlaNf3aVSYb+!UjHsO;p~uRqo_al0y7)8Eus86{R`71t@`)HF z3e-X^*FbKI1-BoR!fgc;Pj##=GR|UxG_;>DV6qpMjU!d6xkjgpAO!U$o>)QCUNtF* z+tzF7>Jgm!r>3$RRV*UCMh8K!&~dSvPsDgzrE(E}e6fsSJ+B_>y&%)?c3gKCVQ8_D zC|=JUK>v$=Un{`FkKRFz7R7Ws_(_ZX5(eWEM$&vMm+vC;Ct9H3$nbM*yqIW6^Eh_# z?BlU*v=pb^EeCgC+?G~7F1A!c4PPo%oeoh4c2>mrH7xH+QYhu{<f`pPAr-DA1$qpE{Me@}!Sq~0+uugdYR?i9Q^DhWNfnf9 zG%$LE7_iTxJokO0+Rf*55bcr6s`uzfWUu3v-DgyPvB0xjS#kd-D&~|btMYKz~->T}5NiSdT;fL0g zs=;qlt7!mY{kbBB+ct@adM8iZ!Ot9$K88c7aot=%oZbK~*M8ZZD_06cF#71TdtJp1 z!3n=`q*^gosdbA?9H!w$E$O$3dDJN{reZrTF&d>6m0aAjJYrn;Bd?w;4wSHbYxZiqpfV6A zqwuM>aUWAgu$Yp#*@%~!CcVQAE@@O6EKLk#$lVr=g-(xHNUxo5S7|8p>d_^8x(qQ9 zEZbfEv0voqEYVbSPXU1BIm$5~tUM{m*k++{KnBra4`rJJCgCIC`v=(1%Up`y&VlZ&~M zjz;ns^{^h?sktj%^{k6U8>e4U?>SN(kF}L4t2eF=adWTQEp~I{SDcRAi?O9x${|T6 zj$zTuf?CpLVPZ`URbkliMqZb&78K)A2W>Zk>S`xp-C4(Wh_Y1FctN8ZaF81dS|P_# z?)E8?hUQ5R*Lxmodnk%9hb0U7+zTgeX?Ltjhsi+ zll~FIC_?wDox;x7sbb``eVwd`a=qV3f^!{lxTdB%X})HdNug6$811Qr0@^v< z<$Alv17PpU9R%71GB&hLkg>DwHKUe#;u6#~7`A}aNWtMsj94Bwwn7$|+m2Qc-CV&7 z^h^lylod?VObW%eWl7z$l@c<3EgVaHj%JFHvr^U(3dS=% zuwmtm)e6}WP7hzBobyc>Rzp56dzDe{ASW+%>)5sveudV}^jP;BSd|#^iNjd#4Y8o$ z3&*qX{*XTD=lnv!>#PPU_Fhh(D_Ya-5#yWnn5~3qu50rPpF3r zlmy9jtbW_?cIpqz}17ZO>Lb2TI3Yu) zfN|09qWKz0HBag^cH=gb5<@TlLygz0F-LFVMmSYYt;M*NCj~`Fp{pVFIitw@!xj){ zXd9Omj3-d3aYLyHbRD6j@q~ntoro*>p7lgn-tQ0?CDIdW3s#(&UyUx>c)1iV0sn^v zPJwU}PqS6l5rQT^+9T^=C+4u#)353+WcC^Yy8l9=uNWrUWgjQ*TwO@*pO zncvDE=TG;oh0|%Gkx*|X#(fe{?gMAo)wOkA3J{f?ThhAgR#(qLRc?VwewG;@)AukBNdO zLp1SDwKh;ZDiky|suk{XjVH!$;&5m+jc%txwWFw>dZkvyZS%l_%IwAfRAZkAeR(#c zo5#z~{l_8K4fT%-!{KzHAJz_Ou3nTHdw!wUNcslhu#L<>!M!v0(w?{xd5w~8nR+W% zd8{>0aJZ(t$5pg~Fsx+~XSEvb27XLFUn{BcP!}URQf0McG>M!Sf~!!Uc(2 z#&=W*0t=3$SGan@WTsdqP>m>jjjP=-th=ogIO6Jz>s-vf2~nQ_6o-a#SyJe?^7=(F zr}ahi)o5Dbbkk(ksYBA#%h<%6u6MRLXlPpgW(32 z`vw@SjJ!kb&y7?f2E(&2H4`(#tD&1g>3i^J2%(T%Z_n`fHD zx*C2OeQsWhUP;3FXLmI*`tvY;0&S*%*wJ2X05lW?k?%7hZ z#Lrq}P52!&E8^Z65r}@|I`qJ}1)-i&O+w(K6vGZv5O&-=m+YH@nF&@41(R0ONv=qT zURaD$183A^+E8E$%h&DoYHhcahe0?d`cC-sS^-)@7QL=uT^>`%uWMC$bNyDmQ_+eD zuBptr%Anq&p;ziPBboVH&orrHB4pqu1E>#}-^D1^sL;n5DD%|0vw3ZtP)2p&FsTQ< z#pEPH9wu0tKPAF;)u3p$WP%+cJ!UKADt^hjD1h4*dUO?oKhv^ z?ie;&`MlA-(k4>D_vNlCRnE|_;6@m8A@XyKq)IRbYK#U&?K0e$Zr5#N#GFpG*vSag zDp#2RPP>8F6I*Sw8B={#Ao$YZW_Ub{hzSqNIGYIO@Qhd$y;$e;Oc>wSgi6T4SWXqB z-&If1W*R~!0vg)WU@~2Bi3W2`GoE51DA?<>tYLci-|jM{pb!pXeG4W+yxP1+vl8G? zk`v;+*{Q(TyNkSB5;-jbdW?MC?3LW0rf-&SWHrBvK{2{az2J#38i2GHlP)J$1B@8R^Sh|M(-8JISwTaQ(yv20P{L6?X6h`)l&7?l)@jK%{JtA3r zlkn_atawqQ$ES?T#6UX zBZ)~~$4sPZq%S8P49p8Ruu3jRjd!_Q6&^s{sAPUzAdyrvbq7Zh=^}~L;yN&+OjSsGG6g1-sb4TvZOy;U zxi?2p2XWiA2}kM*GLdeoavlgJ3Q~0#@T4v%!xlvhL;Naqr^aWB#&N?PE@N_-?M5%$6z$!`n6z<~lTrIM4QKloHxZ?6s>m(~TRG)xbwg7abDDgf z)2=DdGA^84!w{@mKOCwzT6eszXc&6khg6I3lR zG__K#&tSyJ6w#1w2FGt1IcgmbB}Y53*{ z+(`kH2*Ovxh!w;;$=+6S;wijOekIXV4h=!CkOwY=>r*zWMa0f4PG84dOhiRxiYSOs zILW}tGL>;+V@r6~R#2=!GaRVi^CYLov#v4b+^eRS|bpip!MsTjT0f8YZ9kr_Dq-rm?`rK358*$ zMrJz9G_fKIE|BMR%pI#r3(>kjTTnsItkWV3|17AvnI%KMdsil#))7>;;?hfK4);ZY zTv(4KGc{tO^D4u`9Tu;+B?Wh9qRK z-+7Fj8^UIa7Vv&OPmYs2DVhfrilLobj5wX5n7x_=dXAkxW9)4SoS6xTMjLbu@d_5& zxU5@=IV4kCR#zVGPZ$-dWok!2HjKTxAIuTV=O_<$%$6#GRHnnM3nLjmq4CuEn@$!{ z!Wy60OwlxnJhth1w6d=0BJoF)<%ovx77Q;etL~rh>ja#&@k%SNJ@M6JMBvaEyam%IOhuGNaM~4KPmAMU02MoVr-#FX~;K;D{^pHmIxdhIcr=DeN_VpH)9Z| ziv5zZpl(+(EoVKnr@Fdlid>?-#8rf8EMbNsb!11x8jvIPHE(jrd|y+X zN~fS%xWR(=yhu%r2NA}8V4{_dyIrzA#Kakct0r^qoqA^e)Ke^5m`Jlk+8HY$tQ)fp zrlS)+ooaC;BOftpUJ0usLcnG#Qzh(+^(z5_L1k*g&S75C<9rFZTXHBTJoo;{oByqv6_Zq23Ppc~EmVVxA$LSe)U!V~Kd zi7wGW)$BMH7Oz}btyE%A+5=7Mh;hVJlcg&X^5$*{S|{R!0w0WA#d2SGtP#v(>3AL% zc!XR8%*d3I?xy)^VRJnA60Lxx?_;_xs!|6DNM^iCXV8Pkif)S^dSj&vzPxynvVmJ>jA?5%URwxX%L}#mF7^Om9 zoYfdE2xBMusRK=F5|0rasF78S*=t`EPM2i2qm45I)&?q;QnWdtDN^JNE9WL3I@u;T zb~4bbOx-k3rqF;XcGhu&$;*rG8ea2!5ps}vi&=Y4x{S$}siq~Oq!39Z-I}K2LNs(9 zZ9~k#byA5Jc%IF9IpbtTSkI+=`f(=DX2|EZ$fvqm3D3kF|(s!#YZ2DCJ58cU(l2 zGh}$RPOB9$+%03cwfu1W)L6s3!=#0_E`w3#;#h{}Ees0I$QaEF{B5@(M#Q=`)b*lpD%&@)=Vz!ebo!e6ow}3;#!>x)!TLz1DaVu|* zkkcOY%=2Ad2!QN?I$B3cl_GsV&GI{aF?>v_w>>=WO&Erw^R z-y%TV7dS3=42mQzx;j67UNs);DI7x=OVxrm)D=1Fh^}#^sJ}8u5$j=c^}9hsHJuT% zD7*5I2i69{FEHLZoqj32+V@t8#VSU|lu0*45l_YN`Icx3+7t*qx5RHgjI1dGK zuUb+p5}LT0XliuPC^*Tc2bRRSL{j{CTTYNu?M#o6$xhC$0~y(ia?6Bq2&|e_x*5KS zju6wycrYTCYE2I;Md9l|P?oh-fsIBJ;7h^cG7D%0G;?q&;G z7h9>fFqtN(?wE$Do0~Mc!CXiUNLb7&YBhRr-tvDKB(q^VMZw!Oy*?yX`Kz>HjZ!i4 zwWJHigw5$rOhN&s90kX-ByL%@z%L+jYrQ1G@q@T;bOa%%Tt)a& z;4p9X)F>5U@Jh7t!CIL|H@Mmnofntj>4-0uME|+mw4cQ$`FZL|=QInCfRAiT^Fj~( zv;AuT6PPo{{pJ{4!pX)Sjm8BW)ik z4^+y7m5!HcAoUkge<7Ve{Tj`m#`C6T{;X!HPFPEjs^ICY!Nbqi62xL+3B{V8h4@mE z?vz$R+6vN+klKRO9Ht{eIw+=o!LPq-G?=!I=l4%flHU!%gW#$QzJ<`>SdCxozFPIc zId@n(^?2jZAC{ zAoMf#;r|G&`n9m}b9iEK`UFv6Xw>hRi#HDO-{{daFAzM2cE)N%g_GJDV|n@+2na)` zem?Dt14z_@H)6#hdi5KL7ffnrjK2Tg+8GBwY5yLFAjUVGs{rV=dvfUH=@3EcK ze~90&U7@ZgIOJA(zYXdT=L-#pRmb9dbL_pd+KP1vQJE48y7&N2C$JwCfXE&DP1uYx#VbK-Su z4@K0k>iAtB5B^Sbtoy#2{Nz`?z31Hj`I(~J=TGR_uhq^n$@Jvbn=(oMecB?P#HVO9 zu4}*(8J85*`OE$u=ys1=?~jG=I?mbN-)Q@MVDggZdjCQAJOoecyWod#9-QJg7r<@t-o8(Tw|RY* z?M$DeY8jkQ zZE%a%%i*hD|IGGf@N};=ljKszxZF0lDIeqWif!=Ao(s?P`RieW*LT8IP=GsNjn7-Q zZ?)~f0%NHQP1tfxW?5e9x3{vhpW3fx!;je z?*Hj>#C_?zFGngb*j@w1Dud9ldCbti@P7@Ox89W<=mXAwp{?)h4E~mn@`KGu;(CmY zrT?y8DNzx`}EIDKQMjqj5BAfF%X6R z;=TzzM%x!oynj*XF?v7HW89ayTOWO(=lG2YJxAMto}=wR&(U`9d)vY9Z3n-Pu{y4c zpP3EKzHRjR;(d(g_t^%2TA_M>I`0Y2B@Zo5>_6Zitv$&6qqY0+kGj4oBb=-Kce$75 zduYQauX{!_oP!7WTfP78dWh@Km+$ZTwV}{QnYjMJe#QIS?;LZY(!KY)Up~6$XOpd! z>66DB)aQe&A6h8fr#`ihL3{gHcFy(DE^Pw}+~E;Z9uZTU|K-2``Z>dnv6lng!^yZq=*8~*gjm-nGs#nR%4x15 zVtsKzlh!lX10uK20!Xl%NRF#YFT;%_J6B3lU8(BA&4jWtmQUVD5NT`qyAq6F0 zL387)4xXP#Igs-&OJCZa)S+%3p@`Iq$my&~>5lvjSv+V@#&e9&eY9nGN(c@DDyac7 z96NFZBKaUFbx?lE;FMb;Q>lZhMrx{T8YzVmor0jV+>rVuDZVaqN72!y6@sz?$3d;0 zmqg~-cm>D8C?`Y;iu7)YU5JwM1w3=otFUFD@{{KzksEglJ^^Hz(xVl(Q1ifjB2BE~ z`Fwflk)wo@Aed?7bRc|^FN@=;S}!Luq&iY;WidyRJyM6rE07F~AX0{7RbH{gkvdw7 z$WTV#C8tDku9Sg-Tuxc1(m`>uNOO}hEOA3-Or#XJx|({B@D^M>s6c`+LoOS}I$6L$ zhlGIDss67 z^fhX<5u{gWaoUA~tZT^`31*p2qb9c|5<_Kmp^cKGBonM7M+M`Ebk9bD%0^a{gm~$t zS^;(^2@wt~cUOiL(mvVtC{TiFOTG{W8n;E3k5nDE619^oK}<;~{FF(O7t#j#))FnX z4=pzmw&VvFWLaSxsZ6mA6Up2$$#j7Xr778B{$!FeQ^TvCkkS9MMVm-rkIC?EvX&1H(UThc&fsj(ix zM9_~sUq%2`7X}}B;phW3mB>G3L-Z(24+R{in4y9}R!8eZst$rDCzfI>D^oL(Y>T%7 z9g@sZNvsl9qh(8o)}>^72favNH!4YK=HxacNqDamJJDNoC#R z!Yhy04}$i9RnbK3g#Siv6GB*KFBmrD1Zz)T2Yro;#YKmBC-_HNL4gvilk(5-#ko54 zGYy`s8;ESMDB~@dL?pJ!_r_y~ki%`!@FibLUXpc>07mj&3tvMq2#LG6T~LeIDL6&Y z*Qf${bs8;J1H?)};uy_cGGAnMVWMzhC~;~XTvA%B4CwM;42*GRdXhOebl{llJR2+t zO^RR(o8W{lTyQ4K49-_QgA|!%ZrU&C#L3+nXvN{K80f{x7uLn@tsB>f%wUu`C_%L1 z+PyU7FnSasmv&N1j!6h>P{2XMq8DW8%W!8nb;n5bmyL>lO=@!w^FZj!8BYcpbNJ$z zw;luAm-1~-+z~M^!ia~lrYwtH9Y+Eo$jXxtEFBsH8rvz?D;S`#9YpVt_^6$te=!a* z8?0U_bm_1_J)kz7d@{sS(mC%(qYjS&8brjeXe1)fIyCFb!HptEzGu{xoGld=?Vlu2 zHot;MO%s%~?@qNPog)eu=-SnS1J~_CN`RM3hPd&pfTIO}pj%_?bSXg$h#Y31c_$r^ z9GdKWHnG4$}58LZE&hc1DaPv=C0gTe)6Oiczo4p{^# zI3!v@XNVfAkBNuT3>pR;6|&srvKsq>V{@RZ7d;FIn`Bsa5Sd?4SmGyv;C@6JVRg1- z%&kXSWC43(u|+tpg=GLmrREzNd^J2AtbY8y5n6mwJ_mYy*q|is*O+?@jA&Rxo6kuH z`g}+|1{!@Ri=@qhK8y?SZmmAK$G@Q0r_tkpWu#IEV&2!Kf8%CdLw(ecUp(6W8EC?@v=vDU$nx zmavLo&FCzl)}{EZ#6|quLHkwEmr+m#XU4sB05y7muFF|sRoygQScfYRj^mYap@ z94(k3Lh`noj=T(GnXGL5-^Oe_6F4R(^aAr?GzU=*g_-NKGiepRhECy2GE0-^WUos)+3#n*MyGIjvN-$dB$GWgdl{9&W0MbLo}amv zM&Z{opQTXvqU^&mw^Jv4YUX{(b(zbucO*AuUX+}hy?{Dli!$Lg$qzE$PTrY$LH4Ve z24%vJWG>2nCi9NWdoq{OCwyh*-I;0G(mJglnFnPc}nKJlnLKSm2j8< z+mkXEXC9M1F7uRZE?dh!GLy-CD)Wxy{h9TduO!dOyfO2^U7TH( zS(7;{D%RyJd0?fT5qvzKO`l)W}{T=vJA z3o~ENY|1<#`?1XXGM8tbnmsoAU8`o#&5r9tJU8>f%;z)5X5N+gVdlKdDcK9Mw`I<= zGWNF2S=PiBS`RzHTG;IDDVZ;1&d9tdvpDHwHd*hkQA%w?IHRjHp_k$P+925V8bWR_>XY7OdTnWtNQx-@&a zb*IN1U`=9>T5BKN)^uzss zg11k-rN43bmj2&8{FeR?;oncYrT=sIr$^k~ z3)_!NF8`h4*azM?eQZN}_P#$D*L$3ZE8f4($GzjKgZSfN#Aye5E9D0_o}DjG#9w!N z&MEGb_{_cZ%vO4yX;RPZ!2a?6dVl-H^Gt{CxBtw$HuZb(iUe@5f7xUgpIHr#vt^@=Pz+CSPNTW7wm;(sdK?9*aACX z4;+Bu5$A+;unl&@J{Vf&oUl5!bz2yPop380gpr1Fz(&{(W3V6QQ^~BuTG$G^U@yc& zw<};3>|EzKa1ch;I}U7w?Jx%WVg5552iC$?*adqb9zIJ-Eai@J3W|HQyK3}0sf1gSAdlqWt$DCua zV|bQk$6mP*zTc;f=dW|U){6Jo-l|;tzo)L1{oqG3$-0s<9}l)W&BcT*l8yh7O?GMf z&75c2H)oRM5pcEhYy{`scTOg$|BKfja4ZktuJ9o9ttW`^=pWnJc z+4QY-SK8Wt<42sw`*=`&Plp>{ zncVM4Dfe$`Ibxj9)E@oo7$3SqjA;4kxAfm|!7cso{LU@?pQ(-!s2o1OZ-Va}c zJK!#uwd$7s9GDC9;EyjJ$91c3>3{UnTlzoy(JlR`7jc3||L;6k`NA3RobkCCKb|o> z^vI$1&<#WXXXpz<|2lNs%;wDA%$Lr5&&)5)oG8zEM@o5qHk1AF7v)+o9tSx>beZod z*ZItRwYUcBgCi*8-?lSL0%eD>n*;+HJ`^Tl6Wd~opz;iLJB`cLcczgmB{E&e9oce4KY z7dQT_#Pa9AH%=G&*0zTF33LhJ^g7g1V_PqSO~|!L*N7$f#vW}I2BHZ zYX3d`v!Ds*!G*92E{3PU8tB0@;40V**TM7P1+Wzszj9aqA#frrhZT^6BGllw;2by~ zE`*EV$?#P8J?Ov(kN%7P82mN-$9XsPr=EXPezU1eJL9Zm$=?mi-u&{PK4SDW zv+~XV`{xf$_FOR%Yx|?+ZL`OweO2)0wT02+W;}FsJ-)J7kc96j1z{gAjmmV`(ygWBL z>*P{An=rclua9fKP%8Vrk zYt66z^pw#npD;bS_JR|W&wal<_Rc3POCEaC@nfgIe`fPTN6#94!?BB#Cw^#9pJNwq zER9XQu#i0dZ->U-dh1ciCw7%bcU&=d>{CBlF?#Tw#pK=ZTRQevx4!KbV^W&856eJ%3N}KykR#EUn9L%kR$b%MW>|Y;|F@F#m%31#2(Zdcm#>M0HmlIC^+tbK$y$ z+ZOI#xNl)Iw=TCWw>!5lH*|Xb^wp=2o<34tS>0IOULC9Mug7JzrmJW|JN7jvO8`(XwZ)9j$e%b0}`TFYm zXnkk>*80KvNMmJVV`F<`tg*i_|IGTCYtP(z=B_hEM$cMt)~d61uDf;J!F40+SFYc< ze*5~d_50V)e@6WoYoD?88M~e#>UsH!%U4~#ThEX2>pUs` zQr|w_0a?$*3^Q3(f=ZXEC<4-2u{=z1{H!q&Da`M?nI`W#kkx`qMjw^@x(EMfjvodoFEvogcY6}O;A$iM@vm7bMecJFXTEq?%oECTI zAP@4656L->T;sd*jO7?p9j8(&F*s6hC-RCXbBZT(i3fSacA3l@jvV3tEI)WMFF5jm zd-Z&;;6I7by;6_tGcoi^?cRC6cjx;~S~h<{QgadOM!Hj41!*ftJ3?v;QgfJ&2H&OX)E~6 z?+93wueoCL_=A$Mp0FTUazd?8@Atd7e$m&W@fGgQwQ+VM%I=pc*hw3Bgp;lHRDljG^#bB?nJn~oevsvRLZ4b)<&F+JQM3L3Qi$vQS@5U!PiPx zw3-vn$J2}e8m}&+)IiKEQjHX}QBLyhBKFoIvgk$)A#ff8c%zJl7i)RG-)VQMPMcJ* zv3K&pI$R2x@q7tuaJPo98jEb9(XMr?-EM*k6OAXH=PDv$6wJPF1vz*(uY9FK6^C>s z$rp=V{OzTjshcAq3dSnjLJM8b7b@Jie!kX7u&yJ%?YgjPa7W{UMVyS3x{v<0*ssMk z7Lo?$*N%|9k`&cb52JJNgyst9KCx=!7p>(J>kFt}3wXbSiW;kVP%yU@%sNQ0JR=<~ zCdg6y`9c@5dnK6Gdlj_Moh~zuViW~7V%J_Z!R}nfO&xT|D%*04GF1^kV{1idj2^gH z%_r7oTBUL^_}9x=sPpQf-s`Ijl+Z=D%nDH>QM{fzfG)XEHcT|Ss-TFt9nC7zc|5Ge zehK4q3G*s8a0JHcq1{sOdJ8c!?oV{9d0fKn1amBkQN?K^8@E&3mR2yK7tqmHd!-T@ z_@rR52)S^ccTkJ=R4AeJF5s&!)g2I>dQUX16-27IWEB?=a~YL+4nJ)tSL*aSN`ia5 z?gqO@ir323f?FCFu2#~G8f{r3!m3)%S2~F`uSy3UajtJoOe?^Q-72+$yxY1}6-P1( z{>0GOM#9_9VT#o<(Rl~MxZZ%xwq|8Z&*&$WZoZG~9)mbmcI@wUSBF~|gLok>MfEg_ zNeR_3rtosEQ;0V1=Xm6?+*|Z&)H_zsu*oJ^weh-ZK)90iJIh)*D^V>h_N{*2T}7T; zOK_6oVn#8q8njGg%egkM+MXq3trmfBGb6x9!Qbd%*>C&3a_KFo%(0H6@olvW{k|?` z2`t~Q_4{fZ!M)Wv7uM*Zy0=pWEp@S7>?6Q0=KDC(bHS0U6;z!#T0y^GKx2&bw_eI8 z^}*s@L(9c18}!+BawtW#OS#XXD3uZmbUl39no<=ByOyZ|;3G!iUTn8C{W4VG`aOovLxBs$TET7E~1(fqY^it=jA1 z<3~Llml%!GN@ymiCgCf3I*O&9EokT*hLk6MW?xWJDW6-AZz?{aS zUNEsYI;^8U)FE=w30D8aVsak^a1r&n%GUp#uA#%%T7A7nB`_=+UC1k-P7iC#{^%`Q zpSxV@7)_Y$YK6!{Ft+O$x@=iTs3^0w)?%)tqj5h`EbGCYn!92#*3Dv5EiM~YdX7}b z*KI}F%B!nGP~EF`i``tI7^9R`TVqSHltTtxF>qDe2-TA=3lnSMWER4X6UHExp~)MM zIw9>)tW`S+>&`m1-C*r2Nd2{vo_=F2Xz3e0?95yths7pcN6H_!1q(BS(naLo!jWwl zXmpBXHHxJ+Ap_>HugjFnh7Fw8^-vS2BACbbjsV|K=+0=q#ZJLaY;oA}u-M3@$pW-m zL__*@gClb69DNL@E0Q@t$Q|+^sFrD8Fu>|b|A=7}0e;m^Vdv{qM94X)SB&7TcE6DX zM?HdcO-*;weDjV}h~`znId9l4c`hKq1g$fM8K{LFuhujrOTD>4RkyY5Vylv9o)uiz z8h{o0uuqIwrRIKD8gwVHR~O2?ge__m8fq_(&D_1y;p;wO6NAGX6F-u2=8#n_vYC2A zOhL#Pv_n#$-tD9A*9JT^kQ4|fP)Mj-WbUe`zPns+_jmy8UAcoOyin+ni>M-n8GM7%%?$A+IPbX1Y%t0R~*MGvH8LKDP^xFKyi4em(HrhH|{C;Fi> z${i%^xWBQAC;SSno9UrDFt92yXi$cXihc0&WL6GkvyGf9FQ^-&SGzT1sL`Az|s9})w0es** zf`qoYXCU}8=)E1O#7^_Xyrs`V&4b$%QXd|Wpn5@{Mvsj$>87-Ba>^Sb5yTaAgkrfz z4u(}iY_HL&eP)2C)ghHqA^^d+C@?(*Ls5u4FfRICG+!gB=2`csgdu^6NesRG4>ex% zY)JGLZiG|i)LM*N`9h~pc%rKzkr6RA(dj8OrLIotTHy&G)@MveeKz zkoIc`WDSVWc<2Qed^tyy#=Yy1i{P;uNJ+dDdfH|~qibpHJX7iyY^@`?fk@){H~gPY zoWmIt!@mB`Bc@8=bZjtl#-HvX?r${=Q>A=|eWIs8Hz3BID3ejBO}~ZU3o}ruvec!1 z(jgwgLN&C4P^#(hpIV$US}T-yxD_Q2(j>-g;v<|G zx0QAf4FF?Lv^=+@Pf3M91wWI$q<+J9=J~iSd}5Z4>!U^ryfqDzucEVaH>0B!!^x@` z?duh0zt->I?59A&nNAvgHod^5Ah@7R#FyMVPEzPD#aLlp)Jp1XVymD%QX{dGhp6o2 zoHSJo!~7}F`6QK4CklNT{z{C&{AoP=s=e!mRy6TWwKhKyKJjVH!$;&5m+ z4Qen{PKxRYi+{y!^T3hH?8X38W1mWSjWu|VZrOV#n}Qvs?`%VCY^3f4+$ zoOeu8ftE{jDOxB@6UU4u1tUmMh(JT)mhl}Gf{=tG=@qWNXD26ab2Syd#?@{Z*4;ViENNbto9Uc1l5L8uHx6`SjI0NMx#fRLXQqHF$E`>p(AwrIi7g-NBWJW^g5RR>VMHKKqkFcLj6zhtRWF+v^k{R$y)z;ZH_3JAr*R8HC8nB$ zkVz?q9i|}cxOr}igEE*2Rtp7_R?|tYNQb^!j8X$<)MVOFU<%9E?e!=q)oVPAa<345 zC;WM>fOZXwURSU#kE!F=wQ8S8R;%8rXhmJ$xjL0WrA9+1r)4BFU+bABbxec|+;qtF z0rR^Ur5Y7_LIWiu;!cIUHck+vI&hfOgWh6t5+M&0EY05og(13DG+Q#k4pAesm2$Nd zp#Vp6J8P^{JYp!QpfU3U4I6}DnA0|6&oX+OC*7qT5aZzca#xj#i>hEDduA@wubP5} z!*2}K7!8WrWwKxQo18Qb1Jh43fRN*((_=P2Viv z$ZCESgJN`S-11k|`zdW+x?`{XSZA{IzLHb#UL6~rHsMHJL8i9p2Irw@C@s>tVOZ;e zGHg-AFvPDyr)zwsXdE}(Q7B>wiTL%yFZ26CG+*BbG_7&f?;x}hnIIZd7bVWKINMIscO8j~`a#f$i9gT-2CVG)}v2c3QNR^YRKu>S)&vgyJ+L0AtGmdI)9UKViI?G z@c^eX0npq@5)KB@3P+8d6}aq(o}@daRgkuVv?HXpAT@{Sh>#A7sb7$~MyYq0I>_l9 zA)O|rGsRq51!)ynu6_`0LCv#1omhRhCKmy2i!>g5iM0$HOTywzr6k=ct%9@_q#fa~ zZNc4f;>F)g$pBI^fRqd%B?Cyw08%o5lnfvx14zjLQZj&)3?L-~@JvwJ5z;D1TS3|p zQdW#3pA{pegh(kNQc8&5d`gJJqKBE_jp*K&KZ0J?jfbH(ldNc#xl~j-B z2-l-I411R9;idL0wP(ad{iXKo2(xDsIw3cvIw6Oz6H>y}^XsA&BCoW2r09i&0f_e2 zPmAzQ7hVSMgbU$jIL`MO zc!$?tfe*q7-sfyT1IxU=)An638?J}beCGe;yKo~sA3gxb`tUs4r@~pV5Ke$od`=!Y zS?u*Dm%N%#TW1RG!(oDJWH zDUMmQeKY(VZh*JJa-VYi!INON_fN1r z2BvxaNZX&mX>bd?2P*Iscq1HRpQ~&i205?i*xm&%fiJ^)SnTt2ZNC5?gBjjW*td&D zXiKk{k|bMUBLS{qTGA_Fg_}}`Rj`$?;NE#jvSq#>D_9-&!SHaBG>^6K@kz1`b}UPh zG1v$Dm-}s0>ZwVx9d<+g;f~=#b{3qgNRzwdHS7EW0 z1JGQVB&%T~j6TjeRypSr9qVGRpJv}nXke@4EiJDv_Zt|drM((LQ~SU+c)RzXPm+Uw z>v*d&$(AQ%lHHqacfem}l2HQL`@fb+Hr|m*hQH(Wzh#mGcV?10}&ypDY*-^cfRGwR(|_aE##;&tp3i0=cF$IHj( z_TLg)XhM7!zmLyjyB3-d@8h>46KGCszvc7=Z&P z`Dckb_u&rvuYB(Kd7AF|nEM;w4Y~L0{C?}q@#BtpJ)|r{3*BoScAtiN>&brmWFPvA zdYq}eC%=!+7G#ee{5HN> zFnASvCrNx7zgjf$?c{Ib_i^s=e;3{TYW&FK<?W@%{K;T;GCZ>haH+Gizkzr0&(N{^m6sTbtLNbmquO|9ta~J6`v~+h6i$ z-`c)o|0`Z{$Ie@Bf5WcZ-#B{GBL+Kfy!xtV^g8$Ke(sr-o}sj&fydL+_rGe*4WIxa0MIe*3mp z-2U#@PV9Ku#?5Qm*KAsUC7Hw65f=w;JA4p08!$^?bHRCncB&M-W9426H%jG@pKiFS z+q)uu*X;Dq*wEYDI^&8fS{qt9v<&Us`YShWzWSQZ=863`w>DkA=E`d}w0l?kMP>YQ zH(%Yla?=&9&Fina@?Paww|Vo%O=q5QM!$9Cb?bAjjq6Wux7K&BIlXh$hBLf7WAM8( zp0$4C_$x=~O`hhEBephfT(iC#$n>JsyVg0jvYvRnB z%!ZAFi$ICct~P>7wT)KuDbfVsILvrnAqpq^;dSUx^~UA>(;b4P!ZqIo4CRERN0|> z`01ci=oVYNE>s(h!6kVghsC=z`S#lN-9d*bAi|jZY+Y|q>1U2qYmMkF&mFIhO&eRC z-kL#SqiDaR%RKI!$2|F}tFE}@lB;{0uDa&xPEWTQ{O*!Vp1rPh<+Z)_r*FLSvO_BV z^4{QTH(j2)`f=B6zH(jRnrB^6ylzWl!|$HoKPO+Rom)M>_GmPLMf^F%^7)l|`O&3E zVGn5(CJu5%YyCMJ*FR}c$5FTqt@T%)G-nng>Tkjh{^=>nV~y*7#Q*t6{>F;T<1<%h zUYq$)<{O#m+0(MCve#r^m%SzX&)Jz%PM`9mDO;wzamq)h+%e^-sfDS(JM}qJcTD}n z)bC84H?2JFsneb}?XA=HO#A+{qo>!VUo!pr>2II@ndt|nFP?Gcj5RY}IOCl&J~!jX zGlqvAIn*AyVd(!1ePQTdhmM=soY|ZC(wXm>`K6gZn|b1_b7!ra^@>@4G3zU{{(aW6 z+2_x`eD=24@0dK>DebnxwzIIf0&co(BZq763{PCO*&e=C-+T4fF zy=d;{xi`-J@Z4|C9h#S$_r!VE&U?eWkIehlyxH^f^DmzN?D=n+|MB_XoU*j5{?uPv5YyWaQlS*~$KpahuZqaQOY<;NlaO zJZi~hOJ2U@y-WUX$(>6^jydm`XB_j&WB%V`zIx1m9J73Q$uk+H+J0Z$IUwa zjN_kt{B_6w$?+dMe*f`vPAH!6loNjcgg2k?*C%}Ug!v~{PJG&l&p+``PyE!0|8nBO zrS+wkF8zb0?^yabOMkd@$;eqFYe!x*@@FHTANju{$1eNrWu0YPm%V%0-!A*TtC;#Qix1F4va!C6u z79QO8Su9Mn&)uK9yPrnC$v?>D%8!@pP=7NyQp3NUW$D#jz$yYlr$|Sd5ow=8tersmjPTLOb zwC%u7+Yaos?Z8gk4(#+>9cQ9l4#t<_{f!&<9uEfl74C1pdyPYX#q zO-g5q>0~*L7aUO`lavQJ1hJ2l-e2{{Ig-^Whn+$t(FUogv-%&_0WNm>PI6%^AdNUNZmRzX??^|T7oD(I$FkXAttGv8Xin(XhRb zuJNwp=+jPU8;>rdK@4MX5JqU-PHG%)q}_BN1afH{-&gB+o}ov99qXa zCbW)2=Xgu=-de|@bG%}ubHkxp$Gh*NcRZgU_N3Hl7@+%NT;m(k*Vx%zsAYlQu<_77?QzS0&ZHIuiz)3M*}IJ=bVy}tVd z4dzd)i_dyLp{Km_UmX7@uJ2Ce&t{TsAwK-_GDOfqs+CRt6RYyKws%W$Yh^S&|K%%7mu{CB^o*L*9@=8^CE4K%-( zN%lhM@rI7`d^*mfup1^do%fxb9ccKDK2M{$KAWt1K{gqAAx&S%zbKn*g`F^|`#k)U zY%)A!O0w(3DeljdB>(a$$-1{pNk%^~CE5M+Q2m{nt4 zUV6@x-$fG|nLyKa@_l?C`q1&a_-s-mIy9e0CJ^7%A@+^W1BbrOdmmcR@%zbrW1q>- zW4jjqzviwsw5>7Wxy2jLm)_s^J(q$ImZgw+Fszp&`GML359ZnY% zrHZgC6qlma4qI2oik%>VW;5c+2@{UNE2xjCE0mg$@z{tg0{qU<(Rlo+lq!h?74$f_?@% zeb6CEcrXB=Hm#QjS9A_&K**LP4WYFt!W*9=gGLUhI&2>$2 zq_LOIn=pvBllPkR+aa_jVFFUDF#V9PEF@@&&5eB$idotd{82ooF|R`~EFOVGAavHm zpPgNePBr$3+BJgulfN6j5aT86rMQuD5I)h*ufg7NPpLj)i*NhnIC!&X^K1venN9rH zH_HKNnE$FAmO~Kgh461PZik>5hSX!Mj;+X=7`IhuuH-gZs?5+Gz)q^6Z}V=eu&GBc z4yl8782%88s0UJ?=4f4NMbV~llZ87f$GA;vnbx#|*`^&WhYj`2^(Y!!i`st7)?tM7 z0&4V z>H*GbL_;l{L5)CdxP)&HdVg-;TFrXaeQPT5eQPrBTO##1c)hH1p*%Ok%LCMn28*wk(lixm+cMSv7C3r~hh7Fl_%)Qhi{nv4u>U{Or|Nm9>SKT9nEIbNB#c-8y*(KAbv(!QylNhV%@$u9UG)5Fzo#$n zBYLX%d}Uvd-((N?#rGmz`2Gfj8~uU7uD)JI<%#dd;UdPWx3e8=7j7`NJN!%WDv|Dt z@QQ`$5HDT9_KiNzMfq1FeD>LAZ>9&7uBLd6ThHE1*GM;c51t)hDRu-@!H%*_s)C~9 zs(4U5+^NZCx25S0T@!9yUKF7iOzK&`u)k~dKzDSvrf2=ie%~LBl(GA}uJAXHrm1xY zd^_*2>gw^sdsRdIsD+a27?&tThTe^=m1xkY|K$R?$s{&QyoZ+B&gkLVT zBDFXCn0?`2x{p_;4lmqv(+6>ft`TgdG^GdKZ(>Xx+#n2GIh611l!i8T%bSO-o@*8d z3q{4aR9|dd=Bbt}o0hCD)=c#>Mxo{Gcs%=9pjr0cL8y~VfE zHE>MMV)k`~eEZqvPiA?@5I89(-?I9jl&nTVY#ada+nxITxec zN47II{CA8sqwMEIF^e$ojLjk2ny!7rjFt+Q?;`brHFnt^*;S9X$v)Sxa;xNhw zIMkQ(7h2!OF4tcgaa6V2Y4`T}-J{n%>o4ic$5)JHdb`HlsLA(j@FS?{%j$-Z;lg!( zZ@&1Q)uY#!4h`n}df4;fPqfQ=WY^eO%rpE;--CL={d9qDfM>%Hd>e+~-COB?WfWFL z_rb^E^BR^4!wk!WVQm!FMb9aJ@KyEBirp2RC;(%H@Gsp5?myg`9qjM!%69;-f$W0p zlZCfE^T=IK6h8b!;oc_-JDw=q^UNc+JoCtXPZqX6S$NNrg?Btzc;_>ZY*7Ljj-^pYuS?T(4PI^cdVM|F1^Iy?e|~l#yt=gb8u2E4WF>uH zHE@l1o%sDZiO`{RR3C^>$Iap2Qy5bIOCVQ|j<$SyD`SW8QaXy#m9gz@#p^g4wGlc- z&<5x@9EDZI%hA6nR#~8nI31nSLGLZf>9>U`>3SkL?ez6+IMz&9EnTm&2jhEH`OWD9@52y$ABN!j zFof(5L-2hVQa;4jhpUfP_lMXGhkxmQlAJ{ROtY{%dmwuR+G=`RX6}v#ioY1JcZzuU zAB=4SNU{8)e3VwPRTBCK{Ec1yrdBzYR};!X5H9O7^FGv4CMK}DGO?rD2-Wr9s6?TUAZ!cb7AM>+b$13W7$0prE_YuA= z3d8ic4$~tH(<2Pi{|;~ae&&yvb8B<8e(gJIAFTa9wSTUCZQZ4HoppEDeZKC;b$_Xw zQNOr;L;d#pFVz2}{@ME1H7srDZFq0Po`#<{q$kapbmgQQC;i=|hbJAFR5f|tEXYM(3Z`h(JKE4MmutuUvqHC~py0an0D1H2VM6L^)99<{L zA4F)%rl)3`Q<2dfh_XyXKcFPYdcX}o+yKJLObo!vBq0T#3tBnU9jYmVoZTF89Gh@rw#Z$_P~ac z9LJuXgXdkt3y1Q3D~Ft(u22S*#%Ro=N76TxU)I$-l=qM6y*N(OsGS(!85>Tc=c~rr znx!$OmC2{lXLNp6SwCpp^Rj+X`Sa<-SI|pMEHpyLn4)1^dXDx3b0eK2c?CIl8HLo3 zq7b|nhLCe%SjAFd2sszVtBFF$x$u5%6t6Cd2SYc!-w@rO6or$~H>3QgL}6oepUQ)F zR=vAnZ^IEVILzlzlSTI%Dp+Cq_USvP?>}?RnLE+@{mjPn{?_>UuC|uh!?X9! zo<664&i=NMIs4`uowFi)By4ZUcT&sTZF6^}_tor~JAK~pyuI_*WW?}$L5bQ_JK-`?LqL% z82fQ#e$jqJ4c1@xsd-`UlrPw{ynkBq=J53Ve5W4`ZInL>uU`X$6C*!l(3SU-W1L}kB|k*^X`9eb7$8wRakMFU^yvn>?L5Z5CWQ7S$D=Z8vq7d>cyr%v%J%{|dl|sm`Fw8_DnPNVrsC*?uKETrsrsX;dsao&i!mS9)fEy>SN(h z3Oz@@>CQ8UOYMc{jKAZ|U8C{K?LvG{ioS^s%Ad-2v~mGk%kJ>$8t;Ee_K&O3UT3k( zf#Unvm)I{@MXDvWEH#k&KaA5Dt=e1luT@j4jq0`4w^Vr)O*sc#e;S2u2Hysz`oG~Lv6e-r9z6WiO}TwMH};-asAzz0U!Bc;H8(5wAB$A>pLew6KKZlr z3%@+OF*o8@6sBLXfa$%dksrM+oBQ|A&MpYI%+4LUcO>Afq;-a8WMfjIq7qYTR58lMysGcYarYMPmVmXSZOO|IV8YQ9lC`(+N@EYyD zLY$tP-VaGY&q;1{cdhG$^%=?yBumkKw2R?aTe~Rz?g92r_A&Mq_9ONT<{@UJ7Nl0D zx>L8M{yz1^)V|cysdW0>w3=R)4$?QLcced;{-^Yl>A$4Uf`nL7aYM!CiodP+bj7zS zeqQm%iYb*Xl?yAcs~oJntMcQOU#z9`Y@9AFO+*?z?pd z>;9|m?0T_&X}wdwwf=qe57d9V{+IO&QR!ryK&G?Rp!b6=0MQi5&p|ZE2~&r?7qkvU zX6QK}G7{bn`e)EV(7Qoj06hh|4YUMggJ8O{A<)wx3$zq;J?LD}*FkeZzXVC3M?s$h zy%Y2gpesOM0euog?e$@h0D27cX;3}r7ElWGThKiqnq>MCXc6dI&_y6poacki0u6$y zKtBa71AP(nk02j(H|PNn+8J98`W5It(37B9pdWz#1w>8%JD`U^J)jw&i$MnHBcMA# z9|t`SdNZg7v;lM@=tH0m&__Wo=mVhlfL4Ou2s#_|2GE6|uYn!}X`nZO?gjOM=71)F z?gaf1)CIZ}^nK7bL39v5lipV#Fyd=`W4ix5LaX?E5g;r?<8$H7l0Q*CeLV)bmHSozpP#duwLlr{9`ZxbE|Fa(B%-pM89az!raV zX5l-t%#k;}zp1eIp-k?veXlLFeWG&Y8$YSeePU4~`@);279RQXc`WcQWVcr-xx-&r zko(oW({o!seO~TrFFW$=4f6_Xe_X|W`;TW7-Zz-d-Fox9g0WgHENreSeE&!$m!Fpz zd1CA2+~zZDwra#zV>&2(|4L!m7yt)9;l8d>W*qzj&C`rIL}cH-*I%wRSnqSNU2X&MGop2$J`3jW718F)Cu*1G?DpB6 zvrizur|*By)3?0$IrG~#d&lfOCHaxvN=H<_vq#If?D9h&{_??lcK!OxU#3jy`9$U0 zKl||P3=CU(PIBg)Vs>$U=Zxle)zZtCj>)kYF)81cIp&-jO7cBpY`$lV=DT{VBTiC& zg*n^j?2ht7Rw;`2TyhQdnchF=@SOPjAk$M)Njb`|<9y=~`KNMUERJ;0`zXH~vStg( z{dhiM|2&#cFfpIN{(UM2ilcI+z;-IVimrX6>yHLZ=o{6CzogM-2HPi^NxLgdH$BA7=~+mf-8xN zpt`;<*p_Arrfy53Y8n>Q`SJ2^nP<-HEXlvT9W)C9ePXkY6MLXpS`aZPeeXp0ZJW1a z-kvBw;!ieoPKa2HPsnu9_nt`mI65zL;S*8)m-8LH9~2&xfBAEIzhpRJiRn4&FTLi% zs?qpMFmAXViAs;7Q;JvM;qcfc7sL2H=SKH;&>#bjeNk9byd3>Yek#PPak}^cl>>Yl z`aAq7@^?TO`a2*D{T&d7{tgI3e+Pu2zr&vxn_ZgzXp|mdm>yx69$}asVVE9anEoh6 zTqymb(A8*7g_voadU$2#t!pxCvUGnj?&BRSM>U4=(7h;V`!0mHG1iHD?YI}EA2aV+ zK2I3e>o>1?aMYe-)wi5U7)0sQ`{o~*zXkxs`AsxWdSrfao|M{oj3dP_Z})hg$NS?i zt?_!QC$zWJVRg^6-l(3zPO}_Ub@f121X0 z#+mUo<=0X{^GB=0=jH7k{F^vW-Wf`PV!q&)l<0VRNxtd+wopvP@2x4o#wtBxzB?Wt z;#vGS%!l;cs2x#sx=2wzBzJa3awdL<8QGtaIAMiZ#V2uwRZ+yl(R_jlFzG0kx_C~1 zY2F}7?jVm>X+NqsZb!J6`z7w4U&M~aP3b82OT49cn>}B@MDjC37FAU5!2dexH@)2E z5zt=NQ27_ZudR~?peH!!IpKQ%!g$5ig@7j$gbA`8sS| zak}_^{ND0<-4?Zzq}Ep1o=Q zXp!NA?!pbdEcpb5&iCY7qsVS|-;hVeN5RVyjuvp?6-zI>w0+^0_3e&(wRGhhuh=}e zW>Npb%NI%ZMmfLw#!bD;2g=fpmida6%eJiO-aNc+;fm#kePh1ojlIKpN4xT>Mg5DG zUm;ynT7Fa~%D)Z}m*qCmCq%9@e(SMR(Pvy-mb&CSu3WWz$>p6_UAB7osx9lbT({)v z?yD~A?7VjU2I;ybi=^u=yRK*Xo37fhe93BY`Kr}r*^GYDn|h?{mMs!Duk4Yox@OTe z%9}P^z1|bGMb47`<;ym7YpYgWe$}$oR{J%puDQSRR%8L2QfkTiid{JF6)8 zVmbQfNDh|!BTzq}^f;bB0%Y~8ay}6FgnAt+Yb*I6EKclx98g+&HKB3ax5fejienL{LM&n z_`xq8{_1-V-uCfde`P1eZ?9aqv>D5Mn{nEVm8po@Lu*uyA2GBB4sO5usZZ{PILmJ6 z>PaXV$>W|r6rr<^q}a(8?h0glVx=1sv#fYt-;iTN*H45TJTpnUIz9%Ude;>-Nhn%F z6~0k9DJ(AwUvy#xE{i*+$S@kI-qGIQ!rS|+r@i-}%~QQY&xQTxms|hsukiyrTHn+# zuR)c#-udhF4)G6$Y}(8FKiz#;TiDTU_t=o|U48v_7cEuZnmwjR(36p51Is|M5Ojel znqpfPeX3%9P4^p;83dm03h1g}9^Y~?f2g{$;32Bwmkg;`}$*F8a&ESgC$;6aKRUqKaR*TEFAp-BdF4bwMWB-H|47F5h$ z2#zkxg5?>KZTp&MTPhPB*^@Qd7XpjsG|_hwEY-FI2X6~p$JOy#rhBIBin@X6PK6>V zn&5bbCzx^ocU8ymbkAd$l2%+>4+KnpW3*7gb6eLP%$}fpilp0`t1(fQJ;l@%L6SV= zBnQ5L_u7IB2UyJ&oj^1!W}C96AfBM=F1|^(Y{7DDO$cehk1{}ebR1;j_0RmYDqOxj9u42k^pfE8|WOQe30gfNQ z!LdMA?@d=2(_$*(i?VslW-GgZV0xK2}jQ-B%PHuagwy>j$D>SO)PE z=Cd&EW}_-iWayi^@5#&#TtZkzaV^Zm>4;?7mMbW(@1i84X`85E)Denj3x)_^6je9D zB_1Jai>70Go+@fCQ&rTnr8q($NqC*&+k&mw$W9kkQ4cWBrRdC;Wq3LRaY^;SnKoh~ z6j;~Pw}bY1h|j|~{WnV8QP9Lp6E zQwcm@gp-8=2r#Z-TD~UO4$`AsXf3)Y3TPmXWjmO-M3Ztnl-vjcATFcenuZUvd^e1w zS*9pEa==hGVqhtrpj(;`F3|B};({QMEKRlo%Tz>W2KWxtGV0bqB!^0etWZQmQ3G2? zdt^SmKYUpSE9o>!Zx~^O=D7{q`%NQNvi*NCiuIGQ<}N-X#hIYywO=2T5$ zq83=9;bRuoLQHUtg`5mofJ+L7RAfyB?ksRL$uc#x7C-QSxS>!p2Tm%afoMsZC8FSt zs|6lr4h)DHMAGR^I$AwyRsvL}?uZPIEV}QzD3k2aClXUS5QkJ}A{=B;A0lvq^NgZN z0@xmiO8~>vX(GaO3>U28%J7v(WoxDh(S+843Rb{WU|5NyhT+*Jq=+vw%XdUa$DFQj zQj_%2WK3czGzs4e;yHEj_E55Pq#29qFF(3UYZt6G=_HhfgDXFBPf>T>z?chl1y64)&Z*~ zE0`2V^Tf*>OZ1or2_pugCs+Z=aZ5y#u)*@sN3QPanr!GUgLH>bV0W*1BybnTuh3i5jw7ofj+d%33rr|S9|A=-Nr=gc3q=d5fHo;< zs-;2z11Gc(Qlo+)>xe{h-n7BP;C(bQS+$+OVvY{kOkmq+PGDaVIZ4D`@Yb-Pb`^MW zF?eU8F_IWCNRI0USjk7}LZ}^Zwrpz*?5U{|wSm9|Kboi!TQgAKnq|NXLNg@QXOKmf zWC1-<$8$(gg4II7VJ?_~10|-K5QmVeGGr!N16r`}nNay?JSNT91>gY{5J;|x!hoM_ z*8@%gYT0z)ix5EGsjh~GBe_fxbxjc=-7zOmIKd<^!7z{oj%P~9PJs{;4QL__g+MxR z0X1_JToObR4pJ%#2G(Sf>|kUA&0NJxQNaNs!Hf}n_^zpv3B9Q)%oM@l9()=l7Z8Dc z(GVo)GqgYp9&M0Xq6{;{5n*en0-lgyh03>Fv^z{wI$#ygbUo-h5A}>)l;9m%lq#h1 zT@~#PA0I#-!2yH8fZGNhY9Nr%1`J<;MZi>~^CZ|ZfeWbw-J+5uhD+U0 zEzN=1q!{3FTQ@xLBehs66Pl@LL*LmR7|gKYf?_gR^&WNEpM=qsWo z!?uthdL*bK(-(9I7qmNKUuaUWr|F_Zup(83LG{A^0OE#53I^I)Koj!>phkL2l-$6D zd;%i{Fh*^(DaZm79x>3%C>SgO&43vynao2D3YZ5>N0A(Sr!7Ky2Cg5t7_c&}z()o9 za7%>ki%kVkCRHJm0QOIyTW|p~U}@Sa8baVfF@hf<9S{lP(Du=;OxrV1Y?y?$#L!lt zCLC4JMKVTR2OkCr32p9Lk`La5KPw=U<5)-*`%l;UmQ(`q*emv%4kuJYk>r%` z1sU+J^5G{6)$WQOR+acNv_Fhzu!>I>1lo-S7bx@w;Lj)rwrZF*gWh)}ss|Xu_&NuT z*CNhCl?0+6XfO&P4%I-1oCiMz0jdQyF(e&KA0Q&ZS_5OkRzYKjxk(L8CL>FOwl_71 zeIsz;|7xQ*1UF+VfJ~QQQkV)E$bn6!GK?ez9UjsZ=oi_{giN$0@S_gRPX-P2orHH9 zrUW$%dkQ|aOf%5|k;%gf9_LJW|T8Tww}Czuf;m{L}#FQMwNSPeshQ74Et$pV)It7`i&I558jJ4O@@ z1&G5s4gyEeA+<@H<4rOQGt+|EV5@MS6IF&PgYAey*r+Rj8IUsY5eYALU}~c9225=W zY8};TqVtKbgIn1?=dWZ#B6 zI+Te?I2k6IIr0-wyO7YZ!qHQJ3F%9+1e3uuQGsdz1q0s_Scl+0R3u6Sf#FD+fsPr} zD~blDLqoCA;e&G^N`|UO(V)#`_`t(P7Gwv#5p>Os2?kUnFqf*o5A>p+y$%I zs)8m({alBb24*2zienfK_Sth4#eyaS&I+k2G9%zotq2-YqmKps1q;LSZAf1j&<xwP{C&@#Xf)up~BT6+O@f{gj1Q@}O#y|#;nJO$BFc5X2$#^i) zXGBfQFlbcRK?-_7B7_+F<|eqBY(^NP3Oe|xCIC$K!Dr^cU_*#u-~>WUp`ifegz7XPoje(KEyJ4H0CphMhE41X84u$DDqXil5!zmJ zT*&ddaDOivuYoVZ7Fxqm+*%==U8|blI^1F^ntQ`OmCqUfx$@)B!>ue5@#gq&e6ZCC z=MNw-kv!t`v{zlX^jBw??nFbM?u@C z;Tn5q?7RTf%oyzf)%-d7I*-PJoa635wfo=ey2x0nmg#Dl#s0FkG&nRCyr?@%LFiwvEn|7Q}KZ~&f9F}Rn z=-qHI${=jSHyzHR{J>?~kBalioI3W8vMXK{&*+NIis7+yI-_`{Pm1@7`7Xvf zCHk)2z?s%tY{#Yw=34BT1v(#P!FyQ-_(z{i*+lbZ4f76t@!^B-*x7u{ylOu9?z<1Y zYuB$I#{G{R+P3}RJMLpA6AAq%QE6Uw`BU$^52+7-@^&nk8*1&G`PiY| z|MTG8yAOW$E<|PYKu&s)m_kWPk(ia5lp--JEh$A}R(ev3#H@^@6p2}xNhuPuvXW9H zWhIG8DU!01k}4`OD=Dd>lCw&xsN}4YDoRSsDygER#H^AkN=nQssiLIBtddeZj~12m zxgCKXpqEN&9RiHUrb#EHpVlE_y{U!GjGNng$@YMcm9Zs&=T;T2y|Nnf6< z?vhwExpAJ3cO*3fE&#?igGABCD{7q8#~~?)B-xTlk_<1n@+z*^qkdrkzu%qjq&=e1 zj{t22(K@fyp!b8&{W~T5 zGe8%E{uN|^J_5P}^l{MRAaw6e`<{8xctG*tZGq*o?mS z-53}6TYT_t%6GKC-;7s}>F@8y^|t7qh;S#$NqzhRI=?rh#`gbr;CVl&v|mqs`cBYM zq@!~a8ysZNm*0i+6%8mq@@mAI°5KQ-E)Z@vJ}iB9GyjZe77{r3^zNd5Pn8-Q^) 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 zc_wC+=%bvN75A?%_fh8l_2aock*T?V{W$hXYMiIVZJSrrxcW$9QRzjkDDGchUQvlF zy7&T{@d7z%IlwDrferVsA6Mteiw={RK}ymFC%HC+eTDsqJrn*e3hhVFJ!QG4EccY< zp0eCimV3%_Pg(9M%ROber!4oB<({(KQ}*ONWqDtU_oaAWiua{nguc|V@f_}Y&t31i z>pge9=dSnM^`5)lbJu(Bde2?&x$8Z5z2~m?-1VNj-gDP`?t1^ix!#|;k%vmIvboCU zDx0fpuClqx<|_N;pt4V$^5;462@F1g!6z{I1O}hL;1d{p0^?;rfx*~t_-ohv4@ZC_ zz!BgGa0EC490861M}Q;15#R`L1ULd50geDifFr;W;0SO8I0762jsQo1Bft^h2yg^A z0vrL307rl$z!BgGa0EC490861M}Q;15#R`L1ULd50geDifFr;W;0SO8I0762jsQo1 zBft^h2yg^A0vrL307rl$z!BgGa0EC490861M}Q;15#R`L1ULd50geDifFr;W;0SO8 zI0762jsQo1Bft^h2yg^A0vrL307rl$z!BgGa0EC490861M}Q;15#R`L1ULd50geDi zfFr;W;0SO8I0762jsQo1Bft^h2yg^A0vrL307rl$z!BgGa0EC490861M}Q;15#R`L z1ULd50geDifFr;W;0SO8I0762jsQo1Bft^h2yg^A0vrL307rl$z!BgGa0EC490861 zM}Q;15#R`L1ULd50geDifFr;W;0SO8I0762jsQo1Bft^h2yg^A0vrL307rl$z!BgG ka0EC490861M}Q;15#R`L1ULd50geDifFr;Wcm*Ty-|Dl5p#T5? diff --git a/resources/scripts/db/software.realm.lock b/resources/scripts/db/software.realm.lock index 1de12a3e95bebd77ca101c1030374ff97d85e571..651e45516dc925d0bb042cd8f81429b9cd05d1bf 100644 GIT binary patch literal 1416 zcmZQ%vk(Lag2}*$#Ky*i$-`(3pdt{)1{8pTEKm%RX9E#n0MYHi z3*j+AMPU@kjSv7)_d)=f2a{u9K)4&CPJ$o1Ixe^th`IxU*wrDz1)|O%9ik51eM)HN iWjJFuPX$fg3lHq-w9wQYNP-KDGDbsSGz5l82mk<1-v^KY diff --git a/src/define/Tools/file.ts b/src/define/Tools/file.ts index 133541e..ffd7454 100644 --- a/src/define/Tools/file.ts +++ b/src/define/Tools/file.ts @@ -236,3 +236,20 @@ export async function GetFileSize(filePath: string): Promise { throw error } } + +/** + * 获取文件夹下的所有子文件夹的完整路径 + * @param folderPath 文件夹的路径 + * @returns 返回子文件夹的完整路径数组 + */ +export async function GetSubdirectories(folderPath: string): Promise { + try { + const files = await fs.promises.readdir(folderPath, { withFileTypes: true }); + const directories = files + .filter((fileStat) => fileStat.isDirectory()) + .map((fileStat) => path.join(folderPath, fileStat.name)); + return directories; + } catch (error) { + throw new Error(error); + } +} diff --git a/src/define/db/model/SoftWare/software.ts b/src/define/db/model/SoftWare/software.ts index e6f66df..358cfae 100644 --- a/src/define/db/model/SoftWare/software.ts +++ b/src/define/db/model/SoftWare/software.ts @@ -13,6 +13,7 @@ export class SoftwareModel extends Realm.Object { aiSetting: string | null // AI相关的配置的json字符串 watermarkSetting: string | null // 水印相关的配置的json字符串 translationSetting: string | null // 翻译相关的配置的json字符串 + subtitleSetting: string | null // 字幕相关的配置的json字符串 static schema: ObjectSchema = { name: 'Software', @@ -27,7 +28,8 @@ export class SoftwareModel extends Realm.Object { writeSetting: 'string?', aiSetting: 'string?', watermarkSetting: 'string?', - translationSetting: 'string?' + translationSetting: 'string?', + subtitleSetting: "string?" }, // 主键为_id primaryKey: 'id' diff --git a/src/define/db/service/Book/bookBackTaskListService.ts b/src/define/db/service/Book/bookBackTaskListService.ts index feaa05d..76ed356 100644 --- a/src/define/db/service/Book/bookBackTaskListService.ts +++ b/src/define/db/service/Book/bookBackTaskListService.ts @@ -135,13 +135,13 @@ export class BookBackTaskListService extends BaseRealmService { * 新增一个小说相关的后台任务队列 * @param bookBackTask 要添加的小说数据 */ - async AddBookBackTask( + AddBookBackTask( bookId: string, taskType: BookBackTaskType, executeType = TaskExecuteType.AUTO, bookTaskId = null, bookTaskDetailId = null - ) { + ): GeneralResponse.SuccessItem | GeneralResponse.ErrorItem { try { // 通过bookid获取book信息 let book = this.realm.objectForPrimaryKey('Book', bookId) diff --git a/src/define/db/service/Book/bookService.ts b/src/define/db/service/Book/bookService.ts index 58be02a..5ab9172 100644 --- a/src/define/db/service/Book/bookService.ts +++ b/src/define/db/service/Book/bookService.ts @@ -11,6 +11,7 @@ import { isEmpty } from 'lodash' import { FfmpegOptions } from '../../../../main/Service/ffmpegOptions.js' import { version } from '../../../../../package.json' import { Book } from '../../../../model/book.js' +import { GeneralResponse } from '../../../../model/generalResponse.js' export class BookService extends BaseRealmService { static instance: BookService | null = null @@ -37,7 +38,7 @@ export class BookService extends BaseRealmService { * 获取小说信息,没有找到返回null * @param bookId */ - GetBookDataById(bookId): Book.SelectBook | null { + GetBookDataById(bookId: string): Book.SelectBook | null { try { if (isEmpty(bookId)) { throw new Error('获取小说信息失败,缺少小说ID') @@ -275,7 +276,7 @@ export class BookService extends BaseRealmService { * @param bookId 小说的ID * @param bookData 要修改的小说数据 */ - async UpdateBookData(bookId: string, bookData: Book.SelectBook) { + UpdateBookData(bookId: string, bookData: Book.SelectBook): Book.SelectBook { try { if (bookId == null) { throw new Error('修改小说数据失败,缺少小说ID') @@ -303,8 +304,27 @@ export class BookService extends BaseRealmService { if (bookRes == null) { throw new Error('获取修改后的小说数据失败,小说ID对应的数据不存在') } + // return successMessage(bookRes, '修改小说数据成功', 'ReverseBook_UpdateBookData') + return bookRes; + } catch (error) { + throw error + } + } - return successMessage(bookRes, '修改小说数据成功', 'ReverseBook_UpdateBookData') + /** + * 删除指定的小说任务 + * @param bookId 需要删除的小说的ID + */ + DeleteBookData(bookId: string): void { + try { + this.transaction(() => { + let book = this.realm.objectForPrimaryKey('Book', bookId); + if (book == null) { + throw new Error('未找到对应的小说') + } + // 删除对应的小说 + this.realm.delete(book) + }) } catch (error) { throw error } diff --git a/src/define/db/service/Book/bookTaskDetailService.ts b/src/define/db/service/Book/bookTaskDetailService.ts index e6cd94f..b9e9ae6 100644 --- a/src/define/db/service/Book/bookTaskDetailService.ts +++ b/src/define/db/service/Book/bookTaskDetailService.ts @@ -72,7 +72,7 @@ export class BookTaskDetailService extends BaseRealmService { return JoinPath(define.project_path, subImage) }), characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : null, - subValue: item.subValue, + subValue: isEmpty(item.subValue) ? null : JSON.parse(item.subValue), reversePrompt: item.reversePrompt.map((reversePrompt) => { return { ...reversePrompt @@ -101,7 +101,6 @@ export class BookTaskDetailService extends BaseRealmService { if (bookTaskDetailId == null) { throw new Error('获取小说任务详细信息失败,缺少ID') } - let bookTaskDetails = this.GetBookTaskData({ id: bookTaskDetailId }) if (bookTaskDetails.data.length <= 0) { return null; @@ -165,7 +164,7 @@ export class BookTaskDetailService extends BaseRealmService { * @param bookTaskDetailId * @param updateData */ - UpdateBookTaskDetail(bookTaskDetailId: string, updateData: Book.SelectBookTaskDetail) { + UpdateBookTaskDetail(bookTaskDetailId: string, updateData: Book.SelectBookTaskDetail): Book.SelectBookTaskDetail { try { this.transaction(() => { let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId) @@ -178,18 +177,21 @@ export class BookTaskDetailService extends BaseRealmService { } bookTaskDetail.updateTime = new Date() }) - return successMessage( - null, - '修改小说任务详细信息成功', - 'BookTaskDetailService_UpdateBookTaskDetail' - ) + let res = this.GetBookTaskDetailDataById(bookTaskDetailId) + return res; } catch (error) { throw error } } + /** + * 更新指定ID的反推提示词数据 + * @param bookTaskDetailId + * @param mjMessage + */ UpdateBookTaskDetailMjMessage(bookTaskDetailId: string, mjMessage: Book.MJMessage): void { try { + console.log('UpdateBookTaskDetailMjMessage', bookTaskDetailId, mjMessage) this.transaction(() => { let mjMessageRes = this.realm.objectForPrimaryKey('MJMessage', bookTaskDetailId) let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId) @@ -276,12 +278,20 @@ export class BookTaskDetailService extends BaseRealmService { * @param bookTaskDetailId 小说分镜的ID */ DeleteBookTaskDetailReversePromptById(bookTaskDetailId: string): void { - let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId); - if (bookTaskDetail == null) { - throw new Error('删除小说任务详细信息的反推提示词失败,未找到对应的分镜信息') - } this.transaction(() => { - this.realm.delete(bookTaskDetail.reversePrompt) + let bookTaskDetails = this.realm.objects('BookTaskDetail').filtered('id = $0', bookTaskDetailId); + if (bookTaskDetails.length <= 0) { + throw new Error('删除小说任务详细信息的反推提示词失败,未找到对应的分镜信息') + } + let bookTaskDetail = bookTaskDetails[0]; + + bookTaskDetail.gptPrompt = undefined; + // 删除所有的反推提示词 + if (bookTaskDetail.reversePrompt) { + bookTaskDetail.reversePrompt.forEach(item => { + this.realm.delete(item) + }) + } }) } } diff --git a/src/define/db/service/Book/bookTaskService.ts b/src/define/db/service/Book/bookTaskService.ts index badfdf1..1f93f90 100644 --- a/src/define/db/service/Book/bookTaskService.ts +++ b/src/define/db/service/Book/bookTaskService.ts @@ -132,7 +132,7 @@ export class BookTaskService extends BaseRealmService { * @param bookTaskId 小说批次任务Id * @param status 目标状态 */ - UpdateBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg: string | null = null) { + UpdateBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg: string | null = null): GeneralResponse.SuccessItem { try { this.transaction(() => { // 修改对应小说批次任务的状态 diff --git a/src/define/db/service/SoftWare/softwareBasic.ts b/src/define/db/service/SoftWare/softwareBasic.ts index 3e80d51..0ffe2e2 100644 --- a/src/define/db/service/SoftWare/softwareBasic.ts +++ b/src/define/db/service/SoftWare/softwareBasic.ts @@ -147,6 +147,14 @@ const migration = (oldRealm: Realm, newRealm: Realm) => { } }) } + if (oldRealm.schemaVersion < 20) { + newRealm.write(() => { + const newSoftwares = newRealm.objects('Software') + for (let software of newSoftwares) { + software.subtitleSetting = null // 字幕的默认设置 + } + }) + } } export class BaseSoftWareService extends BaseService { @@ -185,7 +193,7 @@ export class BaseSoftWareService extends BaseService { MjSettingModel ], path: dbPath, - schemaVersion: 19, // 当前版本号 + schemaVersion: 21, // 当前版本号 migration: migration } // 判断当前全局是不是又当前这个 diff --git a/src/define/db/service/SoftWare/softwareService.ts b/src/define/db/service/SoftWare/softwareService.ts index bf1d19a..7e6d004 100644 --- a/src/define/db/service/SoftWare/softwareService.ts +++ b/src/define/db/service/SoftWare/softwareService.ts @@ -1,6 +1,7 @@ import Realm, { UpdateMode } from 'realm' import { successMessage } from '../../../../main/Public/generalTools' import { BaseSoftWareService } from './softwareBasic.js' +import { GeneralResponse } from '../../../../model/generalResponse' const { v4: uuidv4 } = require('uuid') export class SoftwareService extends BaseSoftWareService { @@ -24,7 +25,7 @@ export class SoftwareService extends BaseSoftWareService { } // 修改数据库中行中的某个属性数据 - UpdateSoftware(software) { + UpdateSoftware(software: SoftwareSettingModel.SoftwareSetting): GeneralResponse.SuccessItem { try { this.realm.write(() => { this.realm.create('Software', software, UpdateMode.Modified) @@ -41,7 +42,7 @@ export class SoftwareService extends BaseSoftWareService { * @param software 软件配置信息 * @returns */ - AddSfotware(software) { + AddSfotware(software: SoftwareSettingModel.SoftwareSetting): GeneralResponse.SuccessItem { try { software.id = uuidv4() this.realm.write(() => { @@ -56,7 +57,7 @@ export class SoftwareService extends BaseSoftWareService { /** * 或软件基础配置信息 */ - GetSoftwareData() { + GetSoftwareData(): GeneralResponse.SuccessItem { try { let softwares = this.realm.objects('Software') diff --git a/src/define/define_string.ts b/src/define/define_string.ts index 376e3e7..1d84901 100644 --- a/src/define/define_string.ts +++ b/src/define/define_string.ts @@ -123,7 +123,8 @@ export const DEFINE_STRING = { GPT: { INIT_SERVER_GPT_OPTIONS: 'INIT_SERVER_GPT_OPTIONS', GET_AI_SETTING: 'GET_AI_SETTING', - SAVE_AI_SETTING: 'SAVE_AI_SETTING' + SAVE_AI_SETTING: 'SAVE_AI_SETTING', + SYNC_GPT_KEY: "SYNC_GPT_KEY" }, QUEUE_BATCH: { @@ -153,7 +154,6 @@ export const DEFINE_STRING = { SD: { LOAD_SD_SERVICE_DATA: 'LOAD_SD_SERVICE_DATA', TXT2IMG: 'TXT2IMG', - SD_MERGE_PROMPT: "SD_MERGE_PROMPT" }, MJ: { SAVE_WORD_SRT: 'SAVE_WORD_SRT', @@ -175,7 +175,6 @@ export const DEFINE_STRING = { GET_MJ_IMAGE_ROBOT_MODEL: 'GET_MJ_IMAGE_ROBOT_MODEL', MACTH_USER_RETURN: 'MACTH_USER_RETURN', AUTO_MATCH_USER: 'AUTO_MATCH_USER', - MJ_MERGE_PROMPT: "MJ_MERGE_PROMPT", ADD_MJ_GENADD_MJ_GENERATE_IMAGE_TASK: "ADD_MJ_GENADD_MJ_GENERATE_IMAGE_TASK", MJ_IMAGE: "MJ_IMAGE" }, @@ -208,6 +207,7 @@ export const DEFINE_STRING = { BOOK: { MAIN_DATA_RETURN: 'MAIN_DATA_RETURN', // 监听任务返回 + REPLACE_VIDEO_CURRENT_FRAME: 'REPLACE_VIDEO_CURRENT_FRAME', GET_BOOK_TYPE: 'GET_BOOK_TYPE', ADD_OR_MODIFY_BOOK: 'ADD_OR_MODIFY_BOOK', GET_BOOK_DATA: 'GET_BOOK_DATA', @@ -232,6 +232,10 @@ export const DEFINE_STRING = { HD_IMAGE: "HD_IMAGE", USE_BOOK_VIDEO_DATA_TO_BOOK_TASK: "USE_BOOK_VIDEO_DATA_TO_BOOK_TASK", ADD_JIANYING_DRAFT: "ADD_JIANYING_DRAFT", + EXPORT_COPYWRITING: "EXPORT_COPYWRITING", + MERGE_PROMPT: "MERGE_PROMPT", + RESET_BOOK_DATA: "RESET_BOOK_DATA", + DELETE_BOOK_DATA: "DELETE_BOOK_DATA", COMPUTE_STORYBOARD: 'COMPUTE_STORYBOARD', @@ -291,7 +295,10 @@ export const DEFINE_STRING = { WRITE: { GET_WRITE_CONFIG: 'GET_WRITE_CONFIG', SAVE_WRITE_CONFIG: 'SAVE_WRITE_CONFIG', - ACTION_START: 'ACTION_START' + ACTION_START: 'ACTION_START', + GET_SUBTITLE_SETTING: "GET_SUBTITLE_SETTING", + RESET_SUBTITLE_SETTING: "RESET_SUBTITLE_SETTING", + SAVE_SUBTITLE_SETTING: "SAVE_SUBTITLE_SETTING", }, DB: { UPDATE_BOOK_TASK_DATA: "UPDATE_BOOK_TASK_DATA", diff --git a/src/define/enum/bookEnum.ts b/src/define/enum/bookEnum.ts index 6b1faca..9b7bf88 100644 --- a/src/define/enum/bookEnum.ts +++ b/src/define/enum/bookEnum.ts @@ -207,10 +207,6 @@ export enum TagDefineType { SCENE_MAIN = "scene_main", } -export enum MergeType { - BOOKTASK = 'bookTask', // 整个小说批次分镜合并 - BOOKTASKDETAIL = 'bookTaskDetail' // 单个分镜合并 -} export enum OperateBookType { BOOK = 'book', // 这个小说的所有批次 @@ -227,3 +223,14 @@ export enum CopyImageType { // 不包含图 NONE = 'none' } + +export enum PromptMergeType { + // mj 合并 + MJ_MERGE = 'mj_merge', + + // SD 合并 + SD_MERGE = 'sd_merge', + + // D3 合并 + D3_MERGE = 'd3_merge' +} diff --git a/src/define/enum/softwareEnum.ts b/src/define/enum/softwareEnum.ts index fd8e34c..ed0d2bd 100644 --- a/src/define/enum/softwareEnum.ts +++ b/src/define/enum/softwareEnum.ts @@ -60,7 +60,8 @@ export enum ResponseMessageType { GET_TEXT = 'getText', // 获取文案 REMOVE_WATERMARK = "REMOVE_WATERMARK",// 删除水印 MJ_REVERSE = 'MJ_REVERSE',// MJ反推,返回反推结果 - PROMPT_TRANSLATE = 'PROMPT_TRANSLATE',// 提示词翻译 + REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE',// 反推提示词翻译 + GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译 MJ_IMAGE = 'MJ_IMAGE',// MJ 生成图片 HD_IMAGE = 'HD_IMAGE',// HD 生成图片 } @@ -72,4 +73,11 @@ export enum LaiAPIType { HK_PROXY = "hk-proxy", // 备用站点 BAK_MAIN = 'bak-main' +} + +// 同步GPTkey的分类 +export enum SyncGptKeyType { + // 字幕设置 + SUBTITLE_SETTING = 'subtitle_setting', + } \ No newline at end of file diff --git a/src/define/enum/translate.ts b/src/define/enum/translate.ts index 14fefb9..07a78bf 100644 --- a/src/define/enum/translate.ts +++ b/src/define/enum/translate.ts @@ -2,7 +2,6 @@ export enum TranslateType { // 反推提示词翻译 REVERSE_PROMPT_TRANSLATE = 'reverse_prompt_translate', - // GPT提示词翻译 GPT_PROMPT_TRANSLATE = 'gpt_prompt_translate', } diff --git a/src/define/enum/waterMarkAndSubtitle.ts b/src/define/enum/waterMarkAndSubtitle.ts index d92b5aa..6cfa65f 100644 --- a/src/define/enum/waterMarkAndSubtitle.ts +++ b/src/define/enum/waterMarkAndSubtitle.ts @@ -27,4 +27,14 @@ export enum WaterMarkResponseDateType { export enum RemoveWatermarkType { LOCAL_LAMA = 'local_lama', IOPAINT = 'iopaint' +} + +// 获取字幕的方法类型 +export enum GetSubtitleType { + // 本地OCR + LOCAL_OCR = 'local_ocr', + // 本地Whisper + LOCAL_WHISPER = 'local_whisper', + // LAI WHISPER + LAI_WHISPER = 'lai_whisper', } \ No newline at end of file diff --git a/src/main/IPCEvent/bookIpc.js b/src/main/IPCEvent/bookIpc.ts similarity index 77% rename from src/main/IPCEvent/bookIpc.js rename to src/main/IPCEvent/bookIpc.ts index 30a3fda..9147d9d 100644 --- a/src/main/IPCEvent/bookIpc.js +++ b/src/main/IPCEvent/bookIpc.ts @@ -2,7 +2,7 @@ import { ipcMain } from 'electron' import { DEFINE_STRING } from '../../define/define_string' import { ReverseBook } from '../Service/Book/ReverseBook' import { BasicReverse } from '../Service/Book/basicReverse' -import { Subtitle } from '../Service/subtitle' +import { Subtitle } from '../Service/Subtitle/subtitle' import { BookBasic } from '../Service/Book/BooKBasic' import { MJOpt } from '../Service/MJ/mj' import { BookImage } from '../Service/Book/bookImage' @@ -10,6 +10,8 @@ import { ImageStyle } from '../Service/Book/imageStyle' import { BookTask } from '../Service/Book/bookTask' import { BookVideo } from '../Service/Book/bookVideo' import { Watermark } from '../Service/watermark' +import { SubtitleService } from '../Service/Subtitle/subtitleService' +import { BookFrame } from '../Service/Book/bookFrame' let reverseBook = new ReverseBook() let basicReverse = new BasicReverse() let subtitle = new Subtitle() @@ -20,14 +22,16 @@ let imageStyle = new ImageStyle() let bookTask = new BookTask() let bookVideo = new BookVideo() let watermark = new Watermark() +let subtitleService = new SubtitleService() +let bookFrame = new BookFrame() export function BookIpc() { // 获取样式图片的子列表 - ipcMain.handle(DEFINE_STRING.BOOK.GET_BOOK_TYPE, async (event) => reverseBook.GetBookType()) + ipcMain.handle(DEFINE_STRING.BOOK.GET_BOOK_TYPE, async (event) => bookBasic.GetBookType()) // 新增或者是修改小说数据 ipcMain.handle(DEFINE_STRING.BOOK.ADD_OR_MODIFY_BOOK, async (event, book) => - reverseBook.AddOrModifyBook(book) + bookBasic.AddOrModifyBook(book) ) // 获取小说数据(通过传递的参数进行筛选) @@ -83,8 +87,7 @@ export function BookIpc() { //#endregion - //#region 一键反推的单个任务 - + //#region 分镜相关 // 开始计算分镜 ipcMain.handle( DEFINE_STRING.BOOK.COMPUTE_STORYBOARD, @@ -97,23 +100,46 @@ export function BookIpc() { async (event, bookId) => await reverseBook.Framing(bookId) ) - // 开始执行分镜任务 + // 替换分镜视频的当前帧 + ipcMain.handle(DEFINE_STRING.BOOK.REPLACE_VIDEO_CURRENT_FRAME, async (event, bookTaskDetailId, currentTime) => + await bookFrame.ReplaceVideoCurrentFrame(bookTaskDetailId, currentTime) + ) + + //#endregion + + //#region 提示词相关 + + // 合并提示词 + ipcMain.handle(DEFINE_STRING.BOOK.MERGE_PROMPT, async (event, id, type, operateBookType) => await reverseBook.MergePrompt(id, type, operateBookType)) + + //#endregion + + //#region 一键反推的单个任务 + + + // 开始执行获取小说文案的方法 ipcMain.handle( DEFINE_STRING.BOOK.GET_COPYWRITING, - async (event, bookId, bookTaskId) => await reverseBook.GetCopywriting(bookId, bookTaskId) + async (event, bookId, bookTaskId, operateBookType) => await subtitleService.GetCopywriting(bookId, bookTaskId, operateBookType) + ) + + // 获取小说的文案数据,然后保存到对应的文件中 + ipcMain.handle( + DEFINE_STRING.BOOK.EXPORT_COPYWRITING, + async (event, bookTaskId) => await subtitleService.ExportCopywriting(bookTaskId) ) // 执行去除水印 ipcMain.handle( DEFINE_STRING.BOOK.REMOVE_WATERMARK, - async (event, id,operateBookType) => await watermark.RemoveWatermark(id,operateBookType) + async (event, id, operateBookType) => await watermark.RemoveWatermark(id, operateBookType) ) // 添加反推任务到任务列表 ipcMain.handle( DEFINE_STRING.BOOK.ADD_REVERSE_PROMPT, - async (event, bookTaskDetailIds, type) => - await reverseBook.AddReversePromptTask(bookTaskDetailIds, type) + async (event, bookTaskDetailIds, operateBookType, type) => + await reverseBook.AddReversePrompt(bookTaskDetailIds, operateBookType, type) ) // 将反推出来的提示词写入到GPT提示词里面 @@ -154,9 +180,19 @@ export function BookIpc() { // 一拆四,将一个任务拆分成四个任务,并且复制对应的图片 ipcMain.handle( DEFINE_STRING.BOOK.ONE_TO_FOUR_BOOK_TASK, - async (event, bookTaskDetailId) => await bookBasic.OneToFourBookTask(bookTaskDetailId) + async (event, bookTaskDetailId) => await bookTask.OneToFourBookTask(bookTaskDetailId) ) + //#region 小说相关 + + // 重置小说数据 + ipcMain.handle(DEFINE_STRING.BOOK.RESET_BOOK_DATA, async (event, bookId) => await bookBasic.ResetBookData(bookId)) + + // 删除小说数据 + ipcMain.handle(DEFINE_STRING.BOOK.DELETE_BOOK_DATA, async (event, bookId) => await bookBasic.DeleteBookData(bookId)) + + //#endregion + //#region 小说批次任务相关 // 重置小说批次数据 diff --git a/src/main/IPCEvent/gptIpc.js b/src/main/IPCEvent/gptIpc.js index 70ff5d4..b7ccc30 100644 --- a/src/main/IPCEvent/gptIpc.js +++ b/src/main/IPCEvent/gptIpc.js @@ -83,6 +83,12 @@ function GptIpc() { DEFINE_STRING.GPT.SAVE_AI_SETTING, async (event, value) => await gptSetting.SaveAISetting(value) ) + + // 同步GPT Key 到指定的设置 + ipcMain.handle( + DEFINE_STRING.GPT.SYNC_GPT_KEY, + async (event, syncType) => await gptSetting.SyncGptKey(syncType) + ) } export { GptIpc } diff --git a/src/main/IPCEvent/index.js b/src/main/IPCEvent/index.js index a96a926..12c5c8d 100644 --- a/src/main/IPCEvent/index.js +++ b/src/main/IPCEvent/index.js @@ -12,7 +12,7 @@ import { MainIpc } from './mainIpc.js' import { GlobalIpc } from './globalIpc.js' import { ImageIpc } from './imageIpc.js' import { SystemIpc } from './systemIpc.js' -import { BookIpc } from './bookIpc.js' +import { BookIpc } from './bookIpc' import { TTSIpc } from './ttsIpc.js' import { DBIpc } from './dbIpc' diff --git a/src/main/IPCEvent/mjIpc.js b/src/main/IPCEvent/mjIpc.js index c52ffe3..9234023 100644 --- a/src/main/IPCEvent/mjIpc.js +++ b/src/main/IPCEvent/mjIpc.js @@ -138,12 +138,6 @@ function MjIpc() { async (event, value) => await discordSimple.DiscordDeleteMessage(value) ) - // MJ合并提示词命令 - ipcMain.handle( - DEFINE_STRING.MJ.MJ_MERGE_PROMPT, - async (event, id, mergeType) => await mjOpt.MergePrompt(id, mergeType) - ) - // MJ出单张图 ipcMain.handle( DEFINE_STRING.MJ.ADD_MJ_GENADD_MJ_GENERATE_IMAGE_TASK, diff --git a/src/main/IPCEvent/sdIpc.js b/src/main/IPCEvent/sdIpc.js index 706ec08..a972e89 100644 --- a/src/main/IPCEvent/sdIpc.js +++ b/src/main/IPCEvent/sdIpc.js @@ -29,11 +29,5 @@ function SdIpc() { // 文生图,单张 ipcMain.handle(DEFINE_STRING.SD.TXT2IMG, async (event, value) => await sd.txt2img(value)) - - // SD合并提示词 - ipcMain.handle( - DEFINE_STRING.SD.SD_MERGE_PROMPT, - async (event, id, mergeType) => await sdOpt.MergePrompt(id, mergeType) - ) } export { SdIpc } diff --git a/src/main/IPCEvent/writingIpc.js b/src/main/IPCEvent/writingIpc.js index 2d52eb2..46c6dc5 100644 --- a/src/main/IPCEvent/writingIpc.js +++ b/src/main/IPCEvent/writingIpc.js @@ -4,6 +4,8 @@ import { Writing } from '../Service/writing' let writing = new Writing(global) import { WritingSetting } from '../setting/writeSetting' let writingSetting = new WritingSetting() +import { SubtitleService } from '../Service/Subtitle/subtitleService' +let subtitleService = new SubtitleService() function WritingIpc() { // 监听分镜时间的保存 @@ -36,7 +38,7 @@ function WritingIpc() { async (event, value) => await writing.ImportSrtAndGetTime(value) ) - // 获取文案相关的配置(数据库) + // 获取文案格式化相关的配置(数据库) ipcMain.handle( DEFINE_STRING.WRITE.GET_WRITE_CONFIG, async (event) => await writingSetting.GetWritingConfig() @@ -48,6 +50,23 @@ function WritingIpc() { async (event, value) => await writingSetting.SaveWriteConfig(value) ) + // 获取提取文案相关的配置(数据库) + ipcMain.handle( + DEFINE_STRING.WRITE.GET_SUBTITLE_SETTING, + async (event) => await subtitleService.GetSubtitleSetting() + ) + + // 重置提取文案相关的配置(数据库) + ipcMain.handle( + DEFINE_STRING.WRITE.RESET_SUBTITLE_SETTING, + async (event) => await subtitleService.ResetSubtitleSetting() + ) + + ipcMain.handle( + DEFINE_STRING.WRITE.SAVE_SUBTITLE_SETTING, + async (event, subtitleSetting) => await subtitleService.SaveSubtitleSetting(subtitleSetting) + ) + ipcMain.handle( DEFINE_STRING.WRITE.ACTION_START, async (event, aiSetting, word) => await writing.ActionStart(aiSetting, word) diff --git a/src/main/Service/Book/BooKBasic.ts b/src/main/Service/Book/BooKBasic.ts index e5aea30..8535708 100644 --- a/src/main/Service/Book/BooKBasic.ts +++ b/src/main/Service/Book/BooKBasic.ts @@ -1,29 +1,19 @@ import { BookType, OperateBookType, TagDefineType } from '../../../define/enum/bookEnum' import { errorMessage, successMessage } from '../../Public/generalTools' import { BookService } from '../../../define/db/service/Book/bookService' -import { BookTaskService } from '../../../define/db/service/Book/bookTaskService' -import { BookTaskDetailService } from '../../../define/db/service/Book/bookTaskDetailService' -import { CopyImageType } from '../../../define/enum/bookEnum' -const { v4: uuidv4 } = require('uuid') -import { define } from '../../../define/define' import path from 'path' -import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder } from '../../../define/Tools/file' -import { Book } from '../../../model/book' +import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, DeleteFolderAllFile, GetSubdirectories } from '../../../define/Tools/file' import { GeneralResponse } from '../../../model/generalResponse' -import { cloneDeep, isEmpty } from 'lodash' +import { BookServiceBasic } from '../ServiceBasic/bookServiceBasic' +import { BookTask } from './bookTask' +import fs from 'fs' export class BookBasic { - constructor() { } - bookTaskService: BookTaskService - bookTaskDetailService: BookTaskDetailService - - async InitService() { - if (!this.bookTaskService) { - this.bookTaskService = await BookTaskService.getInstance() - } - if (!this.bookTaskDetailService) { - this.bookTaskDetailService = await BookTaskDetailService.getInstance() - } + bookServiceBasic: BookServiceBasic + bookTask: BookTask + constructor() { + this.bookServiceBasic = new BookServiceBasic(); + this.bookTask = new BookTask() } //#region 小说相关操作 @@ -73,209 +63,112 @@ export class BookBasic { //#endregion - //#region 小说批次任务相关操作 + //#region 小说相关操作 - - async OneToFourBookTask(bookTaskId: string) { + /** + * 重置小说数据 + * @param bookId 小说ID + * @returns + */ + async ResetBookData(bookId: string): Promise { try { - console.log(bookTaskId) - await this.InitService(); - let copyCount = 100 - let bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskId) - if (bookTask == null) { - throw new Error("没有找到对应的数小说任务,请检查数据") - } - // 获取所有的出图中最少的 - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ - bookTaskId: bookTaskId - }).data as Book.SelectBookTaskDetail[] - if (bookTaskDetail == null || bookTaskDetail.length <= 0) { - throw new Error("没有对应的小说分镜任务,请先添加分镜任务") + let book = await this.bookServiceBasic.GetBookDataById(bookId) + // 获取所有的小说批次 + let bookTasks = (await this.bookServiceBasic.GetBookTaskData({ + bookId: bookId + })).bookTasks; + // 重置批次任务 + for (let i = 0; i < bookTasks.length; i++) { + const element = bookTasks[i]; + // 第一个重置,后面的删除 + if (i == 0) { + let resetBookTaskData = await this.bookTask.ReSetBookTask(element.id); + if (resetBookTaskData.code == 0) { + throw new Error(resetBookTaskData.message) + } + } else { + let deleteBookTaskData = await this.bookTask.DeleteBookTask(element.id); + if (deleteBookTaskData.code == 0) { + throw new Error(deleteBookTaskData.message) + } + } } - for (let i = 0; i < bookTaskDetail.length; i++) { - const element = bookTaskDetail[i]; - if (isEmpty(element.subImagePath)) { - throw new Error("检测到图片没有出完,请先检查出图") - } - if (element.subImagePath == null || element.subImagePath.length <= 0) { - throw new Error("检测到图片没有出完,请先检查出图") - } - if (element.subImagePath.length < copyCount) { - copyCount = element.subImagePath.length - } + // 开始重置小说数据 + await this.bookServiceBasic.UpdateBookData(bookId, { + srtPath: undefined, + audioPath: undefined, + subtitlePosition: undefined, + imageStyle: undefined, + autoAnalyzeCharacter: undefined, + customizeImageStyle: undefined, + videoConfig: undefined, + prefixPrompt: undefined, + suffixPrompt: undefined, + draftSrtStyle: undefined, + backgroundMusic: undefined, + friendlyReminder: undefined, + watermarkPosition: undefined, + }) + // 文件重置,获取data下面的所有的子文件夹,删除所有的文件夹 + let dirs = await GetSubdirectories(path.join(book.bookFolderPath, 'data')) + for (let i = 0; i < dirs.length; i++) { + const element = dirs[i]; + await DeleteFolderAllFile(element, true) } - if (copyCount <= 0) { - throw new Error("批次设置错误,无法进行一拆四") + let scriptPath = path.join(book.bookFolderPath, 'script') + if (await CheckFileOrDirExist(scriptPath)) { + await DeleteFolderAllFile(scriptPath, true) } - // 开始复制 - let res = await this.CopyNewBookTask(bookTask, bookTaskDetail, copyCount - 1, CopyImageType.ONE) - if (res.code == 0) { - throw new Error(res.message) + // 删掉输入的备份文件和input文件 + let bakPath = path.join(book.bookFolderPath, 'tmp/bak'); + if (await CheckFileOrDirExist(bakPath)) { + await DeleteFolderAllFile(bakPath, true) } - return successMessage(res.data, "一拆四成功", "BookBasic_OneToFourBookTask") + let inputPath = path.join(book.bookFolderPath, 'tmp/input'); + if (await CheckFileOrDirExist(inputPath)) { + await DeleteFolderAllFile(inputPath, true) + } + + // 重置完毕,开始返回 + return successMessage('重置小说数据成功', 'BookBasic_ResetBookData'); } catch (error) { - return errorMessage("一拆四失败,失败信息如下:" + error.message, "BookBasic_OneToFourBookTask") + return errorMessage('重置小说数据失败,失败信息如下:' + error.message, 'BookBasic_ResetBookData'); } } /** - * 复制一个小说批次任务,创建新的小说批次任务 - * @param oldBookTaskId - * @param copyCount 复制的数量 - * @param isCopyImage 是否复制图片 + * 删除指定小说数据 + * @param bookId 要删除的小说ID */ - async CopyNewBookTask(sourceBookTask: Book.SelectBookTask, sourceBookTaskDetail: Book.SelectBookTaskDetail[], copyCount: number, copyImageType: CopyImageType) { + async DeleteBookData(bookId: string): Promise { try { - await this.InitService(); - let addBookTask = [] as Book.SelectBookTask[] - let addBookTaskDetail = [] as Book.SelectBookTaskDetail[] - - // 先处理文件夹的创建,包括小说任务的和小说任务分镜的 - for (let i = 0; i < copyCount; i++) { - let maxNo = this.bookTaskService.realm - .objects('BookTask') - .filtered('bookId = $0', sourceBookTask.bookId) - .max('no') - let no = maxNo == null ? 1 : Number(maxNo) + 1 + i - let name = 'output_0000' + no - let imageFolder = path.join(define.project_path, `${sourceBookTask.bookId}/tmp/${name}`) - await CheckFolderExistsOrCreate(imageFolder) - // 创建对应的文件夹 - let addOneBookTask = { - id: uuidv4(), - bookId: sourceBookTask.bookId, - no: no, - name: name, - generateVideoPath: sourceBookTask.generateVideoPath, - srtPath: sourceBookTask.srtPath, - audioPath: sourceBookTask.audioPath, - draftSrtStyle: sourceBookTask.draftSrtStyle, - backgroundMusic: sourceBookTask.backgroundMusic, - friendlyReminder: sourceBookTask.friendlyReminder, - imageFolder: path.relative(define.project_path, imageFolder), - status: sourceBookTask.status, - errorMsg: sourceBookTask.errorMsg, - updateTime: new Date(), - createTime: new Date(), - isAuto: sourceBookTask.isAuto, - imageStyle: sourceBookTask.imageStyle, - autoAnalyzeCharacter: sourceBookTask.autoAnalyzeCharacter, - customizeImageStyle: sourceBookTask.customizeImageStyle, - videoConfig: sourceBookTask.videoConfig, - prefixPrompt: sourceBookTask.prefixPrompt, - suffixPrompt: sourceBookTask.suffixPrompt, - version: sourceBookTask.version, - imageCategory: sourceBookTask.imageCategory, - } as Book.SelectBookTask - - addBookTask.push(addOneBookTask) - - for (let j = 0; j < sourceBookTaskDetail.length; j++) { - const element = sourceBookTaskDetail[j]; - - let outImagePath = undefined as string - let subImagePath = [] as string[] - - if (copyImageType == CopyImageType.ALL) { // 直接全部复制 - outImagePath = element.outImagePath - subImagePath = element.subImagePath - } else if (copyImageType == CopyImageType.ONE) { // 只复制对应的 - let oldImage = element.subImagePath[i + 1] - outImagePath = path.join(imageFolder, path.basename(element.outImagePath)) - await CopyFileOrFolder(oldImage, outImagePath) - - subImagePath = [] - } - else if (copyImageType == CopyImageType.NONE) { - outImagePath = undefined - subImagePath = [] - } else { - throw new Error("无效的图片复制类型") - } - if (outImagePath) { - // 单独处理一下显示的图片 - let imageBaseName = path.basename(element.outImagePath); - let newImageBaseName = path.join(define.project_path, `${sourceBookTask.bookId}/tmp/${name}/${imageBaseName}`) - await CopyFileOrFolder(outImagePath, newImageBaseName) - } - // 处理SD设置 - let sdConifg = undefined - if (element.sdConifg) { - let sdConifg = cloneDeep(element.sdConifg) - if (sdConifg.webuiConfig) { - let tempSdConfig = cloneDeep(sdConifg.webuiConfig); - tempSdConfig.id = uuidv4() - sdConifg.webuiConfig = tempSdConfig - } - } - - let reverseId = uuidv4() - // 处理反推数据 - let reverseMessage = [] as Book.ReversePrompt[] - if (element.reversePrompt && element.reversePrompt.length > 0) { - reverseMessage = cloneDeep(element.reversePrompt) - for (let k = 0; k < reverseMessage.length; k++) { - reverseMessage[k].id = uuidv4() - reverseMessage[k].bookTaskDetailId = reverseId - } - } - - let addOneBookTaskDetail = { - id: reverseId, - no: element.no, - name: element.name, - bookId: sourceBookTask.bookId, - bookTaskId: addOneBookTask.id, - videoPath: path.relative(define.project_path, element.videoPath), - word: element.word, - oldImage: path.relative(define.project_path, element.oldImage), - afterGpt: element.afterGpt, - startTime: element.startTime, - endTime: element.endTime, - timeLimit: element.timeLimit, - subValue: element.subValue, - characterTags: element.characterTags && element.characterTags.length > 0 ? cloneDeep(element.characterTags) : [], - gptPrompt: element.gptPrompt, - outImagePath: path.relative(define.project_path, outImagePath), - subImagePath: subImagePath || [], - prompt: element.prompt, - adetailer: element.adetailer, - sdConifg: sdConifg, - createTime: new Date(), - updateTime: new Date(), - audioPath: element.audioPath, - subtitlePosition: element.subtitlePosition, - status: element.status, - reversePrompt: reverseMessage, - imageLock: element.imageLock - } as Book.SelectBookTaskDetail - addBookTaskDetail.push(addOneBookTaskDetail) - } + let book = await this.bookServiceBasic.GetBookDataById(bookId); + // 先将所有的数据重置 + let resetRes = await this.ResetBookData(bookId); + if (resetRes.code == 0) { + throw new Error(resetRes.message) + } + let bookTasks = (await this.bookServiceBasic.GetBookTaskData({ + bookId: bookId + })).bookTasks; + // 删除遗留重置的小说批次任务 + for (let i = 0; i < bookTasks.length; i++) { + const element = bookTasks[i]; + await this.bookServiceBasic.DeleteBookTaskData(element.id); } - // 数据处理完毕,开始新增数据 - // 将所有的复制才做,全部放在一个事务中 - this.bookTaskService.transaction(() => { - for (let i = 0; i < addBookTask.length; i++) { - const element = addBookTask[i]; - this.bookTaskService.realm.create('BookTask', element) - } - for (let i = 0; i < addBookTaskDetail.length; i++) { - const element = addBookTaskDetail[i]; - this.bookTaskDetailService.realm.create('BookTaskDetail', element) - } - }) - // 全部创建完成 - // 查找到数据,然后全部返回 - let returnBookTask = this.bookTaskService.GetBookTaskData({ - bookId: sourceBookTask.bookId - }).data as Book.SelectBookTask[] + // 开始删除数据 + await this.bookServiceBasic.DeleteBookData(bookId); - return successMessage(returnBookTask, "复制小说任务成功", "BookBasic_CopyNewBookTask") + // 开始删除文件 + let bookPath = book.bookFolderPath; + if (await CheckFileOrDirExist(bookPath)) { + await DeleteFolderAllFile(bookPath, true) + } + return successMessage(null, '删除小说数据成功', 'BookBasic_DeleteBookData'); } catch (error) { - console.log(error) - throw error + return errorMessage('删除小说数据失败,失败信息如下:' + error.message, 'BookBasic_DeleteBookData'); } } diff --git a/src/main/Service/Book/ReverseBook.ts b/src/main/Service/Book/ReverseBook.ts index e9e411f..7c7d09f 100644 --- a/src/main/Service/Book/ReverseBook.ts +++ b/src/main/Service/Book/ReverseBook.ts @@ -12,65 +12,36 @@ import { TaskScheduler } from "../../Service/taskScheduler" import { Book } from '../../../model/book' import { LoggerStatus, OtherData, ResponseMessageType } from '../../../define/enum/softwareEnum' import { GeneralResponse } from '../../../model/generalResponse' -import { cloneDeep, isEmpty } from 'lodash' -import { Subtitle } from '../../Service/subtitle' +import { Subtitle } from '../Subtitle/subtitle' import { Watermark } from '../watermark' -import { SubtitleSavePositionType } from '../../../define/enum/waterMarkAndSubtitle' -import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, GetFilesWithExtensions } from '../../../define/Tools/file' -import { ValidateJson } from '../../../define/Tools/validate' -import { GetImageBase64, ProcessImage } from '../../../define/Tools/image' -import { BookBackTaskType, BookType, TagDefineType, TaskExecuteType } from '../../../define/enum/bookEnum' -import { BookBackTaskListService } from '../../../define/db/service/Book/bookBackTaskListService' +import { BookBackTaskType, BookType, OperateBookType, PromptMergeType, TagDefineType, TaskExecuteType } from '../../../define/enum/bookEnum' import { MJOpt } from '../MJ/mj' import { TagDefine } from '../../../define/tagDefine' -import { ImageStyleDefine } from '../../../define/iamgeStyleDefine' +import { BookServiceBasic } from '../ServiceBasic/bookServiceBasic' +import { SDOpt } from '../SD/sd' /** * 一键反推的相关操作 */ -export class ReverseBook extends BookBasic { +export class ReverseBook { basicReverse: BasicReverse - bookTaskService: BookTaskService taskScheduler: TaskScheduler - bookService: BookService - bookTaskDetailService: BookTaskDetailService - bookBackTaskListService: BookBackTaskListService mjOpt: MJOpt = new MJOpt() + sdOpt: SDOpt = new SDOpt() tagDefine: TagDefine subtitle: Subtitle watermark: Watermark + bookServiceBasic: BookServiceBasic constructor() { - super() this.basicReverse = new BasicReverse() this.tagDefine = new TagDefine() + this.subtitle = new Subtitle() + this.watermark = new Watermark() + this.taskScheduler = new TaskScheduler() + this.bookServiceBasic = new BookServiceBasic() } - - async InitService() { - if (!this.bookTaskService) { - this.bookTaskService = await BookTaskService.getInstance() - } - if (!this.taskScheduler) { - this.taskScheduler = new TaskScheduler() - } - if (!this.bookService) { - this.bookService = await BookService.getInstance() - } - if (!this.bookTaskDetailService) { - this.bookTaskDetailService = await BookTaskDetailService.getInstance() - } - if (!this.subtitle) { - this.subtitle = new Subtitle() - } - if (!this.watermark) { - this.watermark = new Watermark() - } - if (!this.bookBackTaskListService) { - this.bookBackTaskListService = await BookBackTaskListService.getInstance() - } - } - // 主动返回前端的消息 sendReturnMessage(data: GeneralResponse.MessageResponse, message_name = DEFINE_STRING.BOOK.GET_COPYWRITING_RETURN) { global.newWindow[0].win.webContents.send(message_name, data) @@ -104,15 +75,9 @@ export class ReverseBook extends BookBasic { * 获取小说的任务列表 * @param {*} bookTaskCondition 查询任务列表的条件 */ - async GetBookTaskData(bookTaskCondition): Promise { + async GetBookTaskData(bookTaskCondition: Book.QueryBookTaskCondition): Promise { try { - await this.InitService() - let _bookTaskService = await BookTaskService.getInstance() - let res = _bookTaskService.GetBookTaskData(bookTaskCondition) - if (res.code == 0) { - throw new Error(res.message) - } - + let res = await this.bookServiceBasic.GetBookTaskData(bookTaskCondition) // //TODO 这个后面是不是将所有的数据都是用数据库 // // 这边加载自定义风格 // let styleLists = await this.tagDefine.getTagDataByTypeAndProperty('dynamic', "style_tags"); @@ -142,10 +107,10 @@ export class ReverseBook extends BookBasic { // } // res.data.bookTasks[index].styleList = styleList; // } - return successMessage(res.data, '获取小说任务成功', 'ReverseBook_GetBookTaskData') + return successMessage(res, '获取小说任务成功', 'ReverseBook_GetBookTaskData') } catch (error) { return errorMessage( - '获取小说对应批次错误,错误信息入校:' + error.message, + '获取小说对应批次错误,错误信息入下:' + error.message, 'ReverseBook_GetBookTaskData' ) } @@ -153,20 +118,15 @@ export class ReverseBook extends BookBasic { /** * 获取小说的所有的任务详情 - * @param bookTaskId + * @param bookTaskId 小说任务的ID */ - async GetBookTaskDetail(bookTaskId: string) { + async GetBookTaskDetail(bookTaskId: string): Promise { try { - await this.InitService() - let _bookTaskDetailService = await BookTaskDetailService.getInstance() - let res = _bookTaskDetailService.GetBookTaskData({ bookTaskId: bookTaskId }) - if (res.code == 0) { - throw new Error(res.message) - } - return res + let res = await this.bookServiceBasic.GetBookTaskDetailData({ bookTaskId: bookTaskId }) + return successMessage(res, '获取小说任务详情成功', 'ReverseBook_GetBookTaskDetail') } catch (error) { return errorMessage( - '获取小说对应批次错误,错误信息入校:' + error.message, + '获取小说对应批次错误,错误信息入下:' + error.message, 'ReverseBook_GetBookTaskData' ) } @@ -236,10 +196,7 @@ export class ReverseBook extends BookBasic { * @returns */ async GetBookAndTask(bookId: string, bookTaskName: string) { - let book = this.bookService.GetBookDataById(bookId) - if (book == null) { - throw new Error("查找小说数据失败"); - } + let book = await this.bookServiceBasic.GetBookDataById(bookId) // 获取小说对应的批次任务数据,默认初始化为第一个 let condition = { bookId: bookId @@ -249,20 +206,14 @@ export class ReverseBook extends BookBasic { } else { condition["id"] = bookTaskName } - let bookTaskRes = await this.bookTaskService.GetBookTaskData(condition) - if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) { - let msg = "没有找到对应的小说批次任务数据" - this.taskScheduler.AddLogToDB(bookId, book.type, msg, OtherData.DEFAULT, LoggerStatus.FAIL) - throw new Error(msg) - } - return { book: book as Book.SelectBook, bookTask: bookTaskRes.data.bookTasks[0] as Book.SelectBookTask } + let bookTaskRes = await this.bookServiceBasic.GetBookTaskData(condition) + return { book: book as Book.SelectBook, bookTask: bookTaskRes.bookTasks[0] as Book.SelectBookTask } } /** * 开始分镜任务 */ async ComputeStoryboard(bookId: string): Promise { try { - await this.InitService() let { book, bookTask } = await this.GetBookAndTask(bookId, 'output_00001') let res = await this.basicReverse.ComputeStoryboardFunc(bookId, bookTask.id); // 分镜成功直接返回 @@ -279,15 +230,15 @@ export class ReverseBook extends BookBasic { */ async Framing(bookId: string): Promise { try { - await this.InitService() let { book, bookTask } = await this.GetBookAndTask(bookId, 'output_00001') - - // 获取所有的分镜数据 - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ - bookId: bookId, - bookTaskId: bookTask.id - }) - if (bookTaskDetail.data.length <= 0) { + let bookTaskDetail = undefined as Book.SelectBookTaskDetail[] + try { + // 获取所有的分镜数据 + bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailData({ + bookId: bookId, + bookTaskId: bookTask.id + }) + } catch (error) { // 传入的分镜数据为空,需要重新获取 await this.taskScheduler.AddLogToDB( bookId, @@ -298,9 +249,7 @@ export class ReverseBook extends BookBasic { ) throw new Error("没有传入分镜数据,请先进行分镜计算"); } - - let bookTaskDetails = bookTaskDetail.data as Book.SelectBookTaskDetail[] - + let bookTaskDetails = bookTaskDetail; for (let i = 0; i < bookTaskDetails.length; i++) { const item = bookTaskDetails[i]; @@ -314,12 +263,12 @@ export class ReverseBook extends BookBasic { LoggerStatus.SUCCESS ) - bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ + bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailData({ bookId: bookId, bookTaskId: bookTask.id }) - bookTaskDetails = bookTaskDetail.data as Book.SelectBookTaskDetail[] + bookTaskDetails = bookTaskDetail as Book.SelectBookTaskDetail[] for (let i = 0; i < bookTaskDetails.length; i++) { const item = bookTaskDetails[i]; let res = await this.basicReverse.FrameFunc(book, item); @@ -331,96 +280,70 @@ export class ReverseBook extends BookBasic { return errorMessage("开始切割视频并抽帧失败,失败信息如下:" + error.message, 'ReverseBook_Framing') } } - - /** - * 提起文案 - */ - async GetCopywriting(bookId: string, bookTaskId: string = null): Promise { - try { - await this.InitService() - let { book, bookTask } = await this.GetBookAndTask(bookId, bookTaskId ? bookTaskId : 'output_00001') - if (isEmpty(book.subtitlePosition)) { - throw new Error("请先设置小说的字幕位置") - } - // 获取所有的分镜数据 - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ - bookId: bookId, - bookTaskId: bookTask.id - }) - if (bookTaskDetail.data.length <= 0) { - // 传入的分镜数据为空,需要重新获取 - await this.taskScheduler.AddLogToDB( - bookId, - book.type, - `没有传入分镜数据,请先进行分镜计算`, - OtherData.DEFAULT, - LoggerStatus.DOING - ) - throw new Error("没有传入分镜数据,请先进行分镜计算"); - } - - let bookTaskDetails = bookTaskDetail.data as Book.SelectBookTaskDetail[] - for (let i = 0; i < bookTaskDetails.length; i++) { - const item = bookTaskDetails[i]; - - let res = await this.subtitle.GetVideoFrameText({ - id: item.id, - videoPath: item.videoPath, - type: SubtitleSavePositionType.STORYBOARD_VIDEO, - subtitlePosition: book.subtitlePosition - }) - if (res.code == 0) { - throw new Error(res.message) - } - // 修改数据 - this.bookTaskDetailService.UpdateBookTaskDetail(item.id, { - word: res.data, - afterGpt: res.data - }); - - // let res = await this.basicReverse.GetCopywritingFunc(book, item); - // 将当前的数据实时返回,前端进行修改 - this.sendReturnMessage({ - code: 1, - id: item.id, - type: ResponseMessageType.GET_TEXT, - data: res.data // 返回识别到的文案 - }) - - // 添加日志 - await this.taskScheduler.AddLogToDB( - bookId, - book.type, - `${item.name} 识别文案成功`, - bookTask.id, - LoggerStatus.SUCCESS - ) - } - return successMessage(null, "识别是所有文案成功", "ReverseBook_GetCopywriting") - } catch (error) { - return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'ReverseBook_GetCopywriting') - } - } //#endregion //#region 反推相关任务 + /** + * 所有的反推任务的入口 + * @param id 需要反推的ID,可以是小说任务ID,也可以是小说分镜ID + * @param operateBookType 操作的分类 + * @param type 反推的类型 + * @returns + */ + async AddReversePrompt(id: string, operateBookType: OperateBookType, type: BookType): Promise { + try { + let bookTaskDetailIds: string[] = [] + let bookTaskDetails = [] as Book.SelectBookTaskDetail[] + if (operateBookType == OperateBookType.BOOKTASK) { + bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({ + bookTaskId: id + }) + } else if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(id); + bookTaskDetails = [bookTaskDetail] + } else if (operateBookType == OperateBookType.UNDERBOOKTASK) { + // 下生图 + let tempBooktaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(id); + let tempBooktaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({ + bookTaskId: tempBooktaskDetail.bookTaskId + }); + for (let i = 0; i < tempBooktaskDetails.length; i++) { + const element = tempBooktaskDetails[i]; + if (tempBooktaskDetail.no <= element.no) { + bookTaskDetails.push(element) + } + } + } else { + throw new Error("未知的操作模式,请检查"); + } + + for (let i = 0; i < bookTaskDetails.length; i++) { + const element = bookTaskDetails[i]; + bookTaskDetailIds.push(element.id) + } + if (bookTaskDetailIds.length <= 0) { + throw new Error("没有需要反推的数据,请检查") + } + await this.AddReversePromptTask(bookTaskDetailIds, type); + return successMessage(null, "添加反推任务成功", 'ReverseBook_AddReversePrompt') + } catch (error) { + return errorMessage("添加反推任务失败,错误信息如下:" + error.message, "ReverseBook_AddReversePrompt") + } + } + /** * 添加单句生图任务 * @param bookTaskDetailId 小说单个分镜的ID * @param type 反推的类型 * @returns */ - async AddReversePromptTask(bookTaskDetailIds: string[], type: BookType): Promise { + async AddReversePromptTask(bookTaskDetailIds: string[], type: BookType): Promise { try { - await this.InitService() for (let index = 0; index < bookTaskDetailIds.length; index++) { const bookTaskDetailId = bookTaskDetailIds[index]; - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId) - if (bookTaskDetail == null) { - throw new Error("没有找到对应的分镜数据") - } - let book = this.bookService.GetBookDataById(bookTaskDetail.bookId) + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskDetailId) + let book = await this.bookServiceBasic.GetBookDataById(bookTaskDetail.bookId) // 是不是又额外的类型,没有的话试用小说的类型 let task_type = undefined as BookBackTaskType if (!type) { @@ -436,17 +359,14 @@ export class ReverseBook extends BookBasic { default: throw new Error("暂不支持的推理类型") } - let taskRes = await this.bookBackTaskListService.AddBookBackTask(book.id, task_type, TaskExecuteType.AUTO, bookTaskDetail.bookTaskId, bookTaskDetail.id + // 添加后台任务 + await this.bookServiceBasic.AddBookBackTask(book.id, task_type, TaskExecuteType.AUTO, bookTaskDetail.bookTaskId, bookTaskDetail.id ); - if (taskRes.code == 0) { - throw new Error(taskRes.message) - } // 添加返回日志 await this.taskScheduler.AddLogToDB(book.id, book.type, `添加 ${task_type} 反推任务成功`, bookTaskDetail.bookTaskId, LoggerStatus.SUCCESS) } - return successMessage(null, "添加反推任务成功", "ReverseBook_AddReversePromptTask") } catch (error) { - return errorMessage("添加单个反推失败,错误信息如下:" + error.message, "ReverseBook_SingleReversePrompt") + throw error } } @@ -481,14 +401,12 @@ export class ReverseBook extends BookBasic { } } - /** * 删除指定的提示词数据 * @param bookTaskDetailIds 要删除的提示词ID */ async RemoveReverseData(bookTaskDetailIds: string[]) { try { - await this.InitService() // 开始删除 if (bookTaskDetailIds.length <= 0) { throw new Error("没有传入要删除的数据") @@ -496,7 +414,7 @@ export class ReverseBook extends BookBasic { for (let i = 0; i < bookTaskDetailIds.length; i++) { const element = bookTaskDetailIds[i]; - this.bookTaskDetailService.DeleteBookTaskDetailReversePromptById(element) + await this.bookServiceBasic.DeleteBookTaskDetailReversePromptById(element); } // 全部删除完毕 @@ -505,6 +423,30 @@ export class ReverseBook extends BookBasic { return errorMessage("删除反推数据失败,错误信息如下:" + error.message, "ReverseBook_RemoveReverseData") } } + //#endregion + + //#region 提示词相关操作 + + /** + * 合并提示词的入口函数 + * @param id 合并的ID,可以是小说任务ID,也可以是小说分镜ID + * @param operateBookType 操作的类型 + * @param type 合并的类型(MJ。SD 。D3) + * @returns + */ + async MergePrompt(id: string, type: PromptMergeType, operateBookType: OperateBookType): Promise { + try { + if (type == PromptMergeType.MJ_MERGE) { + return await this.mjOpt.MergePrompt(id, operateBookType); + } else if (type == PromptMergeType.SD_MERGE) { + return await this.sdOpt.MergePrompt(id, operateBookType) + } else { + throw new Error("未知的合并模式,请检查") + } + } catch (error) { + return errorMessage("合并提示词失败,错误信息如下:" + error.message, "ReverseBook_MergePrompt") + } + } //#endregion } diff --git a/src/main/Service/Book/bookFrame.ts b/src/main/Service/Book/bookFrame.ts new file mode 100644 index 0000000..bd88638 --- /dev/null +++ b/src/main/Service/Book/bookFrame.ts @@ -0,0 +1,59 @@ +import { isEmpty } from "lodash"; +import { GeneralResponse } from "../../../model/generalResponse"; +import { errorMessage, successMessage } from "../../Public/generalTools"; +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; +import path from 'path'; +import { FfmpegOptions } from "../ffmpegOptions"; +import { CheckFileOrDirExist, CopyFileOrFolder, DeleteFolderAllFile } from "../../../define/Tools/file"; +import fs from 'fs'; + +export class BookFrame { + bookServiceBasic: BookServiceBasic + ffmpegOptions: FfmpegOptions + constructor() { + this.bookServiceBasic = new BookServiceBasic(); + this.ffmpegOptions = new FfmpegOptions(); + } + + + /** + * 替换指定分镜的视频当前帧 + * @param bookTaskDetailId 指定的小说分镜ID + * @param current 要替换的帧 + */ + async ReplaceVideoCurrentFrame(bookTaskDetailId: string, current: number): Promise { + try { + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskDetailId); + let videoPath = bookTaskDetail.videoPath; + if (isEmpty(videoPath)) { + throw new Error('未找到对应的视频路径,请检查'); + } + let oldImagePath = bookTaskDetail.oldImage; + let tempOldImagePath = path.join(path.dirname(oldImagePath), `temp_${bookTaskDetail.name}.png`); + // 删除之前的图片 + if (await CheckFileOrDirExist(tempOldImagePath)) { + await fs.promises.unlink(tempOldImagePath); + } + // 开始裁剪 + let res = await this.ffmpegOptions.FfmpegGetFrame(current, videoPath, tempOldImagePath); + if (res.code == 0) { + // 抽帧失败 + global.logger.error('BookFrame_ReplaceVideoCurrentFrame', '抽取视频指定帧失败,请检查'); + throw new Error('抽取视频指定帧失败,请检查'); + } + // 成功。开始替换 + let fileExist = await CheckFileOrDirExist(tempOldImagePath); + if (!fileExist) { + throw new Error('抽帧出来的图片不存在,请检查'); + } + await fs.promises.unlink(oldImagePath); + // 重命名 + await CopyFileOrFolder(tempOldImagePath, oldImagePath); + await fs.promises.unlink(tempOldImagePath); + return successMessage(oldImagePath + '?t=' + new Date().getTime(), '视频指定的视频帧成功', 'BookFrame_ReplaceVideoCurrentFrame'); + } catch (error) { + return errorMessage('替换指定分镜的视频当前帧失败,失败信息如下:' + error.toString(), 'BookFrame_ReplaceVideoCurrentFrame'); + } + } + +} \ No newline at end of file diff --git a/src/main/Service/Book/bookTask.ts b/src/main/Service/Book/bookTask.ts index 41847f7..ede97db 100644 --- a/src/main/Service/Book/bookTask.ts +++ b/src/main/Service/Book/bookTask.ts @@ -1,10 +1,15 @@ -import { CheckFolderExistsOrCreate, DeleteFolderAllFile } from "../../../define/Tools/file"; +import { CheckFolderExistsOrCreate, CopyFileOrFolder, DeleteFolderAllFile } from "../../../define/Tools/file"; import { BookTaskService } from "../../../define/db/service/Book/bookTaskService"; import { BookTaskDetailService } from "../../../define/db/service/Book/bookTaskDetailService"; import { BookService } from "../../../define/db/service/Book/bookService"; -import { BookTaskStatus, OperateBookType } from "../../../define/enum/bookEnum"; +import { BookTaskStatus, CopyImageType, OperateBookType } from "../../../define/enum/bookEnum"; import { errorMessage, successMessage } from "../../Public/generalTools"; import { Book } from "../../../model/book"; +import path from 'path' +import { cloneDeep, isEmpty } from "lodash"; +import { define } from '../../../define/define' +import { v4 as uuidv4 } from 'uuid' +import { GeneralResponse } from "../../../model/generalResponse"; /** * 小说批次相关的操作 @@ -33,7 +38,7 @@ export class BookTask { * 重置小说任务数据 * @param bookTaskId 小说任务ID */ - async ReSetBookTask(bookTaskId: string) { + async ReSetBookTask(bookTaskId: string): Promise { try { console.log(bookTaskId) await this.InitService() @@ -57,7 +62,7 @@ export class BookTask { * 删除对应的小说批次数据 * @param bookTaskId 小说批次ID */ - async DeleteBookTask(bookTaskId: string) { + async DeleteBookTask(bookTaskId: string): Promise { try { await this.InitService(); let bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskId); @@ -75,4 +80,212 @@ export class BookTask { } } + /** + * 将小说批次任务出图进行一拆四 + * @param bookTaskId 操作的小说批次任务ID + * @returns + */ + async OneToFourBookTask(bookTaskId: string) { + try { + console.log(bookTaskId) + await this.InitService(); + let copyCount = 100 + let bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskId) + if (bookTask == null) { + throw new Error("没有找到对应的数小说任务,请检查数据") + } + // 获取所有的出图中最少的 + let bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ + bookTaskId: bookTaskId + }).data as Book.SelectBookTaskDetail[] + if (bookTaskDetail == null || bookTaskDetail.length <= 0) { + throw new Error("没有对应的小说分镜任务,请先添加分镜任务") + } + + for (let i = 0; i < bookTaskDetail.length; i++) { + const element = bookTaskDetail[i]; + if (isEmpty(element.subImagePath)) { + throw new Error("检测到图片没有出完,请先检查出图") + } + if (element.subImagePath == null || element.subImagePath.length <= 0) { + throw new Error("检测到图片没有出完,请先检查出图") + } + if (element.subImagePath.length < copyCount) { + copyCount = element.subImagePath.length + } + } + if (copyCount <= 0) { + throw new Error("批次设置错误,无法进行一拆四") + } + // 开始复制 + let res = await this.CopyNewBookTask(bookTask, bookTaskDetail, copyCount - 1, CopyImageType.ONE) + if (res.code == 0) { + throw new Error(res.message) + } + return successMessage(res.data, "一拆四成功", "BookBasic_OneToFourBookTask") + } catch (error) { + return errorMessage("一拆四失败,失败信息如下:" + error.message, "BookBasic_OneToFourBookTask") + } + } + + /** + * 复制一个小说批次任务,创建新的小说批次任务 + * @param oldBookTaskId + * @param copyCount 复制的数量 + * @param isCopyImage 是否复制图片 + */ + async CopyNewBookTask(sourceBookTask: Book.SelectBookTask, sourceBookTaskDetail: Book.SelectBookTaskDetail[], copyCount: number, copyImageType: CopyImageType) { + try { + await this.InitService(); + let addBookTask = [] as Book.SelectBookTask[] + let addBookTaskDetail = [] as Book.SelectBookTaskDetail[] + + // 先处理文件夹的创建,包括小说任务的和小说任务分镜的 + for (let i = 0; i < copyCount; i++) { + let maxNo = this.bookTaskService.realm + .objects('BookTask') + .filtered('bookId = $0', sourceBookTask.bookId) + .max('no') + let no = maxNo == null ? 1 : Number(maxNo) + 1 + i + let name = 'output_0000' + no + let imageFolder = path.join(define.project_path, `${sourceBookTask.bookId}/tmp/${name}`) + await CheckFolderExistsOrCreate(imageFolder) + // 创建对应的文件夹 + let addOneBookTask = { + id: uuidv4(), + bookId: sourceBookTask.bookId, + no: no, + name: name, + generateVideoPath: sourceBookTask.generateVideoPath, + srtPath: sourceBookTask.srtPath, + audioPath: sourceBookTask.audioPath, + draftSrtStyle: sourceBookTask.draftSrtStyle, + backgroundMusic: sourceBookTask.backgroundMusic, + friendlyReminder: sourceBookTask.friendlyReminder, + imageFolder: path.relative(define.project_path, imageFolder), + status: sourceBookTask.status, + errorMsg: sourceBookTask.errorMsg, + updateTime: new Date(), + createTime: new Date(), + isAuto: sourceBookTask.isAuto, + imageStyle: sourceBookTask.imageStyle, + autoAnalyzeCharacter: sourceBookTask.autoAnalyzeCharacter, + customizeImageStyle: sourceBookTask.customizeImageStyle, + videoConfig: sourceBookTask.videoConfig, + prefixPrompt: sourceBookTask.prefixPrompt, + suffixPrompt: sourceBookTask.suffixPrompt, + version: sourceBookTask.version, + imageCategory: sourceBookTask.imageCategory, + } as Book.SelectBookTask + + addBookTask.push(addOneBookTask) + + for (let j = 0; j < sourceBookTaskDetail.length; j++) { + const element = sourceBookTaskDetail[j]; + + let outImagePath = undefined as string + let subImagePath = [] as string[] + + if (copyImageType == CopyImageType.ALL) { // 直接全部复制 + outImagePath = element.outImagePath + subImagePath = element.subImagePath + } else if (copyImageType == CopyImageType.ONE) { // 只复制对应的 + let oldImage = element.subImagePath[i + 1] + outImagePath = path.join(imageFolder, path.basename(element.outImagePath)) + await CopyFileOrFolder(oldImage, outImagePath) + + subImagePath = [] + } + else if (copyImageType == CopyImageType.NONE) { + outImagePath = undefined + subImagePath = [] + } else { + throw new Error("无效的图片复制类型") + } + if (outImagePath) { + // 单独处理一下显示的图片 + let imageBaseName = path.basename(element.outImagePath); + let newImageBaseName = path.join(define.project_path, `${sourceBookTask.bookId}/tmp/${name}/${imageBaseName}`) + await CopyFileOrFolder(outImagePath, newImageBaseName) + } + // 处理SD设置 + let sdConifg = undefined + if (element.sdConifg) { + let sdConifg = cloneDeep(element.sdConifg) + if (sdConifg.webuiConfig) { + let tempSdConfig = cloneDeep(sdConifg.webuiConfig); + tempSdConfig.id = uuidv4() + sdConifg.webuiConfig = tempSdConfig + } + } + + let reverseId = uuidv4() + // 处理反推数据 + let reverseMessage = [] as Book.ReversePrompt[] + if (element.reversePrompt && element.reversePrompt.length > 0) { + reverseMessage = cloneDeep(element.reversePrompt) + for (let k = 0; k < reverseMessage.length; k++) { + reverseMessage[k].id = uuidv4() + reverseMessage[k].bookTaskDetailId = reverseId + } + } + + let addOneBookTaskDetail = { + id: reverseId, + no: element.no, + name: element.name, + bookId: sourceBookTask.bookId, + bookTaskId: addOneBookTask.id, + videoPath: path.relative(define.project_path, element.videoPath), + word: element.word, + oldImage: path.relative(define.project_path, element.oldImage), + afterGpt: element.afterGpt, + startTime: element.startTime, + endTime: element.endTime, + timeLimit: element.timeLimit, + subValue: element.subValue, + characterTags: element.characterTags && element.characterTags.length > 0 ? cloneDeep(element.characterTags) : [], + gptPrompt: element.gptPrompt, + outImagePath: path.relative(define.project_path, outImagePath), + subImagePath: subImagePath || [], + prompt: element.prompt, + adetailer: element.adetailer, + sdConifg: sdConifg, + createTime: new Date(), + updateTime: new Date(), + audioPath: element.audioPath, + subtitlePosition: element.subtitlePosition, + status: element.status, + reversePrompt: reverseMessage, + imageLock: element.imageLock + } as Book.SelectBookTaskDetail + addBookTaskDetail.push(addOneBookTaskDetail) + } + } + + // 数据处理完毕,开始新增数据 + // 将所有的复制才做,全部放在一个事务中 + this.bookTaskService.transaction(() => { + for (let i = 0; i < addBookTask.length; i++) { + const element = addBookTask[i]; + this.bookTaskService.realm.create('BookTask', element) + } + for (let i = 0; i < addBookTaskDetail.length; i++) { + const element = addBookTaskDetail[i]; + this.bookTaskDetailService.realm.create('BookTaskDetail', element) + } + }) + // 全部创建完成 + // 查找到数据,然后全部返回 + let returnBookTask = this.bookTaskService.GetBookTaskData({ + bookId: sourceBookTask.bookId + }).data as Book.SelectBookTask[] + + return successMessage(returnBookTask, "复制小说任务成功", "BookBasic_CopyNewBookTask") + } catch (error) { + console.log(error) + throw error + } + } + } \ No newline at end of file diff --git a/src/main/Service/Book/bookVideo.ts b/src/main/Service/Book/bookVideo.ts index 07169e8..d50e6a7 100644 --- a/src/main/Service/Book/bookVideo.ts +++ b/src/main/Service/Book/bookVideo.ts @@ -10,14 +10,17 @@ import { CheckFolderExistsOrCreate } from "../../../define/Tools/file"; import { BookTaskDetailService } from "../../../define/db/service/Book/bookTaskDetailService"; import fs from 'fs' import { ClipDraft } from '../../Public/clipDraft' +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; export class BookVideo { bookService: BookService bookTaskService: BookTaskService setting: Setting bookTaskDetailService: BookTaskDetailService + bookServiceBasic: BookServiceBasic constructor() { this.setting = new Setting(global) + this.bookServiceBasic = new BookServiceBasic() } async InitService() { @@ -71,17 +74,16 @@ export class BookVideo { } // 将修改数据放在一个事务中 - this.bookService.transaction(() => { - for (let i = 0; i < bookTasks.length; i++) { - const element = bookTasks[i]; - let modifyBookTask = this.bookService.realm.objectForPrimaryKey('BookTask', element.id); - modifyBookTask.backgroundMusic = book.backgroundMusic; - modifyBookTask.friendlyReminder = book.friendlyReminder; - modifyBookTask.draftSrtStyle = book.draftSrtStyle; - modifyBookTask.srtPath = book.srtPath; - modifyBookTask.audioPath = book.audioPath; - } - }) + for (let i = 0; i < bookTasks.length; i++) { + const element = bookTasks[i]; + this.bookServiceBasic.UpdetedBookTaskData(element.id, { + backgroundMusic: book.backgroundMusic, + friendlyReminder: book.friendlyReminder, + draftSrtStyle: book.draftSrtStyle, + srtPath: book.srtPath, + audioPath: book.audioPath, + }) + } return successMessage({ backgroundMusic: book.backgroundMusic, friendlyReminder: book.friendlyReminder, diff --git a/src/main/Service/GPT/gpt.ts b/src/main/Service/GPT/gpt.ts new file mode 100644 index 0000000..eb8d028 --- /dev/null +++ b/src/main/Service/GPT/gpt.ts @@ -0,0 +1,178 @@ +import { isEmpty } from "lodash"; +import { gptDefine } from "../../../define/gptDefine"; +import axios from "axios"; + +/** + * 一些GPT相关的服务都在这边 + */ +export class GptService { + gptUrl: string = undefined + gptModel: string = undefined + gptApiKey: string = undefined + + + //#region GPT 设置 + + /** + * 获取GPT的所有的服务商 + * @param type 获取的类型,就是all + * @param callback 这个是个回调函数,干嘛的不知道 + * @returns + */ + private async GetGPTBusinessOption(type: string, callback: Function = null): Promise { + let res = await gptDefine.getGptDataByTypeAndProperty(type, "gpt_options", []); + if (res.code == 0) { + throw new Error(res.message) + } else { + if (callback) { + callback(res.data) + } + return res.data + } + } + + async RefreshGptSetting() { + let all_options = await this.GetGPTBusinessOption("all", (value) => value.gpt_url); + let index = all_options.findIndex(item => item.value == global.config.gpt_business && item.gpt_url) + if (index < 0) { + throw new Error("没有找到指定的GPT服务商的配置,请检查") + } + this.gptUrl = all_options[index].gpt_url; + this.gptApiKey = global.config.gpt_key; + this.gptModel = global.config.gpt_model; + } + + /** + * 初始化GPT的设置 + */ + async InitGptSetting(refresh = false) { + if (refresh) { + await this.RefreshGptSetting() + } else { + // 判断是不是存在必要信息 + if (isEmpty(this.gptUrl) || isEmpty(this.gptModel) || isEmpty(this.gptApiKey)) { + await this.RefreshGptSetting(); + } + } + } + + /** + * 适配一些请求体中的参数 + * @param data + * @param gpt_url + * @returns + */ + ModifyData(data: any, gpt_url: string = null) { + let res = data; + if (!gpt_url) { + gpt_url = this.gptUrl + } + if (gpt_url.includes("dashscope.aliyuncs.com")) { + res = { + "model": data.model, + "input": { + "messages": data.messages, + }, + "parameters": { + "result_format": "message" + } + } + } + return res; + } + + /** + * 适配返回来的数据 + * @param res 返回的数据 + * @param gpt_url 请求的URL + * @returns + */ + GetResponseContent(res: any, gpt_url: string = null) { + let content = ""; + if (!gpt_url) { + gpt_url = this.gptUrl + } + if (gpt_url.includes("dashscope.aliyuncs.com")) { + content = res.data.output.choices[0].message.content; + } else { + content = res.data.choices[0].message.content; + } + return content; + } + + //#endregion + + /** + * 发送GPT请求 + * @param {*} message 请求的信息 + * @param {*} gpt_url gpt的url,默认在global中取 + * @param {*} gpt_key gpt的key,默认在global中取 + * @param {*} gpt_model gpt的model,默认在global中取 + * @returns + */ + async FetchGpt(message: any, gpt_model: string = null, gpt_key: string = null, gpt_url: string = null): Promise { + try { + await this.InitGptSetting(); + let data = { + "model": gpt_model ? gpt_model : this.gptModel, + "messages": message + }; + + data = this.ModifyData(data, gpt_url); + let config = { + method: 'post', + maxBodyLength: Infinity, + url: gpt_url ? gpt_url : this.gptUrl, + headers: { + 'Authorization': `Bearer ${gpt_key ? gpt_key : this.gptApiKey}`, + 'Content-Type': 'application/json' + }, + data: JSON.stringify(data) + }; + + let res = await axios.request(config); + let content = this.GetResponseContent(res, this.gptUrl); + return content; + } catch (error) { + throw error; + } + } + + //#region 繁体中文 -> 简体中文 + + /** + * 将繁体中文转换为简体中文 + * @param traditionalText 繁体中文文本 + * @param apiKey Lai API的 Key + * @param baseUrl 请求的baseurl + * @returns + */ + async ChineseTraditionalToSimplified(traditionalText: string, apiKey: string, baseUrl: string = null): Promise { + try { + let message = [ + { + "role": "system", + "content": '我想让你充当中文繁体转简体专家,用简体中文100%还原繁体中文,不要加其他的联想,只把原有的繁体中文转换为简体中文,请检查所有信息是否准确,并在回答时保持简活,不需要任何其他反馈。' + }, { + "role": "user", + "content": '上研究生後,發現導師竟然是曾經網戀的前男友。' + }, { + "role": "assistant", + "content": '上研究生后,发现导师竟然是曾经网恋的前男友。' + }, { + "role": "user", + "content": traditionalText + } + ] + + let baseSubUrl = baseUrl ? (baseUrl.endsWith('/') ? baseUrl + 'v1/chat/completions' : baseUrl + '/v1/chat/completions') : null; + let url = baseSubUrl ? baseSubUrl : "https://api.laitool.cc/v1/chat/completions" + // 开始请求,这个默认是使用的是LAI API的gpt-4o-mini + let content = await this.FetchGpt(message, 'gpt-4o-mini', apiKey, url) + return content + } catch (error) { + throw error + } + } + //#endregion +} \ No newline at end of file diff --git a/src/main/Service/MJ/mj.ts b/src/main/Service/MJ/mj.ts index 278d185..d32c53a 100644 --- a/src/main/Service/MJ/mj.ts +++ b/src/main/Service/MJ/mj.ts @@ -7,7 +7,7 @@ import { CheckFolderExistsOrCreate, CopyFileOrFolder, JoinPath } from "../../../ import { define } from "../../../define/define" import { GetImageBase64, ImageSplit } from "../../../define/Tools/image"; import MJApi from "./mjApi" -import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus, BookType, DialogType, MJAction, MergeType, OperateBookType, TaskExecuteType } from "../../../define/enum/bookEnum"; +import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus, BookType, DialogType, MJAction, OperateBookType, TaskExecuteType } from "../../../define/enum/bookEnum"; import { DEFINE_STRING } from "../../../define/define_string"; import { MJ } from "../../../model/mj"; import { MJRespoonseType } from "../../../define/enum/mjEnum"; @@ -21,6 +21,7 @@ import { TaskScheduler } from "../taskScheduler"; import { BookService } from "../../../define/db/service/Book/bookService"; import { Tools } from "../../../main/tools" import { MJSettingService } from "../../../define/db/service/SoftWare/mjSettingService"; +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; import path from "path" const { v4: uuidv4 } = require('uuid') @@ -39,10 +40,12 @@ export class MJOpt { bookService: BookService tools: Tools; mjSettingService: MJSettingService; + bookServiceBasic: BookServiceBasic constructor() { this.imageStyle = new ImageStyle() this.taskScheduler = new TaskScheduler() this.tools = new Tools() + this.bookServiceBasic = new BookServiceBasic(); } async InitService() { if (!this.reverseBook) { @@ -208,7 +211,8 @@ export class MJOpt { this.bookTaskDetail.UpdateBookTaskDetail(task.bookTaskDetailId, { status: BookTaskStatus.REVERSE_DONE, - reversePrompt: reversePrompt + reversePrompt: reversePrompt, + gptPrompt: undefined }); task_res.prompt = JSON.stringify(reversePrompt); @@ -331,20 +335,35 @@ export class MJOpt { * @param id 合并的ID * @param mergeType 合并的类型 */ - async MergePrompt(id: string, mergeType: MergeType): Promise { + async MergePrompt(id: string, operateBookType: OperateBookType): Promise { try { await this.InitService() let bookTaskDetail = undefined as Book.SelectBookTaskDetail[]; let bookTask = undefined as Book.SelectBookTask; - if (mergeType == MergeType.BOOKTASK) { + if (operateBookType == OperateBookType.BOOKTASK) { bookTaskDetail = this.bookTaskDetail.GetBookTaskData({ bookTaskId: id }).data bookTask = this.bookTaskService.GetBookTaskDataById(id); - } else if (mergeType == MergeType.BOOKTASKDETAIL) { - bookTaskDetail = [this.bookTaskDetail.GetBookTaskDetailDataById(id)] + // 判断是不是有为空的 + let emptyName = [] as string[] + for (let i = 0; i < bookTaskDetail.length; i++) { + const element = bookTaskDetail[i]; + if (isEmpty(element.gptPrompt)) { + emptyName.push(element.name) + } + } + if (emptyName.length > 0) { + throw new Error("有空的提示词,请先推理") + } + } else if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + let tempBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(id); + if (isEmpty(tempBookTaskDetail.gptPrompt)) { + throw new Error("当前分镜没有推理提示词,请先生成") + } + bookTaskDetail = [tempBookTaskDetail] bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskDetail[0].bookTaskId); } else { throw new Error("未知的合并类型") @@ -371,7 +390,6 @@ export class MJOpt { for (let i = 0; i < bookTaskDetail.length; i++) { const element = bookTaskDetail[i]; - let promptStr = ''; for (let i = 0; i < promptSort.length; i++) { const element = promptSort[i]; diff --git a/src/main/Service/SD/sd.ts b/src/main/Service/SD/sd.ts index baa529e..d184dcb 100644 --- a/src/main/Service/SD/sd.ts +++ b/src/main/Service/SD/sd.ts @@ -1,4 +1,3 @@ -import { MergeType } from "../../../define/enum/bookEnum"; import { Book } from "../../../model/book"; import { GeneralResponse } from "../../../model/generalResponse"; import { checkStringValueAddSuffix, errorMessage, successMessage } from "../../Public/generalTools"; @@ -8,14 +7,19 @@ import { define } from '../../../define/define' import fs from "fs"; import { ImageStyleDefine } from "../../../define/iamgeStyleDefine"; import { ImageStyle } from "../Book/imageStyle"; +import { OperateBookType } from "../../../define/enum/bookEnum"; +import { isEmpty } from "lodash"; +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; const fspromise = fs.promises export class SDOpt { bookTaskDetailService: BookTaskDetailService bookTaskService: BookTaskService imageStyle: ImageStyle - constructor() { + bookServiceBasic: BookServiceBasic + constructor() { + this.bookServiceBasic = new BookServiceBasic() } @@ -34,22 +38,37 @@ export class SDOpt { /** * SD的提示词合并 * @param id 要处理的ID - * @param mergeType 合并的类型(用于判断是单个还是批量) + * @param operateBookType 合并的类型(用于判断是单个还是批量) * @returns */ - async MergePrompt(id: string, mergeType: MergeType): Promise { + async MergePrompt(id: string, operateBookType: OperateBookType): Promise { try { await this.InitService() let bookTaskDetail = undefined as Book.SelectBookTaskDetail[]; let bookTask = undefined as Book.SelectBookTask; - if (mergeType == MergeType.BOOKTASK) { + if (operateBookType == OperateBookType.BOOKTASK) { bookTaskDetail = this.bookTaskDetailService.GetBookTaskData({ bookTaskId: id }).data bookTask = this.bookTaskService.GetBookTaskDataById(id); - } else if (mergeType == MergeType.BOOKTASKDETAIL) { - bookTaskDetail = [this.bookTaskDetailService.GetBookTaskDetailDataById(id)]; + // 判断是不是有为空的 + let emptyName = [] as string[] + for (let i = 0; i < bookTaskDetail.length; i++) { + const element = bookTaskDetail[i]; + if (isEmpty(element.gptPrompt)) { + emptyName.push(element.name) + } + } + if (emptyName.length > 0) { + throw new Error("有空的提示词,请先推理") + } + } else if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + let tempBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(id); + if (isEmpty(tempBookTaskDetail.gptPrompt)) { + throw new Error("当前分镜没有推理提示词,请先生成") + } + bookTaskDetail = [tempBookTaskDetail]; bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskDetail[0].bookTaskId); } else { throw new Error("未知的合并类型") diff --git a/src/main/Service/ServiceBasic/bookServiceBasic.ts b/src/main/Service/ServiceBasic/bookServiceBasic.ts new file mode 100644 index 0000000..96e82cd --- /dev/null +++ b/src/main/Service/ServiceBasic/bookServiceBasic.ts @@ -0,0 +1,256 @@ +import { DEFINE_STRING } from "../../../define/define_string"; +import { GeneralResponse } from "../../../model/generalResponse"; +import { BookService } from "../../../define/db/service/Book/bookService"; +import { Book } from "../../../model/book"; +import { BookTaskService } from "../../../define/db/service/Book/bookTaskService"; +import { TaskScheduler } from "../taskScheduler"; +import { LoggerStatus, OtherData } from "../../../define/enum/softwareEnum"; +import { BookTaskDetailService } from "../../../define/db/service/Book/bookTaskDetailService"; +import { BookBackTaskListService } from "../../../define/db/service/Book/bookBackTaskListService"; +import { BookBackTaskType, BookTaskStatus, TaskExecuteType } from "../../../define/enum/bookEnum"; + +/** + * 该类中封装了小说的基础服务,主要是检查每次引入对应的服务类 + * 这边进行一个统一的调用,方便后续的维护 + */ +export class BookServiceBasic { + bookService: BookService + bookTaskService: BookTaskService; + taskScheduler: TaskScheduler + bookTaskDetailService: BookTaskDetailService + bookBackTaskListService: BookBackTaskListService + constructor() { + this.taskScheduler = new TaskScheduler() + } + async InitService() { + if (!this.bookService) { + this.bookService = await BookService.getInstance() + } + if (!this.bookTaskService) { + this.bookTaskService = await BookTaskService.getInstance() + } + if (!this.bookTaskDetailService) { + this.bookTaskDetailService = await BookTaskDetailService.getInstance() + } + if (!this.bookBackTaskListService) { + this.bookBackTaskListService = await BookBackTaskListService.getInstance() + } + } + + // 主动返回前端的消息 + sendReturnMessage(data: GeneralResponse.MessageResponse, message_name = DEFINE_STRING.BOOK.GET_COPYWRITING_RETURN) { + global.newWindow[0].win.webContents.send(message_name, data) + } + + //#region 小说相关的基础服务 + + /** + * 通过小说ID获取小说数据 + * @param bookId 小说ID + * @returns + */ + async GetBookDataById(bookId: string): Promise { + await this.InitService(); + let book = this.bookService.GetBookDataById(bookId); + if (book == null) { + let msg = '未找到对应的小说数据,请检查' + throw new Error(msg); + } + return book + } + + /** + * 更新小说指定ID的数据 + * @param bookId 小说ID + * @param data 小说要更新的数据 + */ + async UpdateBookData(bookId: string, data: Book.SelectBook): Promise { + await this.InitService(); + let res = this.bookService.UpdateBookData(bookId, data) + return res + } + + /** + * 删除指定的小说数据 + * @param bookId 需要删除的小说ID + */ + async DeleteBookData(bookId: string): Promise { + await this.InitService(); + this.bookService.DeleteBookData(bookId) + } + + //#endregion + + //#region 小说批次任务相关的基础服务 + + /** + * 通过小说ID获取小说批次任务数据 + * @param bookTaskId 小说批次任务ID + * @returns + */ + async GetBookTaskDataId(bookTaskId: string): Promise { + await this.InitService(); + let bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskId); + if (bookTask == null) { + let msg = '未找到对应的小说批次任务数据,请检查'; + throw new Error(msg); + } + return bookTask + } + + /** + * 通过查询条件获取小说批次任务数据 + * @param bookTaskCondition 小说批次的查询条件 + */ + async GetBookTaskData(bookTaskCondition: Book.QueryBookTaskCondition): Promise<{ bookTasks: Book.SelectBookTask[], total: number }> { + await this.InitService(); + let bookTasks = this.bookTaskService.GetBookTaskData(bookTaskCondition) + if (bookTasks.data.bookTasks.length <= 0 || bookTasks.data.total <= 0) { + throw new Error("未找到对应的小说批次任务数据,请检查") + } + return bookTasks.data + } + + /** + * 更新小说批次任务的数据 + * @param bookTaskId 小说批次任务ID + * @param data + */ + async UpdetedBookTaskData(bookTaskId: string, data: Book.SelectBookTask): Promise { + await this.InitService(); + this.bookTaskService.UpdetedBookTaskData(bookTaskId, data) + } + + /** + * 删除指定的小说批次任务的数据 + * @param bookTaskId 需要删除的指定的小说批次任务ID + */ + async DeleteBookTaskData(bookTaskId: string): Promise { + await this.InitService(); + this.bookTaskService.DeleteBookTask(bookTaskId) + } + //#endregion + + //#region 小说批次任务对应的分镜的相关的基础服务 + + /** + * 获取指定的小说批次任务分镜数据,通过分镜ID + * @param bookTaskDetailId 小说批次任务分镜ID + * @returns + */ + async GetBookTaskDetailDataById(bookTaskDetailId: string): Promise { + await this.InitService(); + let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId) + if (bookTaskDetail == null) { + let msg = "未找到对应的小说批次任务分镜数据,请检查" + global.logger.error('BookServiceBasic_GetBookTaskDetailDataById', msg); + throw new Error("未找到对应的小说批次任务分镜数据,请检查") + } + return bookTaskDetail + } + + /** + * 获取指定的小说批次任务分镜数据,通过查询条件 + * @param condition + */ + async GetBookTaskDetailData(condition: Book.QueryBookTaskDetailCondition): Promise { + await this.InitService(); + let bookTaskDetails = this.bookTaskDetailService.GetBookTaskData(condition) + if (bookTaskDetails.data.length <= 0) { + let msg = "未找到对应的小说批次任务分镜数据,请检查"; + throw new Error(msg) + } + return bookTaskDetails.data + } + + /** + * 修改小说的分镜详细数据,通过ID + * @param bookTaskDetailId 小说分镜的ID + * @param data 要修改的数据,是个对象,会修改全部 + */ + async UpdateBookTaskDetail(bookTaskDetailId: string, data: Book.SelectBookTaskDetail): Promise { + await this.InitService(); + let res = this.bookTaskDetailService.UpdateBookTaskDetail(bookTaskDetailId, data) + return res + } + + /** + * 更行小说批次的状态 + * @param bookTaskId 小说批次的ID + * @param status 修改后的状态 + * @param errorMsg 错误消息 + */ + async UpdateBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg: string | null = null): Promise { + await this.InitService(); + this.bookTaskService.UpdateBookTaskStatus(bookTaskId, status, errorMsg); + } + + + /** + * 删除指定的小说分镜的反推提示词 + * @param bookTaskDetail + */ + async DeleteBookTaskDetailReversePromptById(bookTaskDetailId: string): Promise { + await this.InitService(); + this.bookTaskDetailService.DeleteBookTaskDetailReversePromptById(bookTaskDetailId) + } + + //#endregion + + //#region 小说后台任务相关操作 + + /** + * 添加一个后台任务 + * @param bookId 小说ID + * @param taskType 后台任务类型 + * @param executeType 执行的类型,是不是自动执行 + * @param bookTaskId 小说批次任务ID + * @param bookTaskDetailId 小说批次任务分镜ID + */ + async AddBookBackTask(bookId: string, + taskType: BookBackTaskType, + executeType = TaskExecuteType.AUTO, + bookTaskId = null, + bookTaskDetailId = null): Promise { + + await this.InitService(); + let res = this.bookBackTaskListService.AddBookBackTask(bookId, taskType, executeType, bookTaskId, bookTaskDetailId) + if (res.code == 0) { + throw new Error(res.message) + } + return res.data as TaskModal.Task + } + //#endregion + + + + /** + * 通过小说ID和小说批次任务ID获取小说和小说批次任务数据 + * @param bookId 小说ID + * @param bookTaskName 小说批次的名字,或者是小说批次任务的ID + * @returns + */ + async GetBookAndTask(bookId: string, bookTaskName: string) { + await this.InitService(); + let book = this.bookService.GetBookDataById(bookId) + if (book == null) { + throw new Error("查找小说数据失败"); + } + // 获取小说对应的批次任务数据,默认初始化为第一个 + let condition = { + bookId: bookId + } as Book.QueryBookBackTaskCondition + if (bookTaskName == "output_00001") { + condition["name"] = bookTaskName + } else { + condition["id"] = bookTaskName + } + let bookTaskRes = this.bookTaskService.GetBookTaskData(condition) + if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) { + let msg = "没有找到对应的小说批次任务数据" + this.taskScheduler.AddLogToDB(bookId, book.type, msg, OtherData.DEFAULT, LoggerStatus.FAIL) + throw new Error(msg) + } + return { book: book as Book.SelectBook, bookTask: bookTaskRes.data.bookTasks[0] as Book.SelectBookTask } + } +} diff --git a/src/main/Service/ServiceBasic/softwareServiceBasic.ts b/src/main/Service/ServiceBasic/softwareServiceBasic.ts new file mode 100644 index 0000000..3637906 --- /dev/null +++ b/src/main/Service/ServiceBasic/softwareServiceBasic.ts @@ -0,0 +1,76 @@ +import { SoftwareService } from '../../../define/db/service/SoftWare/softwareService'; + + +export class SoftWareServiceBasic { + softwareService: SoftwareService + constructor() { } + + async InitService() { + if (!this.softwareService) { + this.softwareService = await SoftwareService.getInstance() + } + } + + //#region software相关的基础服务 + + /** + * 更新软件配置信息 + * @param software + */ + async UpdateSoftware(software: SoftwareSettingModel.SoftwareSetting): Promise { + await this.InitService(); + this.softwareService.UpdateSoftware(software) + } + + /** + * 添加新的软件配置信息 + * @param software + */ + async AddSfotware(software: SoftwareSettingModel.SoftwareSetting): Promise { + await this.InitService(); + this.softwareService.AddSfotware(software); + } + + /** + * 获取软件配置信息 + * 软件配置信息只有一个,所以直接返回第一个 + */ + async GetSoftwareData(): Promise { + await this.InitService(); + let softwares = this.softwareService.GetSoftwareData(); + if (softwares.data.length <= 0) { + let msg = "未找到软件配置信息,请检查"; + global.logger.error( + 'SoftWareServiceBasic_GetSoftwareData', + '获取软件的基础设置失败 ,错误信息如下:' + msg + ) + throw new Error(msg) + } + return softwares.data[0] + } + + /** + * 获取软件配置指定的属性数据 + * @param property 属性的名称 + * @returns + */ + async GetSoftWarePropertyData(property: string): Promise { + await this.InitService(); + let res = this.softwareService.GetSoftWarePropertyData(property) + return res + } + + + /** + * 保存软件的指定属性的设置信息,数据一般是字符串 + * @param property 要保存的属性的名称 + * @param data 要保存的数据信息 + */ + async SaveSoftwarePropertyData(property: string, data: string): Promise { + await this.InitService(); + this.softwareService.SaveSoftwarePropertyData(property, data) + } + + //#endregion + +} \ No newline at end of file diff --git a/src/main/Service/subtitle.ts b/src/main/Service/Subtitle/subtitle.ts similarity index 55% rename from src/main/Service/subtitle.ts rename to src/main/Service/Subtitle/subtitle.ts index 97e0261..465aa8b 100644 --- a/src/main/Service/subtitle.ts +++ b/src/main/Service/Subtitle/subtitle.ts @@ -1,45 +1,47 @@ import { isEmpty } from 'lodash' -import { BookService } from '../../define/db/service/Book/bookService' -import { errorMessage, successMessage } from '../Public/generalTools' -import { FfmpegOptions } from './ffmpegOptions' -import { SubtitleSavePositionType } from '../../define/enum/waterMarkAndSubtitle' -import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService' -import { define } from '../../define/define' +import { errorMessage, successMessage } from '../../Public/generalTools' +import { FfmpegOptions } from '../ffmpegOptions' +import { SubtitleSavePositionType } from '../../../define/enum/waterMarkAndSubtitle' +import { define } from '../../../define/define' import path from 'path' import { CheckFileOrDirExist, CheckFolderExistsOrCreate, DeleteFolderAllFile, GetFilesWithExtensions -} from '../../define/Tools/file' +} from '../../../define/Tools/file' import { shell } from 'electron' -import { Book } from '../../model/book' +import { Book } from '../../../model/book' import fs from 'fs' +import { GeneralResponse } from '../../../model/generalResponse' +import { BookServiceBasic } from '../ServiceBasic/bookServiceBasic' +import { LoggerStatus, OtherData, ResponseMessageType } from '../../../define/enum/softwareEnum' +import { TaskScheduler } from '../taskScheduler' +import { SubtitleModel } from '../../../model/subtitle' +import { BookTaskStatus, OperateBookType } from '../../../define/enum/bookEnum' +import axios from 'axios' +import { GptService } from '../GPT/gpt' +import FormData from 'form-data' const util = require('util') const { exec } = require('child_process') const execAsync = util.promisify(exec) const fspromises = fs.promises + /** * 去除水印和获取字幕相关操作 */ export class Subtitle { - bookService: BookService - bookTaskDetailService: BookTaskDetailService ffmpegOptions: FfmpegOptions + bookServiceBasic: BookServiceBasic + taskScheduler: TaskScheduler + gptService: GptService - constructor() { } - - async InitService() { - if (!this.bookService) { - this.bookService = await BookService.getInstance() - } - if (!this.bookTaskDetailService) { - this.bookTaskDetailService = await BookTaskDetailService.getInstance() - } - if (!this.ffmpegOptions) { - this.ffmpegOptions = new FfmpegOptions() - } + constructor() { + this.bookServiceBasic = new BookServiceBasic() + this.taskScheduler = new TaskScheduler() + this.ffmpegOptions = new FfmpegOptions() + this.gptService = new GptService() } //#region 通用方法 @@ -50,7 +52,7 @@ export class Subtitle { * @param {*} framesPerSecond 每秒截取多少帧 * @returns */ - GenerateFrameTimes(videoDurationMs, framesPerSecond) { + GenerateFrameTimes(videoDurationMs: number, framesPerSecond: number): number[] { // 直接使用视频总时长(毫秒),不进行向下取整 const videoDurationSec = videoDurationMs / 1000 @@ -67,117 +69,70 @@ export class Subtitle { let timePoint = Math.min(Math.round(interval * i), videoDurationMs) frameTimes.push(timePoint) } - return frameTimes } + /** + * 加载指定的的小说相关的所有的数据 + * @param bookId 小说ID + * @param bookTaskId 小说任务ID + * @returns + */ + async GetBookAllData(bookId: string, bookTaskId: string = null): Promise<{ book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetails: Book.SelectBookTaskDetail[] }> { + let { book, bookTask } = await this.bookServiceBasic.GetBookAndTask(bookId, bookTaskId ? bookTaskId : 'output_00001') + if (isEmpty(book.subtitlePosition)) { + throw new Error("请先设置小说的字幕位置") + } + // 获取所有的分镜数据 + let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({ + bookId: bookId, + bookTaskId: bookTask.id + }) + if (bookTaskDetails.length <= 0) { + throw new Error("没有找到对应的分镜数据,请先执行对应的操作") + } + return { book, bookTask, bookTaskDetails } + } + + /** + * 通用的小说获取分案的返回方法 + * @param content 获取的文案内容 + * @param book 小说实体类 + * @param bookTask 小说任务实体类 + * @param bookTaskDetail 小说任务分镜实体类 + */ + async GetSubtitleLoggerAndResponse(content: string, progress: GeneralResponse.ProgressResponse, book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail): Promise { + // 修改数据 + await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, { + word: content, + afterGpt: content + }); + + // let res = await this.basicReverse.GetCopywritingFunc(book, item); + // 将当前的数据实时返回,前端进行修改 + this.bookServiceBasic.sendReturnMessage({ + code: 1, + id: bookTaskDetail.id, + type: ResponseMessageType.GET_TEXT, + data: { + content: content, + progress: progress + } as GeneralResponse.SubtitleProgressResponse // 返回识别到的文案 + }) + + // 添加日志 + await this.taskScheduler.AddLogToDB( + book.id, + book.type, + `${bookTaskDetail.name} 识别文案成功`, + bookTask.id, + LoggerStatus.SUCCESS + ) + } + //#endregion - /** - * 获取当前视频中所有的字幕信息 - * @param {*} value 需要的参数的对象,包含下面的参数 - * @param {*} value.id 小说ID/小说分镜详细信息ID/null - * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) - * @param {*} value.videoPath 视频路径 - * @param {*} value.subtitlePosition 字幕位置信息 - */ - async GetVideoFrameText(value: Book.GetVideoFrameTextParams) { - try { - await this.InitService() - let videoPath - let tempImageFolder - let position - if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { - let bookRes = this.bookService.GetBookDataById(value.id) - if (bookRes == null) { - throw new Error('没有找到小说对应的的视频地址') - } - let book = bookRes - tempImageFolder = path.join(define.project_path, `${book.id}/data/subtitle/${book.id}/temp`) - if (isEmpty(book.subtitlePosition)) { - throw new Error('请先保存位置信息') - } - position = JSON.parse(book.subtitlePosition) - videoPath = book.oldVideoPath - } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(value.id); - if (bookTaskDetail == null) { - throw new Error("没有找到小说分镜详细信息") - } - - tempImageFolder = path.join(define.project_path, `${bookTaskDetail.bookId}/data/subtitle/${bookTaskDetail.name}_${bookTaskDetail.id}/temp`) - if (isEmpty(value.subtitlePosition)) { - throw new Error('请先保存位置信息') - } - position = JSON.parse(value.subtitlePosition) - videoPath = bookTaskDetail.videoPath - } else { - throw new Error("不支持的操作"); - } - - await CheckFolderExistsOrCreate(tempImageFolder) - // 判断文件夹是不是存在,存在的话,将里面的所有文件删除 - await DeleteFolderAllFile(tempImageFolder) - - // 将视频进行抽帧,(目前是每秒1帧,时间小于一秒,抽一帧) - let getDurationRes = await this.ffmpegOptions.FfmpegGetVideoDuration(videoPath) - if (getDurationRes.code == 0) { - throw new Error(getDurationRes.message) - } - let videoDuration = getDurationRes.data - let frameTime = this.GenerateFrameTimes(videoDuration, 1) - for (let i = 0; i < frameTime.length; i++) { - const item = frameTime[i]; - let name = i.toString().padStart(6, '0') - let imagePath = path.join(tempImageFolder, `frame_${name}.png`) - // 开始抽帧 - let res = await this.ffmpegOptions.FfmpegGetVideoFramdAndClip( - videoPath, - item, - imagePath, - position - ) - if (res.code == 0) { - throw new Error(res.message) - } - } - - // 开始识别 - let textRes = await this.GetCurrentFrameText({ - id: value.id, - type: value.type, - imageFolder: tempImageFolder - }) - - let allTextData = [] as string[] - // 开始获取所有的数据 - let jsonPaths = await GetFilesWithExtensions(tempImageFolder, ['.json']) - for (let i = 0; i < jsonPaths.length; i++) { - const element = jsonPaths[i] - // 开始拼接 - let texts = JSON.parse(await fspromises.readFile(element, 'utf-8')) - for (let j = 0; j < texts.length; j++) { - const text = texts[j][1][0] - allTextData.includes(text) ? null : allTextData.push(text) - } - } - - console.log(allTextData.join(',')) - // 这边计算相似度,返回过于相似的数据 - // let res = await RemoveSimilarTexts(allTextData) - - return successMessage( - allTextData.join(','), - '获取视频的的文案信息成功', - 'WatermarkAndSubtitle_GetVideoFrameText' - ) - } catch (error) { - return errorMessage( - '提取视频的的文案信息失败,错误消息如下:' + error.toString(), - 'WatermarkAndSubtitle_GetCurrentFrameText' - ) - } - } + //#region 获取字幕位置信息相关操作 /** * 获取当前帧的文字信息 @@ -185,9 +140,8 @@ export class Subtitle { * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) */ - async GetCurrentFrameText(value) { + async GetCurrentFrameText(value: { id: any; type?: SubtitleSavePositionType; imageFolder: any }): Promise { try { - await this.InitService() let iamgePaths = [] let imageFolder = value.imageFolder ? value.imageFolder @@ -249,7 +203,7 @@ export class Subtitle { * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) */ - async OpenBookSubtitlePositionScreenshot(value) { + async OpenBookSubtitlePositionScreenshot(value: { type: SubtitleSavePositionType; id: any }) { try { let folder if ( @@ -291,9 +245,8 @@ export class Subtitle { * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) * @returns */ - async SaveBookSubtitlePosition(value) { + async SaveBookSubtitlePosition(value: { type: SubtitleSavePositionType; id: string; bookSubtitlePosition: string | any[]; currentTime: number }) { try { - await this.InitService() let saveData = [] let videoPath let outImagePath @@ -307,7 +260,7 @@ export class Subtitle { throw new Error('小说ID不能为空') } // 获取指定的小说 - let bookRes = this.bookService.GetBookDataById(value.id) + let bookRes = await this.bookServiceBasic.GetBookDataById(value.id) if (bookRes == null) { throw new Error('没有找到小说信息') } @@ -353,19 +306,16 @@ export class Subtitle { } // 数据保存 - let saveRes = await this.bookService.UpdateBookData(value.id, { + let saveRes = await this.bookServiceBasic.UpdateBookData(value.id, { subtitlePosition: JSON.stringify(saveData) }) - if (saveRes.code == 0) { - throw new Error(saveRes.message) - } } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { // 小说分镜详细信息保存 if (value.id == null) { throw new Error('小说分镜详细信息ID不能为空') } // 获取指定的小说分镜详细信息 - let bookStoryboard = this.bookTaskDetailService.GetBookTaskDetailDataById(value.id) + let bookStoryboard = await this.bookServiceBasic.GetBookTaskDetailDataById(value.id) if (bookStoryboard == null) { throw new Error('没有找到小说分镜信息') } @@ -414,12 +364,9 @@ export class Subtitle { }) } // 数据保存 - let saveRes = this.bookTaskDetailService.UpdateBookTaskDetail(bookStoryboard.id, { + let saveRes = this.bookServiceBasic.UpdateBookTaskDetail(bookStoryboard.id, { subtitlePosition: JSON.stringify(saveData) }) - if (saveRes.code == 0) { - throw new Error(saveRes.message) - } } // 开始设置裁剪出来的图片位置 @@ -445,4 +392,272 @@ export class Subtitle { ) } } + //#endregion + + //#region 本地OCR识别字幕相关操作 + + /** + * 获取当前视频中所有的字幕信息 + * @param {*} value 需要的参数的对象,包含下面的参数 + * @param {*} value.id 小说ID/小说分镜详细信息ID/null + * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) + * @param {*} value.videoPath 视频路径 + * @param {*} value.subtitlePosition 字幕位置信息 + */ + async GetVideoFrameText(value: Book.GetVideoFrameTextParams): Promise { + try { + let videoPath = undefined + let tempImageFolder = undefined + let position = undefined + if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { + let bookRes = await this.bookServiceBasic.GetBookDataById(value.id) + if (bookRes == null) { + throw new Error('没有找到小说对应的的视频地址') + } + let book = bookRes + tempImageFolder = path.join(define.project_path, `${book.id}/data/subtitle/${book.id}/temp`) + if (isEmpty(book.subtitlePosition)) { + throw new Error('请先保存位置信息') + } + position = JSON.parse(book.subtitlePosition) + videoPath = book.oldVideoPath + } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(value.id); + if (bookTaskDetail == null) { + throw new Error("没有找到小说分镜详细信息") + } + + tempImageFolder = path.join(define.project_path, `${bookTaskDetail.bookId}/data/subtitle/${bookTaskDetail.name}_${bookTaskDetail.id}/temp`) + if (isEmpty(value.subtitlePosition)) { + throw new Error('请先保存位置信息') + } + position = JSON.parse(value.subtitlePosition) + videoPath = bookTaskDetail.videoPath + } else { + throw new Error("不支持的操作"); + } + + await CheckFolderExistsOrCreate(tempImageFolder) + // 判断文件夹是不是存在,存在的话,将里面的所有文件删除 + await DeleteFolderAllFile(tempImageFolder) + + // 将视频进行抽帧,(目前是每秒1帧,时间小于一秒,抽一帧) + let getDurationRes = await this.ffmpegOptions.FfmpegGetVideoDuration(videoPath) + if (getDurationRes.code == 0) { + throw new Error(getDurationRes.message) + } + let videoDuration = getDurationRes.data + let frameTime = this.GenerateFrameTimes(videoDuration, 1) + for (let i = 0; i < frameTime.length; i++) { + const item = frameTime[i]; + let name = i.toString().padStart(6, '0') + let imagePath = path.join(tempImageFolder, `frame_${name}.png`) + // 开始抽帧 + let res = await this.ffmpegOptions.FfmpegGetVideoFramdAndClip( + videoPath, + item, + imagePath, + position + ) + if (res.code == 0) { + throw new Error(res.message) + } + } + + // 开始识别 + let textRes = await this.GetCurrentFrameText({ + id: value.id, + type: value.type, + imageFolder: tempImageFolder + }) + + let allTextData = [] as string[] + // 开始获取所有的数据 + let jsonPaths = await GetFilesWithExtensions(tempImageFolder, ['.json']) + for (let i = 0; i < jsonPaths.length; i++) { + const element = jsonPaths[i] + // 开始拼接 + let texts = JSON.parse(await fspromises.readFile(element, 'utf-8')) + for (let j = 0; j < texts.length; j++) { + const text = texts[j][1][0] + allTextData.includes(text) ? null : allTextData.push(text) + } + } + // 这边计算相似度,返回过于相似的数据 + // let res = await RemoveSimilarTexts(allTextData) + + return successMessage( + allTextData.join(','), + '获取视频的的文案信息成功', + 'WatermarkAndSubtitle_GetVideoFrameText' + ) + } catch (error) { + return errorMessage( + '提取视频的的文案信息失败,错误消息如下:' + error.toString(), + 'WatermarkAndSubtitle_GetCurrentFrameText' + ) + } + } + + /** + * 使用本地OCR识别字幕文案 + * @param bookId 小说ID + * @param bookTaskId 小说任务ID + * @returns + */ + async GetCopywritingByLocalOcr(book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetails: Book.SelectBookTaskDetail[]): Promise { + try { + for (let i = 0; i < bookTaskDetails.length; i++) { + const item = bookTaskDetails[i]; + let res = await this.GetVideoFrameText({ + id: item.id, + videoPath: item.videoPath, + type: SubtitleSavePositionType.STORYBOARD_VIDEO, + subtitlePosition: book.subtitlePosition + }) + if (res.code == 0) { + throw new Error(res.message) + } + // 修改数据,并返回 + await this.GetSubtitleLoggerAndResponse(res.data, { + total: bookTaskDetails.length, + current: i + 1 + }, book, bookTask, item) + } + return successMessage(null, "识别是所有文案成功", "Subtitle_GetCopywriting") + } catch (error) { + return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'Subtitle_GetCopywriting') + } + } + + //#endregion + + //#region Lai_WHISPER识别字幕相关操作 + + /** + * 单个分离音频的方法 + * @param book 小说数据 + * @param bookTask 小说任务数据 + * @param bookTaskDetail 小说任务详细信息数据 + * @returns + */ + async SplitAudio(book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail) { + // 开始分离音频 + let videoPath = bookTaskDetail.videoPath + let audioPath = path.join(path.dirname(videoPath), bookTaskDetail.name + '.mp3'); + let audioRes = await this.ffmpegOptions.FfmpegExtractAudio(bookTaskDetail.videoPath, audioPath) + if (audioRes.code == 0) { + let errorMessage = `分离音频失败,错误信息如下:${audioRes.message}` + await this.bookServiceBasic.UpdateBookTaskStatus( + bookTask.id, + BookTaskStatus.AUDIO_FAIL, + errorMessage + ) + throw new Error(audioRes.message) + } + this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, { + audioPath: path.relative(define.project_path, audioPath) + }) + + // 推送成功消息 + await this.taskScheduler.AddLogToDB( + book.id, + book.type, + `${bookTaskDetail.name}分离音频成功,输出地址:${audioPath}`, + OtherData.DEFAULT, + LoggerStatus.SUCCESS + ) + // 修改状态为分离音频成功 + this.bookServiceBasic.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.AUDIO_DONE) + return audioPath; + } + + /** + * 使用LAI Whisper 进行文本识别,然后将繁体转换为简体 + * @param audioPath 要识别的音频地址 + * @param subtitleSetting 识别字幕设置 + */ + async LaiWhisperApi(audioPath: string, subtitleSetting: SubtitleModel.subtitleSettingModel): Promise { + // 开始调用LAI API识别 + let formdata = new FormData() + formdata.append("file", fs.createReadStream(audioPath)); // 如果是Node.js环境,可以使用fs.createReadStream方法 + formdata.append("model", "whisper-1"); + formdata.append("response_format", "srt"); + formdata.append("temperature", "0"); + formdata.append("language", "zh"); + formdata.append("prompt", isEmpty(subtitleSetting.laiWhisper.prompt) ? "eiusmod nulla" : subtitleSetting.laiWhisper.prompt); + let url = subtitleSetting.laiWhisper.url + if (!url.endsWith('/')) { + url = url + '/' + } + const config = { + method: 'post', + url: url + 'v1/audio/transcriptions', + headers: { + 'Accept': 'application/json', + 'Authorization': subtitleSetting.laiWhisper.apiKey, + 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)', + 'Content-Type': 'multipart/form-data', + ...formdata.getHeaders() // 在Node.js环境中需要添加这一行 + }, + data: formdata + }; + + let res = await axios(config) + let text = res.data.text; + // 但是这边是繁体,需要转化为简体 + let simpleText = await this.gptService.ChineseTraditionalToSimplified(text, subtitleSetting.laiWhisper.apiKey, url); + + console.log(res.data) + return simpleText; + } + + /** + * 使用LAI Whisper识别字幕 + * @param bookId 小说ID + * @param bookTaskId 小说任务ID + * @param subtitleSetting 提取文案相关设置 + * @returns + */ + async GetCopywritingByLaiWhisper(book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetails: Book.SelectBookTaskDetail[], subtitleSetting: SubtitleModel.subtitleSettingModel): Promise { + try { + + let emptyVideoPaths = [] as string[] + for (let i = 0; i < bookTaskDetails.length; i++) { + const element = bookTaskDetails[i]; + // 将所有的分镜视频音频分开 + if (isEmpty(element.videoPath)) { + emptyVideoPaths.push(element.name) + } + } + if (emptyVideoPaths.length > 0) { + throw new Error(`以下分镜视频没有找到对应的视频路径:${emptyVideoPaths.join(",")} \n 请先计算分镜`) + } + // 拆分音频和视频 + for (let i = 0; i < bookTaskDetails.length; i++) { + const bookTaskDetail = bookTaskDetails[i]; + // 开始分离音频 + let audioPath = await this.SplitAudio(book, bookTask, bookTaskDetail) + let fileExist = await CheckFileOrDirExist(audioPath) + if (!fileExist) { + throw new Error('没有找到对应的音频文件'); + } + // 开始调用LAI API识别 + let content = await this.LaiWhisperApi(audioPath, subtitleSetting); + // 向前端发送数据 + await this.GetSubtitleLoggerAndResponse(content, { + total: bookTaskDetails.length, + current: i + 1 + }, book, bookTask, bookTaskDetail) + } + return successMessage( + null, + `所有音频识别成功`, + 'Subtitle_GetCopywritingByLaiWhisper' + ) + } catch (error) { + return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'Subtitle_GetCopywritingByLaiWhisper') + } + } + //#endregion } diff --git a/src/main/Service/Subtitle/subtitleService.ts b/src/main/Service/Subtitle/subtitleService.ts new file mode 100644 index 0000000..752c874 --- /dev/null +++ b/src/main/Service/Subtitle/subtitleService.ts @@ -0,0 +1,227 @@ +import { isEmpty } from "lodash"; +import { GetSubtitleType, SubtitleSavePositionType } from "../../../define/enum/waterMarkAndSubtitle" +import { errorMessage, successMessage } from "../../Public/generalTools" +import { SoftWareServiceBasic } from "../ServiceBasic/softwareServiceBasic" +import { ValidateJson } from "../../../define/Tools/validate"; +import { GeneralResponse } from "../../../model/generalResponse"; +import { SubtitleModel } from "../../../model/subtitle"; +import { define } from '../../../define/define' +import path from 'path' +import fs from 'fs' +import { CheckFileOrDirExist } from "../../../define/Tools/file"; +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; +import { Subtitle } from "./subtitle"; +import { LoggerStatus, ResponseMessageType } from "../../../define/enum/softwareEnum"; +import { TaskScheduler } from "../taskScheduler"; +import { OperationType } from "realm/dist/public-types/internal"; +import { OperateBookType } from "../../../define/enum/bookEnum"; +import { Book } from "../../../model/book"; + +export class SubtitleService { + softWareServiceBasic: SoftWareServiceBasic + bookServiceBasic: BookServiceBasic + subtitle: Subtitle + taskScheduler: TaskScheduler + constructor() { + this.softWareServiceBasic = new SoftWareServiceBasic(); + this.bookServiceBasic = new BookServiceBasic(); + this.subtitle = new Subtitle(); + } + + //#region 设置相关的方法 + + /** + * 初始化字幕设置 + */ + async InitSubtitleSetting(): Promise { + let defauleSetting = { + selectModel: GetSubtitleType.LAI_WHISPER, + laiWhisper: { + url: 'https://api.laitool.cc/', + apiKey: '你的LAI API KEY', + syncGPTAPIKey: false, + prompt: undefined + } + } as SubtitleModel.subtitleSettingModel + await this.softWareServiceBasic.SaveSoftwarePropertyData("subtitleSetting", JSON.stringify(defauleSetting)); + return defauleSetting + } + + /** + * 获取提起字幕的设置 + */ + async GetSubtitleSetting(): Promise { + try { + let subtitleSetting = undefined as SubtitleModel.subtitleSettingModel + let subtitleSettingString = await this.softWareServiceBasic.GetSoftWarePropertyData('subtitleSetting'); + if (isEmpty(subtitleSettingString)) { + // 初始化 + subtitleSetting = await this.InitSubtitleSetting(); + } else { + if (ValidateJson(subtitleSettingString)) { + subtitleSetting = JSON.parse(subtitleSettingString) + } else { + throw new Error("提起字幕设置解析失败,请重置后重新配置") + } + } + return successMessage(subtitleSetting, '获取提取字幕设置成功', "SubtitleService_GetSubtitleSetting") + } catch (error) { + return errorMessage("获取字幕设置失败,失败信息如下:" + error.message, "SubtitleService_GetSubtitleSetting") + } + } + + /** + * 重置识别字幕设置 + */ + async ResetSubtitleSetting(): Promise { + try { + let subtitleSetting = await this.InitSubtitleSetting(); + return successMessage(subtitleSetting, "重置字幕设置成功", "SubtitleService_ResetSubtitleSetting") + } catch (error) { + return errorMessage("重置字幕设置失败,失败信息如下:" + error.message, "SubtitleService_ResetSubtitleSetting") + } + } + + /** + * 保存提取字幕设置,并作相应的一些简单的检查 + * @param subtitleSetting 要保存的数据结构体 + */ + async SaveSubtitleSetting(subtitleSetting: SubtitleModel.subtitleSettingModel): Promise { + try { + // 判断模式,通过不同的模式判断是不是又必要检查 + if (subtitleSetting.selectModel == GetSubtitleType.LOCAL_OCR) { + let localOcrPath = path.join(define.scripts_path, 'LaiOcr/LaiOcr.exe'); + let fileIsExists = await CheckFileOrDirExist(localOcrPath); + if (!fileIsExists) { + throw new Error("当前模式未本地OCR,但是没有检查到对应的执行文件,请查看教程,安装对应的拓展"); + } + } else if (subtitleSetting.selectModel == GetSubtitleType.LOCAL_WHISPER) { + // let localWhisper = path.join(define.scripts_path,'') + // 这个好像没有什么可以检查的 + } else if (subtitleSetting.selectModel == GetSubtitleType.LAI_WHISPER) { + // 判断是不是laitool的,不是的话报错 + if (!subtitleSetting.laiWhisper.url.includes('laitool')) { + throw new Error('该模式只能试用LAI API的接口请求'); + } + if (isEmpty(subtitleSetting.laiWhisper.apiKey)) { + throw new Error("当前模式为LAI API的接口请求,请输入LAI API KEY") + } + if (isEmpty(subtitleSetting.laiWhisper.url)) { + throw new Error("当前模式为LAI API的接口请求,请输入LAI API URL") + } + } else { + throw new Error("未知的识别字幕模式") + } + + // 检查做完,开始保存数据 + await this.softWareServiceBasic.SaveSoftwarePropertyData('subtitleSetting', JSON.stringify(subtitleSetting)) + return successMessage(null, "保存提取文案设置成功", "SubtitleService_SaveSubtitleSetting"); + } catch (error) { + return errorMessage("保存提取文案设置失败,失败信息如下:" + error.message, "SubtitleService_SaveSubtitleSetting") + } + } + //#endregion + + + + //#region 语音转文案或者是字幕识别 + /** + * 反推提取文案 + */ + async GetCopywriting(bookId: string, bookTaskId: string, operateBookType: OperateBookType): Promise { + try { + let subtitleSettingRes = await this.GetSubtitleSetting(); + if (subtitleSettingRes.code == 0) { + throw new Error(subtitleSettingRes.message) + } + let subtitleSetting = subtitleSettingRes.data as SubtitleModel.subtitleSettingModel; + let res = undefined as GeneralResponse.ErrorItem | GeneralResponse.SuccessItem + + let bookTaskDetails = undefined as Book.SelectBookTaskDetail[] + + let tempBookTaskId = bookTaskId + if (operateBookType == OperateBookType.BOOKTASK) { + bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({ + bookId: bookId, + bookTaskId: bookTaskId + }) + } else if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + let tempBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskId) + tempBookTaskId = tempBookTaskDetail.bookTaskId + bookTaskDetails = [tempBookTaskDetail] + } else { + throw new Error("未知的操作类型") + } + if (bookTaskDetails.length <= 0) { + throw new Error("分镜信息不存在"); + } + let { book, bookTask } = await this.bookServiceBasic.GetBookAndTask(bookId, tempBookTaskId) + + switch (subtitleSetting.selectModel) { + case GetSubtitleType.LOCAL_OCR: + res = await this.subtitle.GetCopywritingByLocalOcr(book, bookTask, bookTaskDetails) + break; + case GetSubtitleType.LOCAL_WHISPER: + throw new Error("本地Whisper暂时不支持") + break; + case GetSubtitleType.LAI_WHISPER: + res = await this.subtitle.GetCopywritingByLaiWhisper(book, bookTask, bookTaskDetails, subtitleSetting) + break; + default: + throw new Error("未知的识别字幕模式") + } + if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskId) + return successMessage(bookTaskDetail.afterGpt, "获取文案成功", "ReverseBook_GetCopywriting") + } else { + return res + } + } catch (error) { + return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'ReverseBook_GetCopywriting') + } + } + + + /** + * 导出指定导出指定小说的文案 + * @param bookTaskId 小说批次任务ID + * @returns + */ + async ExportCopywriting(bookTaskId: string): Promise { + try { + let bookTask = await this.bookServiceBasic.GetBookTaskDataId(bookTaskId) + let book = await this.bookServiceBasic.GetBookDataById(bookTask.bookId) + let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({ + bookId: book.id, + bookTaskId: bookTaskId + }) + + let emptyList = [] + let content = [] + + // 检查是不是所有的里面都有文案 + for (let i = 0; i < bookTaskDetails.length; i++) { + const element = bookTaskDetails[i]; + if (isEmpty(element.afterGpt)) { + emptyList.push(element.name) + } else { + content.push(element.afterGpt) + } + } + if (emptyList.length > 0) { + throw new Error(`以下分镜没有文案:${emptyList.join("\n")}`); + } + // 写出文案 + let contentStr = content.join("。\n"); + contentStr = contentStr + '。' + let wordPath = path.join(book.bookFolderPath, "文案.txt") + await fs.promises.writeFile(wordPath, contentStr, 'utf-8') + return successMessage(wordPath, "导出文案成功", "ReverseBook_ExportCopywriting") + } catch (error) { + return errorMessage("导出文案失败,失败信息如下:" + error.message, 'ReverseBook_ExportCopywriting') + } + + } + + //#endregion +} \ No newline at end of file diff --git a/src/main/Service/Translate/TranslateService.ts b/src/main/Service/Translate/TranslateService.ts index d4c3746..7714447 100644 --- a/src/main/Service/Translate/TranslateService.ts +++ b/src/main/Service/Translate/TranslateService.ts @@ -10,6 +10,7 @@ import { ResponseMessageType } from "../../../define/enum/softwareEnum"; import { SoftwareService } from '../../../define/db/service/SoftWare/softwareService' import { isEmpty } from "lodash"; import { ValidateJson } from "../../../define/Tools/validate"; +import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic"; /** * 翻译实现服务 @@ -18,8 +19,10 @@ export class TranslateService { translate: Translate bookTaskDetail: BookTaskDetailService; softwareService: SoftwareService + bookServiceBasic: BookServiceBasic constructor() { + this.bookServiceBasic = new BookServiceBasic(); } async InitService() { @@ -107,7 +110,7 @@ export class TranslateService { // 解析 let tryParse = ValidateJson(translateSettingString) if (!tryParse) { - throw new Error("翻译设置数据解析失败") + throw new Error("翻译设置数据解析失败,请重置后重新配置") } translateSetting = JSON.parse(translateSettingString); } @@ -175,7 +178,7 @@ export class TranslateService { /** - * 反推翻译提示词处理 + * 翻译反推提示词处理 * @param bookTaskDetailId 对应的分镜ID * @param reversePromptId 对应的反推提示词数据ID * @param to 目标语言 @@ -200,6 +203,12 @@ export class TranslateService { case TranslateType.REVERSE_PROMPT_TRANSLATE: await this.TranslateProcessReversePrompt(value.bookTaskDetailId, value.reversePromptId, to, dstString) break; + case TranslateType.GPT_PROMPT_TRANSLATE: + // 这个直接改就行 + await this.bookServiceBasic.UpdateBookTaskDetail(value.bookTaskDetailId, { + gptPrompt: dstString + }) + break; default: throw new Error("未知的翻译类型"); } @@ -228,11 +237,15 @@ export class TranslateService { // 写回数据库 await this.TranslateReturnProcess(element, data.to, srcString); + let responseType = ResponseMessageType.REVERSE_PROMPT_TRANSLATE; + if (element.type == TranslateType.GPT_PROMPT_TRANSLATE) { + responseType = ResponseMessageType.GPT_PROMPT_TRANSLATE + } // 做个返回数据 this.sendTranslateReturn(element.windowId, { code: 1, id: element.bookTaskDetailId, - type: ResponseMessageType.PROMPT_TRANSLATE, + type: responseType, data: { progress: i + 1, total: value.length, diff --git a/src/main/Service/d3.ts b/src/main/Service/d3.ts new file mode 100644 index 0000000..a48f5c4 --- /dev/null +++ b/src/main/Service/d3.ts @@ -0,0 +1,15 @@ + +export class D3 { + constructor() { } + + //#region D3进行画图的基础方法 + + //#endregion + + //#region 软件相关的方法 + + + + //endregion + +} \ No newline at end of file diff --git a/src/main/Service/ffmpegOptions.ts b/src/main/Service/ffmpegOptions.ts index 8fbfef1..1959af6 100644 --- a/src/main/Service/ffmpegOptions.ts +++ b/src/main/Service/ffmpegOptions.ts @@ -172,7 +172,7 @@ export class FfmpegOptions { * @param {*} outAudioPath 输出音频地址 * @returns */ - async FfmpegExtractAudio(videoPath, outAudioPath) { + async FfmpegExtractAudio(videoPath: string, outAudioPath: string): Promise { try { // 判断视频地址是不是存在 let videoIsExist = await CheckFileOrDirExist(videoPath) @@ -205,13 +205,12 @@ export class FfmpegOptions { } /** - * Ffmpeg提取视频帧(只提取一帧) - * 根据point判断提取什么位置的帧 + * Ffmpeg提取视频指定时间的帧(只提取一帧) * @param {*} frameTime 视频的时间点 * @param {*} videoPath 视频地址 * @param {*} outFramePath 输出帧地址 */ - async FfmpegGetFrame(frameTime, videoPath, outFramePath) { + async FfmpegGetFrame(frameTime: number, videoPath: string, outFramePath: string): Promise { try { let videoIsExist = await CheckFileOrDirExist(videoPath) if (videoIsExist == false) { @@ -238,7 +237,7 @@ export class FfmpegOptions { .run() }) let res_msg = `视频抽帧完成,输出地址:${res}` - return successMessage(res, '视频抽帧成功', 'BasicReverse_FfmpegGetFrame') + return successMessage(res, res_msg, 'BasicReverse_FfmpegGetFrame') } catch (error) { return errorMessage(error.message, 'BasicReverse_FfmpegGetFrame') } @@ -328,7 +327,7 @@ export class FfmpegOptions { throw new Error(frameRes.message) } let outImagePaths = [] - if(await CheckFileOrDirExist(outImagePath) == false){ + if (await CheckFileOrDirExist(outImagePath) == false) { return successMessage( outImagePaths, '获取指定位置的帧和裁剪成功', diff --git a/src/main/Service/taskManage.ts b/src/main/Service/taskManage.ts index de5b564..cf97711 100644 --- a/src/main/Service/taskManage.ts +++ b/src/main/Service/taskManage.ts @@ -124,6 +124,12 @@ export class TaskManager { for (let index = 0; index < tasks.data.length; index++) { const element = tasks.data[index]; if (element.type == BookBackTaskType.MJ_IMAGE || element.type == BookBackTaskType.MJ_REVERSE) { + // 判断任务数量是不是又修改 + let taskNumber = global.mjQueue.getConcurrencyLimit(); + if (taskNumber != this.mjOpt.mjSetting.taskCount) { + global.mjQueue.concurrencyLimit = this.mjOpt.mjSetting.taskCount // 重置并发执行的数量 + } + if (global.mjQueue.getWaitingQueue() > 10) { console.log('MJ等待中的任务太多,等待中的任务数量:', global.mjQueue.getWaitingQueue()); this.spaceTime = 20000; @@ -275,11 +281,6 @@ export class TaskManager { * @param task */ async AddImageMJImage(task: TaskModal.Task) { - // 判断任务数量是不是又修改 - let taskNumber = global.mjQueue.getConcurrencyLimit(); - if (taskNumber != this.mjOpt.mjSetting.taskCount) { - global.mjQueue.concurrencyLimit = this.mjOpt.mjSetting.taskCount // 重置并发执行的数量 - } // 判断是不是MJ的任务 let batch = DEFINE_STRING.MJ.MJ_IMAGE; global.mjQueue.enqueue(async () => { diff --git a/src/main/Service/watermark.ts b/src/main/Service/watermark.ts index 6c3f8f1..954fe3d 100644 --- a/src/main/Service/watermark.ts +++ b/src/main/Service/watermark.ts @@ -352,7 +352,11 @@ export class Watermark { bookId: bookTask.bookId }).data as Book.SelectBookTaskDetail[] } else if (operateBookType == OperateBookType.BOOKTASKDETAIL) { - bookTask = this.bookTaskService.GetBookTaskDataById(id); + let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(id) + if (bookTaskDetail == null) { + throw new Error("指定的小说任务分镜信息不存在,请检查") + } + bookTask = this.bookTaskService.GetBookTaskDataById(bookTaskDetail.bookTaskId); if (bookTask == null) { throw new Error('指定的小说任务数据不存在') } @@ -360,10 +364,7 @@ export class Watermark { if (book == null) { throw new Error("小说数据不存在,请检查") } - let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(id) - if (bookTaskDetail == null) { - throw new Error("指定的小说任务分镜信息不存在,请检查") - } + bookTaskDetails = [bookTaskDetail]; } else { throw new Error("未知的操作类型") @@ -451,7 +452,11 @@ export class Watermark { this.taskScheduler.AddLogToDB(book.id, book.type, `${element.name} 去除水印完成`, element.bookTaskId, LoggerStatus.SUCCESS) } // 全部完毕 - return successMessage(null, "全部图片去除水印完成", "ReverseBook_RemoveWatermark") + if (operateBookType == OperateBookType.BOOKTASKDETAIL) { + return successMessage(bookTaskDetails[0].oldImage + '?t=' + new Date().getTime(), "去除水印完成", "ReverseBook_RemoveWatermark") + } else { + return successMessage(null, "全部图片去除水印完成", "ReverseBook_RemoveWatermark") + } } catch (error) { return errorMessage("去除水印失败,错误信息如下:" + error.message, "ReverseBook_RemoveWatermark") } diff --git a/src/main/setting/gptSetting.ts b/src/main/setting/gptSetting.ts index 7d23e40..e44c3df 100644 --- a/src/main/setting/gptSetting.ts +++ b/src/main/setting/gptSetting.ts @@ -6,11 +6,20 @@ import axios from 'axios' import { ServiceBase } from '../../define/db/service/serviceBase' import { isEmpty } from 'lodash' import { ValidateJson } from '../../define/Tools/validate' +import { SyncGptKeyType } from '../../define/enum/softwareEnum' +import { GeneralResponse } from '../../model/generalResponse' +import { SoftWareServiceBasic } from '../Service/ServiceBasic/softwareServiceBasic' +import { SubtitleService } from '../Service/Subtitle/subtitleService' +import { SubtitleModel } from '../../model/subtitle' export class GptSetting extends ServiceBase { + softWareServiceBasic: SoftWareServiceBasic + subtitleService: SubtitleService constructor() { super() axios.defaults.baseURL = define.serverUrl + this.softWareServiceBasic = new SoftWareServiceBasic(); + this.subtitleService = new SubtitleService() } /** @@ -105,4 +114,19 @@ export class GptSetting extends ServiceBase { ) } } + + + /** + * 同步GPT的Key到其他的设置中 + * @param syncTpye + */ + async SyncGptKey(syncTpye: SyncGptKeyType): Promise { + try { + let globalGptKey = global.config.gpt_key; + console.log(globalGptKey); + return successMessage(globalGptKey, '获取全局配置成功', 'GptSetting_SyncGptKey') + } catch (error) { + return errorMessage("同步GPT的Key到其他的设置中失败,失败信息如下:" + error.toString(), "GptSetting_SyncGptKey") + } + } } diff --git a/src/model/Setting/softwareSetting.d.ts b/src/model/Setting/softwareSetting.d.ts new file mode 100644 index 0000000..3aef767 --- /dev/null +++ b/src/model/Setting/softwareSetting.d.ts @@ -0,0 +1,17 @@ + +declare namespace SoftwareSettingModel { + type SoftwareSetting = { + id?: string + theme?: SoftwareThemeType + reverse_display_show?: boolean + reverse_show_book_striped?: boolean + reverse_data_table_size?: ComponentSize + globalSetting?: string + ttsSetting?: string + writeSetting?: string + aiSetting?: string + watermarkSetting?: string + translationSetting?: string + subtitleSetting?: string + } +} \ No newline at end of file diff --git a/src/model/book.d.ts b/src/model/book.d.ts index 8116745..2a578cd 100644 --- a/src/model/book.d.ts +++ b/src/model/book.d.ts @@ -30,6 +30,22 @@ declare namespace Book { suffixPrompt?: string | null // 后缀 } + type BookBackTaskList = { + id?: string + bookId?: string + bookTaskId?: string + bookTaskDetailId?: string + name?: string // 任务名称,小说名+批次名+分镜名 + type?: BookBackTaskType + status?: BookBackTaskStatus + errorMessage?: string + executeType?: TaskExecuteType // 任务执行类型,手动还是自动 + createTime?: Date + updateTime?: Date + startTime?: number + endTime?: number + } + type SelectBookTask = { no?: number, id?: string, diff --git a/src/model/generalResponse.d.ts b/src/model/generalResponse.d.ts index 8e98cac..48a17cf 100644 --- a/src/model/generalResponse.d.ts +++ b/src/model/generalResponse.d.ts @@ -21,6 +21,11 @@ declare namespace GeneralResponse { current: number, // 当前进度 } + type SubtitleProgressResponse = { + content: string, // 文案内容 + progress: ProgressResponse + } + // 主线程主动返回前端的消息类 type MessageResponse = { code: number, @@ -28,6 +33,6 @@ declare namespace GeneralResponse { type: ResponseMessageType, dialogType?: DialogType = DialogType.MESSAGE, message?: string, - data?: MJ.MJResponseToFront | Buffer | string | TranslateModel.TranslateResponseMessageModel | ProgressResponse + data?: MJ.MJResponseToFront | Buffer | string | TranslateModel.TranslateResponseMessageModel | ProgressResponse | SubtitleProgressResponse } } diff --git a/src/model/subtitle.d.ts b/src/model/subtitle.d.ts new file mode 100644 index 0000000..4af06fc --- /dev/null +++ b/src/model/subtitle.d.ts @@ -0,0 +1,13 @@ +import { GetSubtitleType } from "../define/enum/waterMarkAndSubtitle" + +declare namespace SubtitleModel { + type subtitleSettingModel = { + selectModel: GetSubtitleType, + laiWhisper: { + url: string, + apiKey: string, + syncGPTAPIKey: boolean, + prompt: string + } + } +} \ No newline at end of file diff --git a/src/preload/book.js b/src/preload/book.js index ed49783..2823163 100644 --- a/src/preload/book.js +++ b/src/preload/book.js @@ -51,7 +51,7 @@ const book = { //#endregion - //#region 一键反推的单个任务 + //#region 分镜 // 开始计算分镜数据 ComputeStoryboard: async (bookId) => @@ -60,17 +60,57 @@ const book = { // 开始执行分镜,切分视频 Framing: async (bookId) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.FRAMING, bookId), + // 替换指定的分镜的视频的当前帧 + ReplaceVideoCurrentFrame: async (bookTaskDetailId, currentTime) => + await ipcRenderer.invoke( + DEFINE_STRING.BOOK.REPLACE_VIDEO_CURRENT_FRAME, + bookTaskDetailId, + currentTime + ), + //#endregion + + //#region 文案相关信息 + // 获取文案信息 - GetCopywriting: async (bookId, bookTaskId) => - await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_COPYWRITING, bookId, bookTaskId), + GetCopywriting: async (bookId, bookTaskId, operateBookType) => + await ipcRenderer.invoke( + DEFINE_STRING.BOOK.GET_COPYWRITING, + bookId, + bookTaskId, + operateBookType + ), + + // 将文案信息导出,方便修改 + ExportCopywriting: async (bookTaskId) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.EXPORT_COPYWRITING, bookTaskId), + + //#endregion + + //#region 水印 // 去除所有水印 - RemoveWatermark: async (id,operateBookType) => - await ipcRenderer.invoke(DEFINE_STRING.BOOK.REMOVE_WATERMARK, id,operateBookType), + RemoveWatermark: async (id, operateBookType) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.REMOVE_WATERMARK, id, operateBookType), - // 添加单句推理的 - AddReversePrompt: async (bookTaskDetailIds, type) => - await ipcRenderer.invoke(DEFINE_STRING.BOOK.ADD_REVERSE_PROMPT, bookTaskDetailIds, type), + //#endregion + + //#region 提示词 + + MergePrompt: async (id, type, operateBookType) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.MERGE_PROMPT, id, type, operateBookType), + + //#endregion + + //#region 一键反推的单个任务 + + // 添加单句反推的 + AddReversePrompt: async (bookTaskDetailIds, operateBookType, type) => + await ipcRenderer.invoke( + DEFINE_STRING.BOOK.ADD_REVERSE_PROMPT, + bookTaskDetailIds, + operateBookType, + type + ), // 将反推的数据指定的位置的提示词写入到GPT提示词中 ReversePromptToGptPrompt: async (bookId, bookTaskId, index) => @@ -115,6 +155,18 @@ const book = { OneToFourBookTask: async (bookTaskId) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.ONE_TO_FOUR_BOOK_TASK, bookTaskId), + //#region 小说相关 + + // 重置小说数据 + ResetBookData: async (bookId) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.RESET_BOOK_DATA, bookId), + + // 删除小说数据 + DeleteBookData: async (bookId) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.DELETE_BOOK_DATA, bookId), + + //#endregion + //#region 小说批次任务相关 // 重置小说批次数据 @@ -142,7 +194,7 @@ const book = { HDImage: async (id, scale, operateBookType) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.HD_IMAGE, id, scale, operateBookType), - // 讲小说视频相关的设置添加到小说任务批次 + // 将小说视频相关的设置添加到小说任务批次 UseBookVideoDataToBookTask: async (id, operateBookType) => await ipcRenderer.invoke( DEFINE_STRING.BOOK.USE_BOOK_VIDEO_DATA_TO_BOOK_TASK, diff --git a/src/preload/gpt.js b/src/preload/gpt.js index 4c46595..9aceaef 100644 --- a/src/preload/gpt.js +++ b/src/preload/gpt.js @@ -7,7 +7,6 @@ const gpt = { return await ipcRenderer.invoke(DEFINE_STRING.GPT.INIT_SERVER_GPT_OPTIONS) }, - //#region GPT 设置相关 // 获取软件设置里面的GPT设置 GetAISetting: async () => { @@ -17,6 +16,11 @@ const gpt = { // 保存软件设置里面的GPT设置 SaveAISetting: async (data) => { return await ipcRenderer.invoke(DEFINE_STRING.GPT.SAVE_AI_SETTING, data) + }, + + // 同步GPT Key 到指定的设置 + SyncGptKey: async (syncType) => { + return await ipcRenderer.invoke(DEFINE_STRING.GPT.SYNC_GPT_KEY, syncType) } //#endregion diff --git a/src/preload/mj.js b/src/preload/mj.js index 6f8de10..c2f1096 100644 --- a/src/preload/mj.js +++ b/src/preload/mj.js @@ -69,10 +69,6 @@ const mj = { AutoMatchUser: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.MJ.AUTO_MATCH_USER, value)), - // 合并提示词 - MergePrompt: async (id, mergeType) => - await ipcRenderer.invoke(DEFINE_STRING.MJ.MJ_MERGE_PROMPT, id, mergeType), - // 单个出图 AddMJGenerateImageTask: async (id, operateBookType) => await ipcRenderer.invoke(DEFINE_STRING.MJ.ADD_MJ_GENADD_MJ_GENERATE_IMAGE_TASK, id, operateBookType) diff --git a/src/preload/sd.js b/src/preload/sd.js index 5f5aeba..14da344 100644 --- a/src/preload/sd.js +++ b/src/preload/sd.js @@ -9,9 +9,5 @@ const sd = { // 文生图,单张 txt2img: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.SD.TXT2IMG, value)), - - // 合并提示词 - MergePrompt: async (id, mergeType) => - await ipcRenderer.invoke(DEFINE_STRING.SD.SD_MERGE_PROMPT, id, mergeType) } export { sd } diff --git a/src/preload/write.js b/src/preload/write.js index 974f0fa..382691d 100644 --- a/src/preload/write.js +++ b/src/preload/write.js @@ -11,6 +11,18 @@ const write = { SaveWriteConfig: async (data) => await ipcRenderer.invoke(DEFINE_STRING.WRITE.SAVE_WRITE_CONFIG, data), + // 获取当前的识别字幕设置的数据 + GetSubtitleSetting: async () => + await ipcRenderer.invoke(DEFINE_STRING.WRITE.GET_SUBTITLE_SETTING), + + // 重置识别字幕设置的数据 + ResetSubtitleSetting: async () => + await ipcRenderer.invoke(DEFINE_STRING.WRITE.RESET_SUBTITLE_SETTING), + + // 保存识别字幕设置的数据 + SaveSubtitleSetting: async (subtitleSetting) => + await ipcRenderer.invoke(DEFINE_STRING.WRITE.SAVE_SUBTITLE_SETTING, subtitleSetting), + //#endregion //#region AI相关的任务 diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 827efa0..c1afe1d 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,19 +1,19 @@ diff --git a/src/renderer/src/components/Book/Components/BookTaskListAction.vue b/src/renderer/src/components/Book/Components/BookTaskListAction.vue index eee3d13..fa72458 100644 --- a/src/renderer/src/components/Book/Components/BookTaskListAction.vue +++ b/src/renderer/src/components/Book/Components/BookTaskListAction.vue @@ -34,14 +34,11 @@ diff --git a/src/renderer/src/components/Book/Components/ManageBookDetailButton.vue b/src/renderer/src/components/Book/Components/ManageBookDetailButton.vue index 547df16..135dee2 100644 --- a/src/renderer/src/components/Book/Components/ManageBookDetailButton.vue +++ b/src/renderer/src/components/Book/Components/ManageBookDetailButton.vue @@ -134,10 +134,12 @@ export default defineComponent({ code.value = code.value + '\n' + value.data return } + // 修改进度 + softwareStore.spin.tip = `正在提取文案,当前进度 ${value.data.progress.current} / ${value.data.progress.total} 。。。` let index = reverseManageStore.selectBookTaskDetail.findIndex((item) => item.id == value.id) if (index >= 0) { - reverseManageStore.selectBookTaskDetail[index].word = value.data - reverseManageStore.selectBookTaskDetail[index].afterGpt = value.data + reverseManageStore.selectBookTaskDetail[index].word = value.data.content + reverseManageStore.selectBookTaskDetail[index].afterGpt = value.data.content } }) @@ -251,18 +253,39 @@ export default defineComponent({ return } - let copywriting_res = await window.book.GetCopywriting(reverseManageStore.selectBook.id) - if (copywriting_res.code == 0) { - message.error(copywriting_res.message) + let subtitleSettingRes = await window.write.GetSubtitleSetting() + if (subtitleSettingRes.code == 0) { + message.error(subtitleSettingRes.message) return } - message.success('获取文案成功') + + let da = dialog.warning({ + title: '开始提取文案提示', + content: `即将进行文案提取,当前的文案提取模式为 ${subtitleSettingRes.data.selectModel} ,是否继续?`, + positiveText: '继续', + negativeText: '取消', + onPositiveClick: async () => { + da?.destroy() + softwareStore.spin.spinning = true + softwareStore.spin.tip = `正在使用 ${subtitleSettingRes.data.selectModel} 提取文案,正在准备中。。。` + let copywriting_res = await window.book.GetCopywriting( + reverseManageStore.selectBook.id, + reverseManageStore.selectBookTask.id, + OperateBookType.BOOKTASK + ) + softwareStore.spin.spinning = false + if (copywriting_res.code == 0) { + message.error(copywriting_res.message) + return + } + // dialog.success({}) + message.success('获取全部文案成功') + } + }) } // 开始去除水印 async function RemoveWatermark() { - // softwareStore.spin.spinning = true - // softwareStore.spin.tip = '正在去除水印中' if (isEmpty(reverseManageStore.selectBookTask.id)) { window.api.showGlobalMessageDialog({ code: 0, @@ -270,12 +293,23 @@ export default defineComponent({ }) return } - let res_frame = await window.book.RemoveWatermark( - reverseManageStore.selectBookTask.id, - OperateBookType.BOOKTASK - ) - softwareStore.spin.spinning = false - window.api.showGlobalMessageDialog(res_frame) + let da = dialog.warning({ + title: '去除水印提示', + content: '即将去除全部水印,是否继续?', + positiveText: '继续', + negativeText: '取消', + onPositiveClick: async () => { + da?.destroy() + softwareStore.spin.spinning = true + softwareStore.spin.tip = '正在去除水印中。。。' + let res_frame = await window.book.RemoveWatermark( + reverseManageStore.selectBookTask.id, + OperateBookType.BOOKTASK + ) + softwareStore.spin.spinning = false + window.api.showGlobalMessageDialog(res_frame) + } + }) } // 获取文案信息设置 @@ -297,6 +331,28 @@ export default defineComponent({ }) } + // 导出文案 + async function ExportCopywriting() { + debugger + softwareStore.spin.spinning = true + softwareStore.spin.tip = '正在导出文案中...' + let res = await window.book.ExportCopywriting(reverseManageStore.selectBookTask.id) + if (res.code == 0) { + message.error(res.message) + } else { + dialog.success({ + title: '导出文案成功', + content: '导出文案成功,是否打开导出的字幕文件', + positiveText: '打开', + negativeText: '取消', + onPositiveClick: () => { + window.system.OpenFile(res.data) + } + }) + } + softwareStore.spin.spinning = false + } + /** * 获取水印位置 */ @@ -329,6 +385,9 @@ export default defineComponent({ case 'recognizing_setting': // 文案位置设置 await GetCopywritingSetting() break + case 'export_recognizing': // 导出文案 + await ExportCopywriting() + break case 'watermark_position': // 水印位置设置 await GetWatermarkPosition() break @@ -376,7 +435,6 @@ export default defineComponent({ * @param type 反推类型 */ async function ImageReversePrompt(type = undefined) { - debugger if (isEmpty(reverseManageStore.selectBookTask.id)) { window.api.showGlobalMessageDialog({ code: 0, @@ -384,32 +442,36 @@ export default defineComponent({ }) return } + dialog.warning({ + title: '反推提示', + content: `即将进行反推操作,反推方式为 ${ + type ? type : reverseManageStore.selectBook.type + } ,是否继续?`, + positiveText: '继续', + negativeText: '取消', + onPositiveClick: async () => { + if (!type) { + let bookType = reverseManageStore.selectBook.type + type = bookType + } - if (!type) { - let bookType = reverseManageStore.selectBook.type - type = bookType - } - - if (type != BookType.MJ_REVERSE && type != BookType.SD_REVERSE) { - message.error(`该类型 ${bookType} 的小说不支持反推`) - return - } - let reverseIds = [] - // 开始获取需要反推的数据 - for (let i = 0; i < reverseManageStore.selectBookTaskDetail.length; i++) { - const element = reverseManageStore.selectBookTaskDetail[i] - if (!element.reversePrompt || element.reversePrompt.length <= 0) { - reverseIds.push(element.id) + if (type != BookType.MJ_REVERSE && type != BookType.SD_REVERSE) { + message.error(`该类型 ${bookType} 的小说不支持反推`) + return + } + // 开始添加 + let res = await window.book.AddReversePrompt( + reverseManageStore.selectBookTask.id, + OperateBookType.BOOKTASK, + type + ) + if (res.code == 0) { + message.error(res.message) + } else { + message.success('添加所有反推任务成功') + } } - } - - // 开始添加 - let res = await window.book.AddReversePrompt(reverseIds, type) - if (res.code == 0) { - message.error(res.message) - } else { - message.success('添加所有反推任务成功') - } + }) } /** @@ -417,13 +479,16 @@ export default defineComponent({ */ async function RemoveReverseData(id) { let deleteIds = [] - - // 删除全部的 - for (let i = 0; i < reverseManageStore.selectBookTaskDetail.length; i++) { - const element = reverseManageStore.selectBookTaskDetail[i] - if (element.reversePrompt && element.reversePrompt.length > 0) { - deleteIds.push(element.id) + if (id == undefined) { + // 删除全部的 + for (let i = 0; i < reverseManageStore.selectBookTaskDetail.length; i++) { + const element = reverseManageStore.selectBookTaskDetail[i] + if (element.reversePrompt && element.reversePrompt.length > 0) { + deleteIds.push(element.id) + } } + } else { + deleteIds.push(id) } let res = await window.book.RemoveReverseData(deleteIds) @@ -643,6 +708,10 @@ export default defineComponent({ label: '提取文案位置', key: 'recognizing_setting' }, + { + label: '导出文案', + key: 'export_recognizing' + }, { label: '停止提取', key: 'stop_recognizing' @@ -665,7 +734,8 @@ export default defineComponent({ }, { label: 'SD反推', - key: 'sd_reverse' + key: 'sd_reverse', + disabled: reverseManageStore.selectBook.type != BookType.SD_REVERSE }, { label: '清除反推数据', diff --git a/src/renderer/src/components/Book/Components/ManageBookOldImage.vue b/src/renderer/src/components/Book/Components/ManageBookOldImage.vue index c16a8a8..37836fd 100644 --- a/src/renderer/src/components/Book/Components/ManageBookOldImage.vue +++ b/src/renderer/src/components/Book/Components/ManageBookOldImage.vue @@ -1,28 +1,107 @@ - diff --git a/src/renderer/src/components/Book/Components/ManageBookTaskGenerateInformation.vue b/src/renderer/src/components/Book/Components/ManageBookTaskGenerateInformation.vue index a2ad11e..114e845 100644 --- a/src/renderer/src/components/Book/Components/ManageBookTaskGenerateInformation.vue +++ b/src/renderer/src/components/Book/Components/ManageBookTaskGenerateInformation.vue @@ -48,7 +48,7 @@ {{ - type == 'bookTsk' ? '应用主小说相关数据' : '当前就是用的主小说的数据' + type == bookTask ? '应用主小说相关数据' : '当前就是用的主小说的数据' }} 保存数据 @@ -73,6 +73,7 @@ export default defineComponent({ props: ['bookTask', 'type'], setup(props) { let bookTask = ref(props.bookTask) + debugger let type = ref(props.type) let backgroundMusicOptions = ref([]) let message = useMessage() diff --git a/src/renderer/src/components/Book/MJReverse/MJReversePrompt.vue b/src/renderer/src/components/Book/MJReverse/MJReversePrompt.vue index dfd8b9a..1426a14 100644 --- a/src/renderer/src/components/Book/MJReverse/MJReversePrompt.vue +++ b/src/renderer/src/components/Book/MJReverse/MJReversePrompt.vue @@ -1,5 +1,5 @@ + + diff --git a/src/renderer/src/components/Setting/TranslateSetting.vue b/src/renderer/src/components/Setting/TranslateSetting.vue index 6eb4aff..493d905 100644 --- a/src/renderer/src/components/Setting/TranslateSetting.vue +++ b/src/renderer/src/components/Setting/TranslateSetting.vue @@ -39,8 +39,14 @@ + @@ -93,6 +99,7 @@ export default defineComponent({ return } translateSetting.value = translateSettingRes.data + console.log(translateSetting.value) }) function GetTranslationName(name) { diff --git a/src/renderer/src/components/Watermark/GetWaterMaskRectangle.vue b/src/renderer/src/components/Watermark/GetWaterMaskRectangle.vue index 1ad0fe9..17af09d 100644 --- a/src/renderer/src/components/Watermark/GetWaterMaskRectangle.vue +++ b/src/renderer/src/components/Watermark/GetWaterMaskRectangle.vue @@ -448,7 +448,7 @@ export default defineComponent({ async function SaveMask() { debugger // 判断当前是不是又蒙板 - if (canvasHistory.length <= 1) { + if (canvasHistory.length <= 0) { message.error('请先选择去水印的区域') return } diff --git a/src/stores/reverseManage.ts b/src/stores/reverseManage.ts index e57936c..25b3aa8 100644 --- a/src/stores/reverseManage.ts +++ b/src/stores/reverseManage.ts @@ -81,6 +81,8 @@ export const useReverseManageStore = defineStore('reverseManage', { // 获取小说任务数据 async GetBookTaskDataFromDB(condition) { try { + debugger + this.bookTaskData = [] //@ts-ignore let res = await window.book.GetBookTaskData(condition) if (res.code == 0) { @@ -90,7 +92,6 @@ export const useReverseManageStore = defineStore('reverseManage', { this.bookTaskData = res.data.bookTasks this.selectBookTask = res.data.bookTasks[0] } else { - this.bookTaskData = [] this.selectBookTask = { no: null, id: null,