9Syeatg_WhC;({(369Oan|@J3@moebybeK!!tGwJuM=`ukk1QB&@+dj3W
zm~q1|Df2kXJhVE5N}1q+mfNoBy_`$Fz|F<}gym$w8JnelOJMsB-nt3t`kJg4wd`GD%4SNpjKB3U!?G^n`TyBm0%^*(IYu?B=w+A4N~N
zq^Jt(s>`j9dqFmtykV8go~1z>e
z{fTJwJpB3v{#JGKxB$L#hPYb!RrIj5Bn*yOy2j5C>LCm)yPTlPQL@jQMMvMYPvA5Y
zKuH@$#P||l6P`3uO?;QY!GVZOYNz@vWi|j{xrjio
zj*k(ZP0COdQQwn;`HFFk4e%D1Q~r5NElPDrZ7_1kXFuDAfx`d@1x^R>O0T)sNYv-s7W5KO%_duKWRPt@y52yJ7dRF_3{%LYGoK^
zS%WzY>^2T2^>_4!QAaAyBSA!&mObE^iC&Zv^X|}OA>2&g>ewa3zc7pvZQ`vn*E?6K
zoj-6OW~d%U0oWvpRV*Wyr~tf?Y(x(B!(AfyAR^>R)4{SoY};%jC~b5u`+d?X;!~9FyfI{xo_|N@b6|l!}+|fxlVZP
zt9Jc;fl6RBY>|qf?FuQ_6Si0fu^rXG{n(OI&ptPjyBvyZ{!s7h`#_p@oM!r!`4pQU
zRGZ;7!3`Tp3+`_Wdct_O8*%ec(Mr)u$k7(WDK^+mf4VX#_=j}*jF0%EMB`i#dpxen
zd;gJ|KrRFRn^M8=O@LV^?%3>Kjt{OGYbeHyMC=U-Q;(jUK?#g$;Tkb=%d7Ok6s_5s
zCH*t#r3pSb6c+!?t_rii@V+RwLaGzJ_MpAqRzJg3<>XuG`=?l)VgUQ;cSpLw`gUfz
z@E@^>WZUm89(qzOQ=goA?^3)rU529AlnsTXun8Mx>9^A
ziJr5Yigw|dx)8E4I5*2IHMlxA_P!Y`t0C0&mNdNi)C6i3&gCKyAs6QIyZB$VYK1Hec_eYl*|JvjUwxT7h%)Qjz*@6avRi|HpK0)@w}yWjc#Eaf^_+wF
z%_T5IB=+ZabwDW+Pi9m-r=6Ra5%ce~_q{r6y@b-M)5wf`?lsIqW%=U9o}9RdYs&Z*
z)p&Dl&*1rxP2f%`F`y~g2R|gPy++)qTanB(o$@H8$GpWof)NFC3V$~(%mSPF*qlYf
zMX6f{9~+~%pPYenk*R6F*;U$vgpJt_dlGbsuMN17p1xS!k-%>WKaE86Xya|0EiU`*
zyCLxcoNbktkso~}!w)%U?eCsM&w>o{^C143VGYFhIO?UyB@Q@9zbZ8rVvNT5*OoFjT6*BM`-_T4Q{<>)FsE
z>mg@4IfpleChlSjG!@TpxHCdTt5|mZt$3pQ>Um_Bc{WJ&+b@;-kW%IFASTQeEmOOu
z$n2mGAA5GO(kAC>>RtkRKPo^_l!x@hW56{hyI<$~_WMV=B0-L$DV9cMPG=qA!r^ba
z1XbQhcmuoIQl4EUCIJ}xXEJ@Q31jj?*~6JiYA&Ti$h*AhVxvUd#vjJAVwi{nPWF(M
zGsJB>WDDmtx%BXJ|B8DRKy_j*lcz=C0^SY@jj)^kgPl&26~h`)CcB&EJtWeh5Ld0e
z2&o-Gv1yuBa3LuxJ=UtniL&6ZmfWqijn-R(wtFV$NP1g#%s;XHE^l1U_-t+Z8U@LQ
zd=uGmaGHg=`8hk{`PX=qt-8ECahZF)YVP}b)NDJidEH_^+
zu5tr{3TvsDU09Tg*XDgK>cVFuH8Xq-%t!-OKBevi@Y`j|M>-9OZLPMa-^Y&p3Pq-)
z@UZ#n^rI@om^1Ll5V=>`i=MI<)bq7hsL0(bs6p%*3|Wyz}+M0{!0*^D{!8D_BwWRS%Sc=&Lfo{1t&5
zlVY&_$}DcOAlKEBr-|c>-?KuL0-AgarAbj5FS<=Ug)-
zDPc*_w-p(yBsTfJ0A;Z{uSlK9G#_y{a5gRs#RTBBx5EC8eiQTnWxR_C7iD~p=AON7(oUW$5
z7y?69j%y_N@!Kb?ksp=!nIuJJmb0{`k;npi*Y7jv7(!h-pF$xL9i?rIE%fmHx*~?(
z(!MQ#Qy)}fA40NKV^usuPG^T9Z=(JJ0%MOkHS{9Z63@ZyC5pwR&G|rDP9TmSm9$Qz>
znrkzv(4ANkT2)$)^M^J&sWd)){xC;4DIC4D2xO*cXfW+tE`f(0x_vpP6;Vju(F*#V
zB*DIj&z!eS>DSXiClZHukkOim-v~VBGa#aWEpkb2;=LtTT(qfKcbrzg?tLCK`y#?_
z%1}%YNzRt@YGTp>Q%nc^GKML^Kj7Jg6Q*GlCiNB8^IuXF-$=GgTJZKHVN{PzW3Bzd
z<6wN#GdxMhaH$bhpk^BW%eT>n`$hG(GRi_cU!Oqy@RgfgmUIA;7FUaHqF>GvC)4WB
z6K!M?R$l1MChJ-Qsh-@Wf>+ANFIkvhSFGx#7FrDVu?0T@PYjd;4VR4JXMNA1h5aw-
zXLBpFiUFuo7OQ-_9MYUvZi8>+y58vnSzpu={bOLXb8&IeTKMy0SYQ0u`eZ@Su`6UT
zpg$sEd9|1Mnd?M?r>wW_f<~rV1N!ZSyF9|iKYZ}aQ@DFiW#zHtHcwCx^7KHCbvF`a2=|<>&QX3BH}bE<}xZv-^e$xylJ+eO0x;Ro0oBUbE>M--MS7b>zBv*^S+#E
zEH&)yf0))3SY(k6&PEj5=Z!SHFOvSdBZ-(bss1#Hut0vJRGxhKgAg57scfue`PC%-
zQ~n;34=}9tOaoYC!;psEYLa|juiJ^F*0xld?=onMvE8cf?Ba=_N&UE1S?Is8>k1n?
z=cutea~S0^zpkZ+Rpw5jq=N>n^UoR+*4XMo#BP7vZr{@#Om)gP7z+cU)mqqm8iVQ^
zDJinC;1JU*elzG>;EOMCCUZIa^KwuifZI$~fJ^30=%t@SSj9
zd%Glc2K+zx>&q`~jtMLFCnSLjMuoPxhYwnHR5?ZviS%@&g1@kyQ(iwv39{H)8;$Gd
z(b%@#C>YViAqcp*HCz(>^US-!Lbt1tCPSGO*z{~@sdFF0`pv?m(sMivy}05+i)
Qw*8f&oSJOCv_-`K0(EhW0ssI2
delta 7583
zcmV;Q9bn?CUe7!siBL{Q4GJ0x0000DNk~Le0007<0000~2nGNE055OnRgobyRvqL?
zL_t(|0qvcOaTCcFhR^QBRctK}5y1`-U?M<7fE)oL0!#!p5v+;8A_7bVhzKwdAR@pl
zRN*Sa`kr4t<88NkUOhe1NdCVnVQG+Nre}J(PoJle;bngzgb?EK<;$0W5JCtcgb?EJ
zny}x(w-b`BZ?c28x_%;bv7N{xpU`*tPy(O$NJ~c-L|#0mHqL@AD?9d(0er0d-TrTwn^trX$p8NLKL{Wry!1T;
zE(nP`5(l)xnfI9xLI@#bWB6SXp@P?Q_p=IPv)i|Czn7M%(E8Nd?@G0sKsB3By-g=V
z2qAe-A!Ji&EpYz+`|pUb
z&Ln>^A?@a>>7&M#kaYoR({9|j@y6W-pX@HOGJOC3{i*Mcqhr;FcA;7gN`YN62-emB0z@rany1B{7!Ib5Ht*e@>HC2QU$`Mhtxa=dQ3!
z8AET|L9$8JkefxH#Z(o1izI}Ttgj^$937q=yT**y{Rh(;LMt(_m
zWe9CXSSHsSnt+Xup#m}EV-|91R
zEQD+zDriCA`rF;6^Th5obaxmdr?ZY-k7+3**MTU3Q-w`vE<-}$6#)zn5ms?>k*s}=
zMR`v{`?Zuqhu(f?|Ni^$EC0J#73zQYpLtr=UETJ!OrVXSz@Ky?Yl^l9*LW-)q5|2h
zO>mbFG~8v22^BWcclKp@?7F71lx%Hn&GZiiPO_=^#V#<;uUo+jn8oWXI0yC`7W{vv
ziWB%XKNLbXoW=sD3UtsaURj-*P#6>jU@}mlaO`Ztif-r&g%CDPc+A}G5Hx>^R)5$>
z6M?n`3#pLq-o5)8@1c>E7XGFI?b%TG`A%BE;0Qtp=?+~K?l#SKqI29>$L2lXDhJD-
zj`@2XTIUGq1&stw2%7=F%@Y+i9~b$dV0#^*5av){_=46n7wn2Kffgp3e1$wjgzBwZ
zw@QCkmjd6-efjSk@qa@}M;3oF;WmN65rh!ZFLW-P=%0yN2q%8EI>bBZ{Q8RioMe-!
zEpVF8Buufs>0`gj|3rnt*8wf(z16g&x9|JE)rozl?!?~Imn9D%QUT{ns>t!W
zIYyDlSC2cC2AIiwQsb+WnAA(EA^RxJIB8RTNd>EgR8tT(+w6a{iwJNPVxei@cem+z
z;vO$6v%$&>1(>jT>4S|)4lWo7@S^6PkmX=X7>seQ#bqdjY%oz#>gJ0w$NV}GWtZVI
zqStj&6JO{wp>S~X=FP665$938MfKZN_?bxqOp{xAt|o-fqyak6+^_l7jH#E>VObNV
z;tcHYostd&PD_99m3f<2UMQes5w$Pr+)y?ayRT2o?FSmE`51v}GIHUfG1b@mE|Pj<
z{4{@cm}7?)*?r~sa7DuLb@348Dq_{3uJjQ-ry9SJ
zelL?nPV`!Cq?y?&5H;3n`qxwM&wDor*X|+A^CSQMpx=KqnNu*I>i(bT+o5x=*x!kr
zf2;mh2j`g!^?AIe)<K{$yW4P|d!Vg8k%HWTfqqEj;$B*svcrbjs7%L2xGKD}{t$(Zp$Le4
zciw-zv?5%B_VoCl+8p0Imx;M!v=U~>1%a$AGiia%x1qq^ru&KMbIJM}evkd+3!A#X
z!};R8QTtxj5F2Yg#I#!#DMx6xl?87N`Vy|CBvwSU7C1qL(77vXL?{<%N?gqs1{htL
zOuVnm#1T3zT?PmEgapc~&U3>`B0>Uesz86XHsCoeDJ?q@a)mrM@OiuX??ddP=_HJ2
z-X@<)Kk^E)nB)7H)}<4(<8>rtZKx{CT!v}jTP>Uu^*Qvn54x{W%#Y1Q?}N0PgLk#R
zepsh@GjI2KyTCEHxx8!JZi`>(MCq2oq&~-dG#^`9qsxni6|rug!Yr)sKP@RoBrbn=
z;|^aCl|Ge3&fLD{t0+B=Za;D+Ii|}9j*pAqrqFL=(9%GvhrL+$e~F0IZ>(*jxS+`X1H|oYe1^X;i2Be@F1;a)lI}
zgZ|kqI6gFAk0kAOg
z?aaA9S(#9)^URx2pUdwx6iRuYv;9G%I12PP_!=K-f1fpR2XmgeLEs!_*58iJhf(rp
zP6F<4*bZ4lYE*F2`QSvqJCk+{^q3(*Z(8xYs&-$FHFeuc;GJBQ1@
z6qCL$O5k*y*Dtwq=guTgos{R&l>Zt4758H^DRypFv>Vt+;ItL|!b7V!_nBJ{z775$
z1AoGhv$7LSo%?V=IH16ph*0iUDX4(el)h>=UG!Ofp>2J6^XARFr>Cc1nQVFyn|w2O
zTP~azq;i>xA{E1!c40&m{iZ(q9U8guGaa5AB|ZXjvgpfsC@m<`ZuT+V5c(LRi79L@0)fxh^q4^K7@FX!YliRWmOUe8`|Lar__GX`G_#EQ!se__;Ti-MN_Xwf
zq&cnnQWZ&jIDN2+MnY`}nsqwts<8*%AD}00Qv0?9G+zg0--mye)BSAKlSm>8{28-0
zKF8~z-6XXLSvnp!F%Q}pxS(OE3Q)u32)845^0vW$r-?vf$M(nFp+tgn(0xAR0;M^)
zr&z=w_$(xBLO5ux{VU(rInX?qG5T7%RwItK?9AI7rG3H}sQa7eFmyIAl=by~
zgY6i5UaEldNZT>ivFL5n<6}R1Y-mWv&gQ|7A3y$zDad~SGrIO=3Y*5+*%=bI*Zq2i
zf6f=vNKLKMS?z^P5wRLl%uNAPdX%yS#}BwFRmLC`EYx>n+T+gS&gL=9znHm`@Y
z)wRyONI{XR-6rh6ifH#i7s4j!OyG3E0K2udl{8fW2H}*+X3hWfC45m*OZR@qs;l+#
zb4EhTspo(AmRg21BQa1>&
ztva(2Jwx?6?%;wWB}_1z)?-y_yvw3bGqaSSoV8
z2x)sPGPVuSUSWagY95BNq98yWKkiCHin3{tXdr;WedjH5wI-Nvrm;#0%dFt;o+iS&Cq+kFfq0jI8E~~^Gy6v
z=KiLfC#n$ACO>3Xz=cJvQLT$&=rwxXMHzq68pqwX6gE#)czLCQ%F#E=+}alS74p4oVioIayIZ?=l(7InT^idC9Ll?xK}BxW=O{*aU3)e#39&Oj@E(q
zU+AZQ>s&ENSr)Pms_otm_?4#cC!GcMn}zlr$Odto$Ysp8DrAxX3-noVpVGegqRE&a
z@G+lt(QaAN;)5ykULh(4=H#
zEn5)^U@8k7Dg#2LK3mX^{blCRZ6^`4kmazowRI2^d{vOMGOx;avP?~9fe?R&+T5)K
zpU{5wdOeVUz=`YNj`jGQ=$XsB;|!!FPOeb@So=*aqc|ULjvFLuck6a`2~Fx9fm9RT
z&7ZVz2!;l3B2L7qnS38IfG7wLS>#OZ|Q%FGSVL8tVHEKy`OK~ihLk8Lq;rEv;_M5QZ-@$0cp_D
zGuK)WGs|M;ZJx$dX$uJDU9~%t9zlV#1430B+C={4zqjg1`GZrtatWzNfoVN2&w89|
zltWdiKvS~{A!V4a82TB4E~P2vuFz|ZM=U>o{`{=t-&9%>Q=YFa7$bi|WuWE1oRM6#
zrl-|&P?X%A3WjD-;7m-&lx`~bnY5rF$Jq#z(kdLzHx-!ARp`VdFm$(DnYPDfb`Mp8
zX>3*@Bm}gY=B_bp^1Jhkdeg`7gM)9Pz8^MqD_9qxDU_B`y{jZI105)v40oyC7fZQL
zSdQh8f?LnH^lq6yqey=KEj@(tTnPH|alLsGd
zZEfvJB&uydfWj4_7n7Y%nlp(+_j$Bz^G*&bs+jg6b2j7Neh7X4{Gw(qqte%$^NZTS
zK4a+s)UYr5zcOLd39`yxC;IFjhx`fQ_b#NjSX40f5UlCc4B-+?l%#6*?p#Fxz@U?I72_wODPP
z#5lm`=%BeUc|zE1v(L7U>uoTT*N$E1(uI!YUaPE`+c)w&aEK^fMOv7ive4(2olDi~
zxDWm9R_gLVpZ|Z?)P8LgNQHC7RI6hr0&J$zG1{jV#%G`Tr+#?NkI-KMW^h8Us##0wgVn2QI!payA#B#E
zncF9@Vn;Njo;wE8L;O`ChkZvbi%DDor66UApwdxb^O1k{vxmrjKw)#U&@m>YC0oj~
zsK=i9+nS~lYm21}bLq`ERFK?#);1o)_T#rZlWV}LOzKC{yh;!>qS0pGE(ryZq%@`b
zz|T;RIrL2H+b{#0rn7WX(g)_fuH4*BQ|TFMQh(uAMM02&EFTOCQz^**iV7_2CIBEP
zA)rAU>Gpq_{#`pwZA$dIeDt^Jvoh2_OVd2~_a+ZaprA=QU_sc`zJ@tvY5m=Nj@?az
z#3}!x2qkw1smfjl+AV!(ch{^$;KapN&0nKVty-DS@Gp^`Q6y}lVd>=2LjNBCIQPu7ksUO8=M`oSbV}$=82&k!uty-gOWNOp7@#_-|~#)&i&A
z$Emjv6H6|>lnL1U#jb#{GPZA>CrWlrxJ@>Y;EO8uAMZ!n_nbT{6
z!Ua=V5k<+}A%CO_66#~SmjO-cEvQP5V~LrEWiMG2XtJu`Ab%S6A2Rt`xwH?V$&1tj
zt6P5)=Cc}K-(92YJ&63jYk>)NQ*ZOI?Y(UiywkD@(@;pS(eeCgVB0bm+rFi7u3?lG
zpug93(J_tQl?B!LMfv9F@d&^D{{4GYj7Ujx_k)xLJ-TQ&h;8?>puic4O+ul;Vrl7d
z&}miZd=WSYn%^SG{Tpp{8+!035>D2QLF#{#`SJD^EK)4^WPv45a=k^Lrv+`MOKFwEG9kUi`oY-Wu6^*MED6=LEeKIoR3u99v2wp}bRX#N
z@AUP&N(c%>r6VoabBP4`s#WRpL7#z7`Z{d#%!QJ>o5eD@dt9j9ZN?fr^Rew-78HLt
zBf;ct0V9_J2!zdLr^nIrxRRSQ4`g9}ux8|1&60F9yLax~d8U)W!y=+HO4Teb#n{
z5*1^C0v8P-b9a-#{T;_FCNOF1z7Fjp-eV%1Xg_;YaO{~ZE@(#5{J5C}@&@)9^Buf4
z6lv0w=`zRe0&cqV9KjuVX=RO%nGH#qY1sfwr@X1)9UqoyoV4rQ319x_^%-F
z_r&JY!~!PsNH%$9!QT^RazwYDK`C0COyMVpA!-`wL|S#3A;kh&d%VM
z>iVWu=EU@K_~E$vu;A_3lF?GbSrgMhEu^!`vDNKP?o;Yi33*fCpF7ufq7;9r`F75v
zM?fpC=YQWOhD={*-zzRq{=-s_y>Yi$t!q4M`*rqV$=l!gzW8T|2Z}_dgWR-#SIRv2d22!P1rq
z=*xgeVY8Z=g60f^RMp^bLjHUG?qNaxx_OyX9kie7;4$r^#*>86!FpHk>w|z6JHrA(
z@8w>a`Et5tmW%HIN}t1KV^*DMwI?gkdnnVDuWc24#4N#6Ig
zJp(hF7xni>zK^gZ1uDFOvF-~R*OS0AS6V~{+QX`GTo*Top~az{$Dy=Dt(?$47S7ED
zmF+;QcHiq^spWrV3Y=3($cpkZk~PH4o6uH`&nqfiyMhIL(`Rn!F%>af
zr*zq{tPi4mOtFM-FR=Ec8;>Le(eoc2~G*m158Si#}>La~P#g
zHVO!ta9+nmc79R5u}VJ7U`aFLYfvRa}qm)>{)V#heYYy(Ar5>B;
zYpiX)hx@zCy5)62rUvb^_pR^M>Pxn;I?&&szrSef-evvbzBG8|lqs@_;2G%Sb*+Pq
zHI3Shd*nR(y%jdWZxVq;MSXI&X$OhgSLE@l@^rz!;ffio&H1S?aGq*kJ}PAFc~NQ;
zq~Cv$HNu>#ulTxbZEa=C)JoVn6y?thG1o41IWTEI9Y<;k)~*j+C-lBT0NO^X71#F*
zK33-!wU$4o7W!~Z?MshLd!E_mnOyjW9qTdQuDcI6<`wUno}dgbebZNq&G!$ikV)qJ
z*NJHpkGmG|be?>K)LAx`y4CgO&z#OT(@lTWf^S;8*D`1b15hZktAweLjiw;gN{AyQ
z++D^BI4>cJK?7!VsS&`@)X|581Pz4?bN2?+?SQ5*s<~Y+VzV~
zYxmlk)Dh*_;Je96QjvaQr9OjN!Q9+vW~`lMn_F-^OB!Ekrh@MgBiRWdglrn?oECox
zleVAB?UFn#2$Ig~1kM%nH30rAc}f|}K?rT;q%Y3nCP+^3f|*K%%XvUk9%QttJbZ)@
zLP*!d3Y-x1#
Date: Thu, 1 Jan 2026 12:50:58 +0900
Subject: [PATCH 48/55] [Update] Add new auth key URLs and update translation
labels for key management.
---
locales/en.yml | 1 +
locales/ja.yml | 1 +
src-ui/logics/ui_configs.js | 6 ++++
.../setting_box/translation/Translation.jsx | 32 ++++++++++++-------
4 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/locales/en.yml b/locales/en.yml
index c7b4135e..1e865589 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -77,6 +77,7 @@ config_page:
version: "Version {{version}}"
model_download_button_label: "Download"
correct_auth_key_required: "Correct Auth Key Required"
+ open_auth_key_webpage: "Open Auth Key Management Webpage"
compute_device:
desc: "The accuracy and speed of each processing type may vary depending on your machine specs, and the compatibility with calculation methods may differ from the displayed order. Please use this as a general guideline."
label_device: "Processing Device"
diff --git a/locales/ja.yml b/locales/ja.yml
index 8748c969..64cbf3d8 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -76,6 +76,7 @@ config_page:
common:
version: "バージョン {{version}}"
model_download_button_label: "ダウンロード"
+ open_auth_key_webpage: "認証キー管理ページを開く"
compute_device:
desc: "各処理タイプの精度・速度は、マシンスペックによって計算方法に相性があり、表示順とは異なる事があるため、大まかな目安としてください。"
label_device: "処理デバイス"
diff --git a/src-ui/logics/ui_configs.js b/src-ui/logics/ui_configs.js
index be6eb194..7c9b1d02 100644
--- a/src-ui/logics/ui_configs.js
+++ b/src-ui/logics/ui_configs.js
@@ -137,6 +137,12 @@ export const whisper_weight_type_status = [
export const deepl_auth_key_url = "https://www.deepl.com/ja/your-account/keys";
+export const plamo_auth_key_url = "https://plamo.preferredai.jp/api";
+export const gemini_auth_key_url = "https://aistudio.google.com/api-keys";
+export const openai_auth_key_url = "https://platform.openai.com/api-keys";
+export const groq_auth_key_url = "https://console.groq.com/keys";
+export const openrouter_auth_key_url = "https://openrouter.ai/keys";
+
export const vrct_document_home_url = "https://misyaguziya.github.io/VRCT-Docs";
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx b/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
index b7e3b91e..1f5a4cce 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
+++ b/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
@@ -29,7 +29,15 @@ import {
ConnectionCheckButton,
} from "../_components";
-import { deepl_auth_key_url } from "@ui_configs";
+import {
+ deepl_auth_key_url,
+ plamo_auth_key_url,
+ gemini_auth_key_url,
+ openai_auth_key_url,
+ groq_auth_key_url,
+ openrouter_auth_key_url,
+} from "@ui_configs";
+
import { useLLMConnection } from "@logics_common";
export const Translation = () => {
@@ -256,7 +264,7 @@ const DeepLAuthKey_Box = () => {
{translator: t("main_page.translator")}
)}
webpage_url={deepl_auth_key_url}
- open_webpage_label={t("config_page.translation.deepl_auth_key.open_auth_key_webpage")}
+ open_webpage_label={t("config_page.common.open_auth_key_webpage")}
variable={variable}
state={currentDeepLAuthKey.state}
onChangeFunction={onChangeFunction}
@@ -282,8 +290,8 @@ const PlamoAuthKey_Box = () => {
{
{
{
{
Date: Thu, 1 Jan 2026 14:38:47 +0900
Subject: [PATCH 49/55] [Update] Adjust button padding and entry width in
OpenWebpageButton; add responsive text size in SidebarSection
---
.../_OpenWebpageButton.module.scss | 3 ++-
.../setting_box/_components/auth_key/AuthKey.jsx | 2 +-
.../app/config_page/sidebar_section/SidebarSection.jsx | 10 ++++++++--
.../sidebar_section/SidebarSection.module.scss | 3 +++
4 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/_components/_atoms/_open_webpage_button/_OpenWebpageButton.module.scss b/src-ui/views/app/config_page/setting_section/setting_box/_components/_atoms/_open_webpage_button/_OpenWebpageButton.module.scss
index fd300952..9547963f 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/_components/_atoms/_open_webpage_button/_OpenWebpageButton.module.scss
+++ b/src-ui/views/app/config_page/setting_section/setting_box/_components/_atoms/_open_webpage_button/_OpenWebpageButton.module.scss
@@ -2,10 +2,11 @@
display: flex;
justify-content: center;
align-items: center;
+ width: fit-content;
}
.open_webpage_button {
- padding: 0.6rem 2.8rem;
+ padding: 0.6rem 1.2rem;
display: flex;
gap: 1rem;
justify-content: center;
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/_components/auth_key/AuthKey.jsx b/src-ui/views/app/config_page/setting_section/setting_box/_components/auth_key/AuthKey.jsx
index 85ae9928..4cddbdeb 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/_components/auth_key/AuthKey.jsx
+++ b/src-ui/views/app/config_page/setting_section/setting_box/_components/auth_key/AuthKey.jsx
@@ -38,7 +38,7 @@ export const AuthKey = (props) => {
return (
- <_Entry ref={entryRef} width="30rem" onChange={onchangeEntryAuthKey} ui_variable={props.variable} is_disabled={is_disabled}/>
+ <_Entry ref={entryRef} width="24rem" onChange={onchangeEntryAuthKey} ui_variable={props.variable} is_disabled={is_disabled}/>
{is_disabled
?
diff --git a/src-ui/views/app/config_page/sidebar_section/SidebarSection.jsx b/src-ui/views/app/config_page/sidebar_section/SidebarSection.jsx
index b7d36272..4242f5b0 100644
--- a/src-ui/views/app/config_page/sidebar_section/SidebarSection.jsx
+++ b/src-ui/views/app/config_page/sidebar_section/SidebarSection.jsx
@@ -27,11 +27,15 @@ export const SidebarSection = () => {
import clsx from "clsx";
import { useI18n } from "@useI18n";
-import { useStore_SelectedConfigTabId } from "@store";
+import {
+ useStore_SelectedConfigTabId,
+ useStore_IsBreakPoint,
+} from "@store";
const Tab = (props) => {
const { t } = useI18n();
const { updateSelectedConfigTabId, currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
+ const { currentIsBreakPoint } = useStore_IsBreakPoint();
const onclickFunction = () => {
updateSelectedConfigTabId(props.tab_id);
};
@@ -54,7 +58,9 @@ const Tab = (props) => {
return (
-
{getLabel()}
+
{getLabel()}
);
diff --git a/src-ui/views/app/config_page/sidebar_section/SidebarSection.module.scss b/src-ui/views/app/config_page/sidebar_section/SidebarSection.module.scss
index 56977d6f..f5806d06 100644
--- a/src-ui/views/app/config_page/sidebar_section/SidebarSection.module.scss
+++ b/src-ui/views/app/config_page/sidebar_section/SidebarSection.module.scss
@@ -64,6 +64,9 @@
font-size: 1.6rem;
// text-overflow: ellipsis;
position: relative;
+ &.tab_text_small {
+ font-size: 1.4rem;
+ }
}
.crown_emoji {
font-size: 1.6rem;
From 457b5e04eeb2dcea396e4fc9b61fc27e0f6f2071 Mon Sep 17 00:00:00 2001
From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com>
Date: Thu, 1 Jan 2026 18:01:04 +0900
Subject: [PATCH 50/55] [Update] Add connection check functionality and new
auth key labels in translation files.
---
locales/en.yml | 43 ++++++++++++++++++
locales/ja.yml | 44 ++++++++++++++++++
.../ConnectionCheckButton.jsx | 13 +++---
.../ConnectionCheckButton.module.scss | 11 +++++
.../setting_box/translation/Translation.jsx | 45 ++++++++++---------
5 files changed, 129 insertions(+), 27 deletions(-)
diff --git a/locales/en.yml b/locales/en.yml
index 1e865589..8db7c916 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -78,6 +78,11 @@ config_page:
model_download_button_label: "Download"
correct_auth_key_required: "Correct Auth Key Required"
open_auth_key_webpage: "Open Auth Key Management Webpage"
+ connection_check:
+ button_label: "Connection Check"
+ checking: "Checking..."
+ connected: "Connected"
+ disconnected: "Disconnected"
compute_device:
desc: "The accuracy and speed of each processing type may vary depending on your machine specs, and the compatibility with calculation methods may differ from the displayed order. Please use this as a general guideline."
label_device: "Processing Device"
@@ -148,6 +153,7 @@ config_page:
large: "High Accuracy Model ({{capacity}})"
translation_compute_device:
label: "Processing device for AI translation"
+
deepl_auth_key:
label: "DeepL Auth Key"
desc: "When using it, please change {{translator}} on the main screen to DeepL_API. ※Some languages may not be supported."
@@ -156,6 +162,43 @@ config_page:
edit: "Edit"
auth_key_success: "Auth key update completed."
+ plamo_auth_key:
+ label: "Plamo Auth Key"
+ select_plamo_model:
+ label: "Select Plamo Model"
+
+ gemini_auth_key:
+ label: "Gemini Auth Key"
+ select_gemini_model:
+ label: "Select Gemini Model"
+
+ openai_auth_key:
+ label: "OpenAI Auth Key"
+ select_openai_model:
+ label: "Select OpenAI Model"
+
+ groq_auth_key:
+ label: "Groq Auth Key"
+ select_groq_model:
+ label: "Select Groq Model"
+
+ openrouter_auth_key:
+ label: "OpenRouter Auth Key"
+ select_openrouter_model:
+ label: "Select OpenRouter Model"
+
+ lmstudio_connection_check:
+ label: "Check LM Studio Connection"
+ select_lmstudio_model:
+ label: "Select LMStudio Model"
+ connection_required: "LMStudio Connection Required"
+
+ ollama_connection_check:
+ label: "Check Ollama Connection"
+ select_ollama_model:
+ label: "Select Ollama Model"
+ connection_required: "Ollama Connection Required"
+
transcription:
section_label_mic: "Mic"
section_label_speaker: "Speaker"
diff --git a/locales/ja.yml b/locales/ja.yml
index 64cbf3d8..51ae19f4 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -76,7 +76,13 @@ config_page:
common:
version: "バージョン {{version}}"
model_download_button_label: "ダウンロード"
+ correct_auth_key_required: "正しい認証キーが必要"
open_auth_key_webpage: "認証キー管理ページを開く"
+ connection_check:
+ button_label: "接続確認"
+ checking: "確認中..."
+ connected: "接続済み"
+ disconnected: "未接続"
compute_device:
desc: "各処理タイプの精度・速度は、マシンスペックによって計算方法に相性があり、表示順とは異なる事があるため、大まかな目安としてください。"
label_device: "処理デバイス"
@@ -147,6 +153,7 @@ config_page:
large: "高精度モデル ({{capacity}})"
translation_compute_device:
label: "AI翻訳の処理デバイス"
+
deepl_auth_key:
label: "DeepL APIキーの登録"
desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。"
@@ -155,6 +162,43 @@ config_page:
edit: "編集"
auth_key_success: "認証キーの更新が完了しました。"
+ plamo_auth_key:
+ label: "Plamo 認証キー"
+ select_plamo_model:
+ label: "Plamo モデルを選択"
+
+ gemini_auth_key:
+ label: "Gemini 認証キー"
+ select_gemini_model:
+ label: "Gemini モデルを選択"
+
+ openai_auth_key:
+ label: "OpenAI 認証キー"
+ select_openai_model:
+ label: "OpenAI モデルを選択"
+
+ groq_auth_key:
+ label: "Groq 認証キー"
+ select_groq_model:
+ label: "Groq モデルを選択"
+
+ openrouter_auth_key:
+ label: "OpenRouter 認証キー"
+ select_openrouter_model:
+ label: "OpenRouter モデルを選択"
+
+ lmstudio_connection_check:
+ label: "LM Studio との接続確認"
+ select_lmstudio_model:
+ label: "LMStudio モデルを選択"
+ connection_required: "LMStudio との接続が必要"
+
+ ollama_connection_check:
+ label: "Ollama との接続確認"
+ select_ollama_model:
+ label: "Ollama モデルを選択"
+ connection_required: "Ollama との接続が必要"
+
transcription:
section_label_mic: "マイク"
section_label_speaker: "スピーカー"
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.jsx b/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.jsx
index 1dfb3251..2f202e95 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.jsx
+++ b/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.jsx
@@ -1,17 +1,20 @@
import styles from "./ConnectionCheckButton.module.scss";
+import { useI18n } from "@useI18n";
export const ConnectionCheckButton = (props) => {
+ const { t } = useI18n();
+
const label = props.state === "pending"
- ? "Checking... 🌀"
+ ? `${t("config_page.common.connection_check.checking")} 🌀`
: props.variable === true
- ? "Connected ✅"
- : "Disconnected ❌";
+ ? `${t("config_page.common.connection_check.connected")} ✅`
+ : `${t("config_page.common.connection_check.disconnected")} ❌`;
return (
-
{label}
+
{label}
- Connection Check
+ {t("config_page.common.connection_check.button_label")}
);
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.module.scss b/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.module.scss
index 116feab4..6b1bd5d0 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.module.scss
+++ b/src-ui/views/app/config_page/setting_section/setting_box/_components/connection_check_button/ConnectionCheckButton.module.scss
@@ -9,6 +9,17 @@
}
}
+.status_label {
+ font-size: 1.2rem;
+ color: var(--dark_200_color);
+ margin-bottom: 0.8rem;
+}
+
+.button_label {
+ font-size: 1.4rem;
+ color: var(--dark_100_color);
+}
+
.button_svg {
width: 2.4rem;
color: var(--dark_400_color);
diff --git a/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx b/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
index 1f5a4cce..684b7090 100644
--- a/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
+++ b/src-ui/views/app/config_page/setting_section/setting_box/translation/Translation.jsx
@@ -288,8 +288,8 @@ const PlamoAuthKey_Box = () => {
return (
<>
{
return (
{
return (
<>
{
return (
{
return (
<>
{
return (
{
return (
<>
{
return (
{
return (
<>
{
return (
{
return (
<>
{
<>
{
};
let selected_label = (!currentIsLMStudioConnected.data && !currentSelectedLMStudioModel.data)
- ? "Connection Required"
+ ? t("config_page.translation.select_lmstudio_model.connection_required")
: currentSelectedLMStudioModel.data;
return (
{
return (
<>
{
};
let selected_label = (!currentIsOllamaConnected.data && !currentSelectedOllamaModel.data)
- ? "Connection Required"
+ ? t("config_page.translation.select_ollama_model.connection_required")
: currentSelectedOllamaModel.data;
return (
Date: Thu, 1 Jan 2026 19:10:39 +0900
Subject: [PATCH 51/55] [Update] Localization: Adjust keys and lines. add
missing keys ko.yml, zh-Hans.yml, zh-Hant.yml
---
locales/en.yml | 1 -
locales/ko.yml | 47 +++++++++++++++++++++++++++++++++++++++++++++
locales/zh-Hans.yml | 45 +++++++++++++++++++++++++++++++++++++++++++
locales/zh-Hant.yml | 45 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 137 insertions(+), 1 deletion(-)
diff --git a/locales/en.yml b/locales/en.yml
index 8db7c916..1b08a31b 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -315,7 +315,6 @@ config_page:
convert_message_to_hiragana:
label: "Show Hiragana"
-
hotkeys:
toggle_vrct_visibility:
label: "Toggle VRCT visibility"
diff --git a/locales/ko.yml b/locales/ko.yml
index 6134f1ad..bc07d6c3 100644
--- a/locales/ko.yml
+++ b/locales/ko.yml
@@ -76,6 +76,13 @@ config_page:
common:
version: "버전 {{version}}"
model_download_button_label: "다운로드"
+ correct_auth_key_required:
+ open_auth_key_webpage:
+ connection_check:
+ button_label:
+ checking:
+ connected:
+ disconnected:
compute_device:
desc:
label_device:
@@ -83,6 +90,8 @@ config_page:
type_template_auto:
type_template_low:
type_template_high:
+ warning_labels:
+ unable_to_use_osc_query:
side_menu_labels:
device: "장치"
@@ -144,6 +153,7 @@ config_page:
large: "정밀 모델 ({{capacity}})"
translation_compute_device:
label: "AI 번역 처리 장치"
+
deepl_auth_key:
label: "DeepL 인증키"
desc: "사용시 메인화면에 있는 {{translator}}를 DeepL_API로 변경해 주세요.\n지원하지 않는 언어도 있습니다."
@@ -152,6 +162,43 @@ config_page:
edit: "편집"
auth_key_success: "인증키 갱신이 완료되었습니다."
+ plamo_auth_key:
+ label:
+ select_plamo_model:
+ label:
+
+ gemini_auth_key:
+ label:
+ select_gemini_model:
+ label:
+
+ openai_auth_key:
+ label:
+ select_openai_model:
+ label:
+
+ groq_auth_key:
+ label:
+ select_groq_model:
+ label:
+
+ openrouter_auth_key:
+ label:
+ select_openrouter_model:
+ label:
+
+ lmstudio_connection_check:
+ label:
+ select_lmstudio_model:
+ label:
+ connection_required:
+
+ ollama_connection_check:
+ label:
+ select_ollama_model:
+ label:
+ connection_required:
+
transcription:
section_label_mic: "마이크"
section_label_speaker: "스피커"
diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml
index 25363241..c3403912 100644
--- a/locales/zh-Hans.yml
+++ b/locales/zh-Hans.yml
@@ -76,6 +76,13 @@ config_page:
common:
version: "版本 {{version}}"
model_download_button_label: "下载"
+ correct_auth_key_required:
+ open_auth_key_webpage:
+ connection_check:
+ button_label:
+ checking:
+ connected:
+ disconnected:
compute_device:
desc: "各处理类型的精度和速度会因机器规格不同而有所差异,可能与显示顺序不同,请作为大致参考。"
label_device: "处理设备"
@@ -146,6 +153,7 @@ config_page:
large: "高精度模型 ({{capacity}})"
translation_compute_device:
label: "AI 翻译的处理设备"
+
deepl_auth_key:
label: "DeepL 授权密匙"
desc: "在使用的时候,使用时请在主屏幕上通过 DeepL_API 选择 {{translator}}\n※某些语言可能不支持"
@@ -154,6 +162,43 @@ config_page:
edit: "编辑"
auth_key_success: "授权密匙认证完成。"
+ plamo_auth_key:
+ label:
+ select_plamo_model:
+ label:
+
+ gemini_auth_key:
+ label:
+ select_gemini_model:
+ label:
+
+ openai_auth_key:
+ label:
+ select_openai_model:
+ label:
+
+ groq_auth_key:
+ label:
+ select_groq_model:
+ label:
+
+ openrouter_auth_key:
+ label:
+ select_openrouter_model:
+ label:
+
+ lmstudio_connection_check:
+ label:
+ select_lmstudio_model:
+ label:
+ connection_required:
+
+ ollama_connection_check:
+ label:
+ select_ollama_model:
+ label:
+ connection_required:
+
transcription:
section_label_mic: "麦克风"
section_label_speaker: "扬声器"
diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml
index ebe44ea1..8362972b 100644
--- a/locales/zh-Hant.yml
+++ b/locales/zh-Hant.yml
@@ -76,6 +76,13 @@ config_page:
common:
version: "版本 {{version}}"
model_download_button_label: "下載"
+ correct_auth_key_required:
+ open_auth_key_webpage:
+ connection_check:
+ button_label:
+ checking:
+ connected:
+ disconnected:
compute_device:
desc: "各處理類型的精度和速度會因機器規格不同而有所差異,可能與顯示順序不同,請作為大致參考。"
label_device: "處理裝置"
@@ -146,6 +153,7 @@ config_page:
large: "高準確率模型({{capacity}})"
translation_compute_device:
label: "AI 翻譯的處理裝置"
+
deepl_auth_key:
label: "DeepL 授權金鑰"
desc: "使用 DeepL API 時請在主螢幕選擇 {{translator}}。※可能不支援某些語言。"
@@ -154,6 +162,43 @@ config_page:
edit: "編輯"
auth_key_success: "授權金鑰更新完成。"
+ plamo_auth_key:
+ label:
+ select_plamo_model:
+ label:
+
+ gemini_auth_key:
+ label:
+ select_gemini_model:
+ label:
+
+ openai_auth_key:
+ label:
+ select_openai_model:
+ label:
+
+ groq_auth_key:
+ label:
+ select_groq_model:
+ label:
+
+ openrouter_auth_key:
+ label:
+ select_openrouter_model:
+ label:
+
+ lmstudio_connection_check:
+ label:
+ select_lmstudio_model:
+ label:
+ connection_required:
+
+ ollama_connection_check:
+ label:
+ select_ollama_model:
+ label:
+ connection_required:
+
transcription:
section_label_mic: "麥克風"
section_label_speaker: "喇叭"
From b0cf8bf3352e10b3b491c9a6a3be64d6ca7c83a9 Mon Sep 17 00:00:00 2001
From: misyaguziya <53165965+misyaguziya@users.noreply.github.com>
Date: Fri, 2 Jan 2026 19:38:06 +0900
Subject: [PATCH 52/55] feat: Implement unified error handling system
- Added `errors.py` to define a centralized error management system with error codes and metadata.
- Created `VRCTError` class for generating standardized error responses.
- Introduced `error_handling_migration_guide.md` to document migration patterns for existing error handling to the new system.
- Updated error handling patterns in the codebase to utilize the new error management system.
---
src-python/controller.py | 782 +++++++-----------
.../docs/error_handling_migration_guide.md | 212 +++++
src-python/errors.py | 694 ++++++++++++++++
3 files changed, 1227 insertions(+), 461 deletions(-)
create mode 100644 src-python/docs/error_handling_migration_guide.md
create mode 100644 src-python/errors.py
diff --git a/src-python/controller.py b/src-python/controller.py
index 5bacdb4f..3b97b89f 100644
--- a/src-python/controller.py
+++ b/src-python/controller.py
@@ -7,6 +7,7 @@ from device_manager import device_manager
from config import config
from model import model
from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer
+from errors import ErrorCode, VRCTError
class Controller:
def __init__(self) -> None:
@@ -153,13 +154,14 @@ class Controller:
def progressBarMicEnergy(self, energy) -> None:
if energy is False:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.DEVICE_NO_MIC,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_device"],
- {
- "message":"No mic device detected",
- "data": None
- },
+ error_response["result"],
)
else:
self.run(
@@ -170,13 +172,14 @@ class Controller:
def progressBarSpeakerEnergy(self, energy) -> None:
if energy is False:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.DEVICE_NO_SPEAKER,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_device"],
- {
- "message":"No speaker device detected",
- "data": None
- },
+ error_response["result"],
)
else:
self.run(
@@ -209,13 +212,14 @@ class Controller:
self.weight_type,
)
else:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_ctranslate2_weight"],
- {
- "message":"CTranslate2 weight download error",
- "data": None
- },
+ error_response["result"],
)
class DownloadWhisper:
@@ -242,13 +246,14 @@ class Controller:
self.weight_type,
)
else:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.WEIGHT_WHISPER_DOWNLOAD,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_whisper_weight"],
- {
- "message":"Whisper weight download error",
- "data": None
- },
+ error_response["result"],
)
def micMessage(self, result: dict) -> None:
@@ -298,23 +303,25 @@ class Controller:
# VRAM不足エラーの検出
is_vram_error, error_message = model.detectVRAMError(e)
if is_vram_error:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_VRAM_MIC,
+ data=error_message
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_mic_vram_overflow"],
- {
- "message":"VRAM out of memory during translation of mic",
- "data": error_message
- },
+ error_response["result"],
)
# 翻訳機能をOFFにする
self.setDisableTranslation()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_translation"],
- {
- "message":"Translation disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
return
else:
@@ -448,13 +455,14 @@ class Controller:
translation, success = model.getOutputTranslate(message, source_language=language)
if all(success) is not True:
self.changeToCTranslate2Process()
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_ENGINE_LIMIT,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_engine"],
- {
- "message":"Translation engine limit error",
- "data": None
- },
+ error_response["result"],
)
else:
pass
@@ -462,23 +470,25 @@ class Controller:
# VRAM不足エラーの検出
is_vram_error, error_message = model.detectVRAMError(e)
if is_vram_error:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_VRAM_SPEAKER,
+ data=error_message
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_speaker_vram_overflow"],
- {
- "message":"VRAM out of memory during translation of speaker",
- "data": error_message
- },
+ error_response["result"],
)
# 翻訳機能をOFFにする
self.setDisableTranslation()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_translation"],
- {
- "message":"Translation disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
return
else:
@@ -625,13 +635,14 @@ class Controller:
if all(success) is not True:
self.changeToCTranslate2Process()
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_ENGINE_LIMIT,
+ data=None
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_engine"],
- {
- "message":"Translation engine limit error",
- "data": None
- },
+ error_response["result"],
)
else:
pass
@@ -639,23 +650,25 @@ class Controller:
# VRAM不足エラーの検出
is_vram_error, error_message = model.detectVRAMError(e)
if is_vram_error:
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_VRAM_CHAT,
+ data=error_message
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_chat_vram_overflow"],
- {
- "message":"VRAM out of memory during translation of chat",
- "data": error_message
- },
+ error_response["result"],
)
# 翻訳機能をOFFにする
self.setDisableTranslation()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_translation"],
- {
- "message":"Translation disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
# エラー時は翻訳なしで返す
return {"status":200,
@@ -845,22 +858,24 @@ class Controller:
if is_vram_error:
# Defaultのデバイス設定に戻す
printLog("VRAM error detected, reverting device setting")
+ error_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_VRAM_ENABLE,
+ data=error_message
+ )
self.run(
- 400,
+ error_response["status"],
self.run_mapping["error_translation_enable_vram_overflow"],
- {
- "message":"VRAM out of memory enabling translation",
- "data": error_message
- },
+ error_response["result"],
)
self.setDisableTranslation()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSLATION_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_translation"],
- {
- "message":"Translation disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
model.changeTranslatorCTranslate2Model()
model.setChangedTranslatorParameters(False)
@@ -1185,13 +1200,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Mic energy threshold value is out of range",
- "data": config.MIC_THRESHOLD
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_MIC_THRESHOLD,
+ data=config.MIC_THRESHOLD
+ )
else:
response = {"status":status, "result":config.MIC_THRESHOLD}
return response
@@ -1226,13 +1238,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Mic record timeout value is out of range",
- "data": config.MIC_RECORD_TIMEOUT
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT,
+ data=config.MIC_RECORD_TIMEOUT
+ )
else:
response = {"status":200, "result":config.MIC_RECORD_TIMEOUT}
return response
@@ -1250,13 +1259,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Mic phrase timeout value is out of range",
- "data": config.MIC_PHRASE_TIMEOUT
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT,
+ data=config.MIC_PHRASE_TIMEOUT
+ )
else:
response = {"status":200, "result":config.MIC_PHRASE_TIMEOUT}
return response
@@ -1274,13 +1280,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Mic max phrases value is out of range",
- "data": config.MIC_MAX_PHRASES
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_MIC_MAX_PHRASES,
+ data=config.MIC_MAX_PHRASES
+ )
else:
response = {"status":200, "result":config.MIC_MAX_PHRASES}
return response
@@ -1368,13 +1371,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Speaker energy threshold value is out of range",
- "data": config.SPEAKER_THRESHOLD
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_SPEAKER_THRESHOLD,
+ data=config.SPEAKER_THRESHOLD
+ )
else:
response = {"status":200, "result":config.SPEAKER_THRESHOLD}
return response
@@ -1408,13 +1408,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Speaker record timeout value is out of range",
- "data": config.SPEAKER_RECORD_TIMEOUT
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT,
+ data=config.SPEAKER_RECORD_TIMEOUT
+ )
else:
response = {"status":200, "result":config.SPEAKER_RECORD_TIMEOUT}
return response
@@ -1432,13 +1429,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Speaker phrase timeout value is out of range",
- "data": config.SPEAKER_PHRASE_TIMEOUT
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT,
+ data=config.SPEAKER_PHRASE_TIMEOUT
+ )
else:
response = {"status":200, "result":config.SPEAKER_PHRASE_TIMEOUT}
return response
@@ -1457,13 +1451,10 @@ class Controller:
else:
raise ValueError()
except Exception:
- response = {
- "status":400,
- "result":{
- "message":"Speaker max phrases value is out of range",
- "data": config.SPEAKER_MAX_PHRASES
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES,
+ data=config.SPEAKER_MAX_PHRASES
+ )
else:
response = {"status":200, "result":config.SPEAKER_MAX_PHRASES}
return response
@@ -1510,13 +1501,10 @@ class Controller:
def setOscIpAddress(self, data, *args, **kwargs) -> dict:
if isValidIpAddress(data) is False:
- response = {
- "status":400,
- "result":{
- "message":"Invalid IP address",
- "data": config.OSC_IP_ADDRESS
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_INVALID_IP,
+ data=config.OSC_IP_ADDRESS
+ )
else:
try:
model.setOscIpAddress(data)
@@ -1533,13 +1521,10 @@ class Controller:
response = {"status":200, "result":config.OSC_IP_ADDRESS}
except Exception:
model.setOscIpAddress(config.OSC_IP_ADDRESS)
- response = {
- "status":400,
- "result":{
- "message":"Cannot set IP address",
- "data": config.OSC_IP_ADDRESS
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_CANNOT_SET_IP,
+ data=config.OSC_IP_ADDRESS
+ )
return response
@staticmethod
@@ -1588,30 +1573,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of deepL auth key",
- "data": config.AUTH_KEYS[translator_name]
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_DEEPL_FAILED,
+ data=config.AUTH_KEYS[translator_name]
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"DeepL auth key length is not correct",
- "data": config.AUTH_KEYS[translator_name]
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_DEEPL_LENGTH,
+ data=config.AUTH_KEYS[translator_name]
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.AUTH_KEYS[translator_name]
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.AUTH_KEYS[translator_name]
+ )
return response
def delDeeplAuthKey(self, *args, **kwargs) -> dict:
@@ -1649,30 +1625,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of plamo auth key",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_PLAMO_FAILED,
+ data=None
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"Plamo auth key length is not correct",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_PLAMO_LENGTH,
+ data=None
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": None
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=None
+ )
if response["status"] == 400:
self.delPlamoAuthKey()
return response
@@ -1707,22 +1674,16 @@ class Controller:
model.updateTranslatorPlamoClient()
response = {"status":200, "result":config.SELECTED_PLAMO_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"Plamo model is not valid",
- "data": config.SELECTED_PLAMO_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_PLAMO_INVALID,
+ data=config.SELECTED_PLAMO_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_PLAMO_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_PLAMO_MODEL
+ )
return response
def getGeminiAuthKey(self, *args, **kwargs) -> dict:
@@ -1751,30 +1712,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of gemini auth key",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_GEMINI_FAILED,
+ data=None
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"Gemini auth key length is not correct",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_GEMINI_LENGTH,
+ data=None
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": None
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=None
+ )
if response["status"] == 400:
self.delGeminiAuthKey()
return response
@@ -1809,22 +1761,16 @@ class Controller:
model.updateTranslatorGeminiClient()
response = {"status":200, "result":config.SELECTED_GEMINI_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"Gemini model is not valid",
- "data": config.SELECTED_GEMINI_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_GEMINI_INVALID,
+ data=config.SELECTED_GEMINI_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_GEMINI_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_GEMINI_MODEL
+ )
return response
@staticmethod
@@ -1854,30 +1800,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of OpenAI auth key",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_OPENAI_FAILED,
+ data=None
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"OpenAI auth key is not valid",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_OPENAI_INVALID,
+ data=None
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": None
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=None
+ )
if response["status"] == 400:
self.delOpenAIAuthKey()
return response
@@ -1912,22 +1849,16 @@ class Controller:
model.updateTranslatorOpenAIClient()
response = {"status":200, "result":config.SELECTED_OPENAI_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"OpenAI model is not valid",
- "data": config.SELECTED_OPENAI_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_OPENAI_INVALID,
+ data=config.SELECTED_OPENAI_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_OPENAI_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_OPENAI_MODEL
+ )
return response
@staticmethod
@@ -1957,30 +1888,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of Groq auth key",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_GROQ_FAILED,
+ data=None
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"Groq auth key is not valid",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_GROQ_INVALID,
+ data=None
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": None
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=None
+ )
if response["status"] == 400:
self.delGroqAuthKey()
return response
@@ -2015,22 +1937,16 @@ class Controller:
model.updateTranslatorGroqClient()
response = {"status":200, "result":config.SELECTED_GROQ_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"Groq model is not valid",
- "data": config.SELECTED_GROQ_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_GROQ_INVALID,
+ data=config.SELECTED_GROQ_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_GROQ_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_GROQ_MODEL
+ )
return response
@staticmethod
@@ -2060,30 +1976,21 @@ class Controller:
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
- response = {
- "status":400,
- "result":{
- "message":"Authentication failure of OpenRouter auth key",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_OPENROUTER_FAILED,
+ data=None
+ )
else:
- response = {
- "status":400,
- "result":{
- "message":"OpenRouter auth key is not valid",
- "data": None
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.AUTH_OPENROUTER_INVALID,
+ data=None
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": None
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=None
+ )
if response["status"] == 400:
self.delOpenRouterAuthKey()
return response
@@ -2118,22 +2025,16 @@ class Controller:
model.updateTranslatorOpenRouterClient()
response = {"status":200, "result":config.SELECTED_OPENROUTER_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"OpenRouter model is not valid",
- "data": config.SELECTED_OPENROUTER_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_OPENROUTER_INVALID,
+ data=config.SELECTED_OPENROUTER_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_OPENROUTER_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_OPENROUTER_MODEL
+ )
return response
def getTranslatorLMStudioConnection(self, *args, **kwargs) -> dict:
@@ -2164,13 +2065,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":"Cannot connect to LMStudio server",
- "data": False
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.CONNECTION_LMSTUDIO_FAILED,
+ data=False
+ )
except Exception as e:
errorLogging()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
@@ -2179,13 +2077,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": False
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=False
+ )
return response
def getConnectedLMStudio(self, *args, **kwargs) -> dict:
@@ -2222,13 +2117,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":"LMStudio URL is not valid",
- "data": config.LMSTUDIO_URL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID,
+ data=config.LMSTUDIO_URL
+ )
except Exception as e:
errorLogging()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
@@ -2237,13 +2129,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.LMSTUDIO_URL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.LMSTUDIO_URL
+ )
return response
def getTranslatorLStudioModelList(self, *args, **kwargs) -> dict:
@@ -2264,22 +2153,16 @@ class Controller:
model.updateTranslatorLMStudioClient()
response = {"status":200, "result":config.SELECTED_LMSTUDIO_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"LMStudio model is not valid",
- "data": config.SELECTED_LMSTUDIO_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_LMSTUDIO_INVALID,
+ data=config.SELECTED_LMSTUDIO_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_LMSTUDIO_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_LMSTUDIO_MODEL
+ )
return response
def getTranslatorOllamaConnection(self, *args, **kwargs) -> dict:
@@ -2310,13 +2193,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST)
self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":"Cannot connect to ollama server",
- "data": False
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.CONNECTION_OLLAMA_FAILED,
+ data=False
+ )
except Exception as e:
errorLogging()
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
@@ -2325,13 +2205,10 @@ class Controller:
self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST)
self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL)
self.updateTranslationEngineAndEngineList()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": False
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=False
+ )
return response
def getTranslatorOllamaModelList(self, *args, **kwargs) -> dict:
@@ -2352,22 +2229,16 @@ class Controller:
model.updateTranslatorOllamaClient()
response = {"status":200, "result":config.SELECTED_OLLAMA_MODEL}
else:
- response = {
- "status":400,
- "result":{
- "message":"ollama model is not valid",
- "data": config.SELECTED_OLLAMA_MODEL
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.MODEL_OLLAMA_INVALID,
+ data=config.SELECTED_OLLAMA_MODEL
+ )
except Exception as e:
errorLogging()
- response = {
- "status":400,
- "result":{
- "message":f"Error {e}",
- "data": config.SELECTED_OLLAMA_MODEL
- }
- }
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=config.SELECTED_OLLAMA_MODEL
+ )
return response
@staticmethod
@@ -2599,13 +2470,10 @@ class Controller:
model.changeMicTranscriptStatus()
response = {"status":200, "result":config.VRC_MIC_MUTE_SYNC}
else:
- response = {
- "status":400,
- "result":{
- "message":"Cannot enable VRC mic mute sync while OSC query is disabled",
- "data": config.VRC_MIC_MUTE_SYNC
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED,
+ data=config.VRC_MIC_MUTE_SYNC
+ )
else:
response = {"status":200, "result":config.VRC_MIC_MUTE_SYNC}
return response
@@ -2814,23 +2682,25 @@ class Controller:
# VRAM不足エラーの検出
is_vram_error, error_message = model.detectVRAMError(e)
if is_vram_error:
+ response = VRCTError.create_error_response(
+ ErrorCode.TRANSCRIPTION_VRAM_MIC,
+ data=error_message
+ )
self.run(
- 400,
+ response["status"],
self.run_mapping["error_transcription_mic_vram_overflow"],
- {
- "message":"VRAM out of memory during mic transcription",
- "data": error_message
- },
+ response["result"],
)
# ここでマイクの音声認識を停止
self.stopTranscriptionSendMessage()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_transcription_send"],
- {
- "message":"Transcription send disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
else:
# その他のエラーは通常通り処理
@@ -2863,23 +2733,25 @@ class Controller:
# VRAM不足エラーの検出
is_vram_error, error_message = model.detectVRAMError(e)
if is_vram_error:
+ response = VRCTError.create_error_response(
+ ErrorCode.TRANSCRIPTION_VRAM_SPEAKER,
+ data=error_message
+ )
self.run(
- 400,
+ response["status"],
self.run_mapping["error_transcription_speaker_vram_overflow"],
- {
- "message":"VRAM out of memory during speaker transcription",
- "data": error_message
- },
+ response["result"],
)
# ここでスピーカーの音声認識を停止
self.stopTranscriptionReceiveMessage()
+ disable_response = VRCTError.create_error_response(
+ ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM,
+ data=False
+ )
self.run(
- 400,
+ disable_response["status"],
self.run_mapping["enable_transcription_receive"],
- {
- "message":"Transcription receive disabled due to VRAM overflow",
- "data": False
- },
+ disable_response["result"],
)
else:
# その他のエラーは通常通り処理
@@ -3068,13 +2940,10 @@ class Controller:
@staticmethod
def setWebSocketHost(data, *args, **kwargs) -> dict:
if isValidIpAddress(data) is False:
- response = {
- "status":400,
- "result":{
- "message":"Invalid IP address",
- "data": config.WEBSOCKET_HOST
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.VALIDATION_INVALID_IP,
+ data=config.WEBSOCKET_HOST
+ )
else:
if model.checkWebSocketServerAlive() is False:
config.WEBSOCKET_HOST = data
@@ -3088,13 +2957,10 @@ class Controller:
config.WEBSOCKET_HOST = data
response = {"status":200, "result":config.WEBSOCKET_HOST}
else:
- response = {
- "status":400,
- "result":{
- "message":"WebSocket server host is not available",
- "data": config.WEBSOCKET_HOST
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.WEBSOCKET_HOST_UNAVAILABLE,
+ data=config.WEBSOCKET_HOST
+ )
return response
@@ -3116,13 +2982,10 @@ class Controller:
config.WEBSOCKET_PORT = int(data)
response = {"status":200, "result":config.WEBSOCKET_PORT}
else:
- response = {
- "status":400,
- "result":{
- "message":"WebSocket server port is not available",
- "data": config.WEBSOCKET_PORT
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.WEBSOCKET_PORT_UNAVAILABLE,
+ data=config.WEBSOCKET_PORT
+ )
return response
@staticmethod
@@ -3137,13 +3000,10 @@ class Controller:
config.WEBSOCKET_SERVER = True
response = {"status":200, "result":config.WEBSOCKET_SERVER}
else:
- response = {
- "status":400,
- "result":{
- "message":"WebSocket server host or port is not available",
- "data": config.WEBSOCKET_SERVER
- }
- }
+ response = VRCTError.create_error_response(
+ ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE,
+ data=config.WEBSOCKET_SERVER
+ )
else:
response = {"status":200, "result":config.WEBSOCKET_SERVER}
return response
diff --git a/src-python/docs/error_handling_migration_guide.md b/src-python/docs/error_handling_migration_guide.md
new file mode 100644
index 00000000..051489f6
--- /dev/null
+++ b/src-python/docs/error_handling_migration_guide.md
@@ -0,0 +1,212 @@
+# エラーハンドリング統一システム移行ガイド
+
+## 概要
+
+`errors.py`で定義された統一エラーシステムを使用して、すべてのエラーハンドリングを標準化しました。
+
+## 変更パターン
+
+### 1. 基本的なエラーレスポンス
+
+#### 修正前:
+```python
+response = {
+ "status": 400,
+ "result": {
+ "message": "Error message",
+ "data": some_value
+ }
+}
+```
+
+#### 修正後:
+```python
+from errors import ErrorCode, VRCTError
+
+response = VRCTError.create_error_response(
+ ErrorCode.APPROPRIATE_ERROR_CODE,
+ data=some_value
+)
+```
+
+### 2. run_mapping経由のエラー通知
+
+#### 修正前:
+```python
+self.run(
+ 400,
+ self.run_mapping["error_device"],
+ {
+ "message": "No mic device detected",
+ "data": None
+ },
+)
+```
+
+#### 修正後:
+```python
+error_response = VRCTError.create_error_response(
+ ErrorCode.DEVICE_NO_MIC,
+ data=None
+)
+self.run(
+ error_response["status"],
+ self.run_mapping["error_device"],
+ error_response["result"],
+)
+```
+
+### 3. 例外からのエラー生成
+
+#### 修正前:
+```python
+except Exception as e:
+ errorLogging()
+ response = {
+ "status": 400,
+ "result": {
+ "message": f"Error {e}",
+ "data": original_value
+ }
+ }
+```
+
+#### 修正後:
+```python
+except Exception as e:
+ errorLogging()
+ response = VRCTError.create_exception_error_response(
+ e,
+ data=original_value
+ )
+```
+
+## 既に移行済みの箇所
+
+### デバイスエラー
+- ✅ `progressBarMicEnergy` - `ErrorCode.DEVICE_NO_MIC`
+- ✅ `progressBarSpeakerEnergy` - `ErrorCode.DEVICE_NO_SPEAKER`
+
+### ウェイトダウンロードエラー
+- ✅ `DownloadCTranslate2.downloaded` - `ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD`
+- ✅ `DownloadWhisper.downloaded` - `ErrorCode.WEIGHT_WHISPER_DOWNLOAD`
+
+### 翻訳エラー
+- ✅ `micMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_MIC`, `ErrorCode.TRANSLATION_DISABLED_VRAM`
+- ✅ `speakerMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_SPEAKER`, `ErrorCode.TRANSLATION_DISABLED_VRAM`
+- ✅ `chatMessage` - `ErrorCode.TRANSLATION_ENGINE_LIMIT`, `ErrorCode.TRANSLATION_VRAM_CHAT`, `ErrorCode.TRANSLATION_DISABLED_VRAM`
+- ✅ `setEnableTranslation` - `ErrorCode.TRANSLATION_VRAM_ENABLE`, `ErrorCode.TRANSLATION_DISABLED_VRAM`
+
+### バリデーションエラー
+- ✅ `setMicThreshold` - `ErrorCode.VALIDATION_MIC_THRESHOLD`
+- ✅ `setSpeakerThreshold` - `ErrorCode.VALIDATION_SPEAKER_THRESHOLD`
+- ✅ `setMicRecordTimeout` - `ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT`
+- ✅ `setMicPhraseTimeout` - `ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT`
+- ✅ `setMicMaxPhrases` - `ErrorCode.VALIDATION_MIC_MAX_PHRASES`
+- ✅ `setSpeakerRecordTimeout` - `ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT`
+- ✅ `setSpeakerPhraseTimeout` - `ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT`
+- ✅ `setSpeakerMaxPhrases` - `ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES`
+- ✅ `setOscIpAddress` - `ErrorCode.VALIDATION_INVALID_IP`, `ErrorCode.VALIDATION_CANNOT_SET_IP`
+
+### VRC連携エラー
+- ✅ `setEnableVrcMicMuteSync` - `ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED`
+
+### 認証エラー
+- ✅ `setDeeplAuthKey` - `ErrorCode.AUTH_DEEPL_LENGTH`, `ErrorCode.AUTH_DEEPL_FAILED`
+
+## 未移行の箇所(要対応)
+
+以下の箇所は同様のパターンで移行が必要です:
+
+### 認証関連
+- ⬜ `setPlamoAuthKey` - `ErrorCode.AUTH_PLAMO_LENGTH`, `ErrorCode.AUTH_PLAMO_FAILED`
+- ⬜ `setPlamoModel` - `ErrorCode.MODEL_PLAMO_INVALID`
+- ⬜ `setGeminiAuthKey` - `ErrorCode.AUTH_GEMINI_LENGTH`, `ErrorCode.AUTH_GEMINI_FAILED`
+- ⬜ `setGeminiModel` - `ErrorCode.MODEL_GEMINI_INVALID`
+- ⬜ `setOpenAIAuthKey` - `ErrorCode.AUTH_OPENAI_INVALID`, `ErrorCode.AUTH_OPENAI_FAILED`
+- ⬜ `setOpenAIModel` - `ErrorCode.MODEL_OPENAI_INVALID`
+- ⬜ `setGroqAuthKey` - `ErrorCode.AUTH_GROQ_INVALID`, `ErrorCode.AUTH_GROQ_FAILED`
+- ⬜ `setGroqModel` - `ErrorCode.MODEL_GROQ_INVALID`
+- ⬜ `setOpenRouterAuthKey` - `ErrorCode.AUTH_OPENROUTER_INVALID`, `ErrorCode.AUTH_OPENROUTER_FAILED`
+- ⬜ `setOpenRouterModel` - `ErrorCode.MODEL_OPENROUTER_INVALID`
+
+### 接続関連
+- ⬜ `checkTranslatorLMStudioConnection` - `ErrorCode.CONNECTION_LMSTUDIO_FAILED`
+- ⬜ `setTranslatorLMStudioURL` - `ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID`
+- ⬜ `setTranslatorLMStudioModel` - `ErrorCode.MODEL_LMSTUDIO_INVALID`
+- ⬜ `checkTranslatorOllamaConnection` - `ErrorCode.CONNECTION_OLLAMA_FAILED`
+- ⬜ `setTranslatorOllamaModel` - `ErrorCode.MODEL_OLLAMA_INVALID`
+
+### WebSocket関連
+- ⬜ `setWebSocketHost` - `ErrorCode.VALIDATION_INVALID_IP`, `ErrorCode.WEBSOCKET_HOST_INVALID`
+- ⬜ `setWebSocketPort` - `ErrorCode.WEBSOCKET_PORT_UNAVAILABLE`
+- ⬜ `setEnableWebSocketServer` - `ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE`
+
+### 音声認識VRAM関連
+- ⬜ `startTranscriptionSendMessage` - `ErrorCode.TRANSCRIPTION_VRAM_MIC`, `ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM`
+- ⬜ `startTranscriptionReceiveMessage` - `ErrorCode.TRANSCRIPTION_VRAM_SPEAKER`, `ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM`
+
+## エラーコードとエンドポイントの対応
+
+`errors.py`の`ENDPOINT_ERROR_MAPPING`に、すべてのエンドポイントとエラーコードの対応が定義されています。
+UI開発者はこのマッピングを参照して、各エンドポイントがどのようなエラーを返すか確認できます。
+
+## エラーレスポンスの構造
+
+統一されたエラーレスポンスは以下の構造を持ちます:
+
+```python
+{
+ "status": 400, # HTTPステータスコード
+ "result": {
+ "error_code": "ERROR_CODE_CONSTANT", # エラーコード定数
+ "message": "Human readable message", # 人間が読めるメッセージ
+ "data": None or original_value, # エラー時に戻す値(通常は元の値)
+ "details": {}, # 追加情報(オプション)
+ "category": "category_name", # エラーカテゴリ
+ "severity": "warning|error|critical", # 重要度
+ }
+}
+```
+
+## UI側での活用
+
+UI側では`error_code`を使用して、エラーの種類を判定し、適切な処理を行うことができます:
+
+```javascript
+if (response.status === 400) {
+ const { error_code, message, data, severity } = response.result;
+
+ switch (error_code) {
+ case "DEVICE_NO_MIC":
+ // マイクデバイスエラーの処理
+ break;
+ case "VALIDATION_MIC_THRESHOLD":
+ // バリデーションエラーの処理(元の値に戻す)
+ setValue(data);
+ break;
+ // ...
+ }
+
+ // 重要度に応じた表示
+ if (severity === "critical") {
+ showCriticalError(message);
+ }
+}
+```
+
+## 移行作業の進め方
+
+1. **パターンの確認**: 上記の変更パターンを参照
+2. **エラーコードの特定**: `errors.py`から適切な`ErrorCode`を選択
+3. **コードの置き換え**: 古いエラーハンドリングを新しいシステムに置き換え
+4. **テスト**: エラーが正しく返されることを確認
+5. **チェックリストの更新**: このドキュメントの✅を更新
+
+## 注意事項
+
+- すべてのエラーは`errors.py`に定義されたエラーコードを使用すること
+- 新しいエラーが必要な場合は、まず`errors.py`に追加すること
+- エラーメッセージは`ERROR_METADATA`で定義されたデフォルトメッセージを使用すること
+ - カスタムメッセージが必要な場合は`custom_message`パラメータを使用
+- `data`パラメータには、エラー時にUIが元の値に戻せるように、元の値を渡すこと
diff --git a/src-python/errors.py b/src-python/errors.py
new file mode 100644
index 00000000..abe75628
--- /dev/null
+++ b/src-python/errors.py
@@ -0,0 +1,694 @@
+# src-python/errors.py
+"""
+統一エラー管理システム
+
+すべてのエラーを一元管理し、エンドポイントとエラーコードの対応を明確にする。
+"""
+
+from typing import Any, Optional, Dict
+from enum import Enum
+
+
+class ErrorCode(str, Enum):
+ """エラーコード定数
+
+ 命名規則: カテゴリ_具体的な内容
+ """
+ # ============================================================================
+ # デバイス関連エラー (DEVICE_*)
+ # ============================================================================
+ DEVICE_NO_MIC = "DEVICE_NO_MIC"
+ DEVICE_NO_SPEAKER = "DEVICE_NO_SPEAKER"
+
+ # ============================================================================
+ # 翻訳関連エラー (TRANSLATION_*)
+ # ============================================================================
+ TRANSLATION_ENGINE_LIMIT = "TRANSLATION_ENGINE_LIMIT"
+ TRANSLATION_VRAM_CHAT = "TRANSLATION_VRAM_CHAT"
+ TRANSLATION_VRAM_MIC = "TRANSLATION_VRAM_MIC"
+ TRANSLATION_VRAM_SPEAKER = "TRANSLATION_VRAM_SPEAKER"
+ TRANSLATION_VRAM_ENABLE = "TRANSLATION_VRAM_ENABLE"
+ TRANSLATION_DISABLED_VRAM = "TRANSLATION_DISABLED_VRAM"
+
+ # ============================================================================
+ # 音声認識関連エラー (TRANSCRIPTION_*)
+ # ============================================================================
+ TRANSCRIPTION_VRAM_MIC = "TRANSCRIPTION_VRAM_MIC"
+ TRANSCRIPTION_VRAM_SPEAKER = "TRANSCRIPTION_VRAM_SPEAKER"
+ TRANSCRIPTION_SEND_DISABLED_VRAM = "TRANSCRIPTION_SEND_DISABLED_VRAM"
+ TRANSCRIPTION_RECEIVE_DISABLED_VRAM = "TRANSCRIPTION_RECEIVE_DISABLED_VRAM"
+
+ # ============================================================================
+ # ウェイトダウンロード関連エラー (WEIGHT_*)
+ # ============================================================================
+ WEIGHT_CTRANSLATE2_DOWNLOAD = "WEIGHT_CTRANSLATE2_DOWNLOAD"
+ WEIGHT_WHISPER_DOWNLOAD = "WEIGHT_WHISPER_DOWNLOAD"
+
+ # ============================================================================
+ # バリデーションエラー (VALIDATION_*)
+ # ============================================================================
+ VALIDATION_MIC_THRESHOLD = "VALIDATION_MIC_THRESHOLD"
+ VALIDATION_SPEAKER_THRESHOLD = "VALIDATION_SPEAKER_THRESHOLD"
+ VALIDATION_MIC_RECORD_TIMEOUT = "VALIDATION_MIC_RECORD_TIMEOUT"
+ VALIDATION_MIC_PHRASE_TIMEOUT = "VALIDATION_MIC_PHRASE_TIMEOUT"
+ VALIDATION_MIC_MAX_PHRASES = "VALIDATION_MIC_MAX_PHRASES"
+ VALIDATION_SPEAKER_RECORD_TIMEOUT = "VALIDATION_SPEAKER_RECORD_TIMEOUT"
+ VALIDATION_SPEAKER_PHRASE_TIMEOUT = "VALIDATION_SPEAKER_PHRASE_TIMEOUT"
+ VALIDATION_SPEAKER_MAX_PHRASES = "VALIDATION_SPEAKER_MAX_PHRASES"
+ VALIDATION_INVALID_IP = "VALIDATION_INVALID_IP"
+ VALIDATION_CANNOT_SET_IP = "VALIDATION_CANNOT_SET_IP"
+
+ # ============================================================================
+ # 認証エラー (AUTH_*)
+ # ============================================================================
+ AUTH_DEEPL_LENGTH = "AUTH_DEEPL_LENGTH"
+ AUTH_DEEPL_FAILED = "AUTH_DEEPL_FAILED"
+ AUTH_PLAMO_LENGTH = "AUTH_PLAMO_LENGTH"
+ AUTH_PLAMO_FAILED = "AUTH_PLAMO_FAILED"
+ AUTH_GEMINI_LENGTH = "AUTH_GEMINI_LENGTH"
+ AUTH_GEMINI_FAILED = "AUTH_GEMINI_FAILED"
+ AUTH_OPENAI_INVALID = "AUTH_OPENAI_INVALID"
+ AUTH_OPENAI_FAILED = "AUTH_OPENAI_FAILED"
+ AUTH_GROQ_INVALID = "AUTH_GROQ_INVALID"
+ AUTH_GROQ_FAILED = "AUTH_GROQ_FAILED"
+ AUTH_OPENROUTER_INVALID = "AUTH_OPENROUTER_INVALID"
+ AUTH_OPENROUTER_FAILED = "AUTH_OPENROUTER_FAILED"
+
+ # ============================================================================
+ # モデル選択エラー (MODEL_*)
+ # ============================================================================
+ MODEL_PLAMO_INVALID = "MODEL_PLAMO_INVALID"
+ MODEL_GEMINI_INVALID = "MODEL_GEMINI_INVALID"
+ MODEL_OPENAI_INVALID = "MODEL_OPENAI_INVALID"
+ MODEL_GROQ_INVALID = "MODEL_GROQ_INVALID"
+ MODEL_OPENROUTER_INVALID = "MODEL_OPENROUTER_INVALID"
+ MODEL_LMSTUDIO_INVALID = "MODEL_LMSTUDIO_INVALID"
+ MODEL_OLLAMA_INVALID = "MODEL_OLLAMA_INVALID"
+
+ # ============================================================================
+ # 接続エラー (CONNECTION_*)
+ # ============================================================================
+ CONNECTION_LMSTUDIO_FAILED = "CONNECTION_LMSTUDIO_FAILED"
+ CONNECTION_OLLAMA_FAILED = "CONNECTION_OLLAMA_FAILED"
+ CONNECTION_LMSTUDIO_URL_INVALID = "CONNECTION_LMSTUDIO_URL_INVALID"
+
+ # ============================================================================
+ # WebSocketエラー (WEBSOCKET_*)
+ # ============================================================================
+ WEBSOCKET_HOST_INVALID = "WEBSOCKET_HOST_INVALID"
+ WEBSOCKET_PORT_UNAVAILABLE = "WEBSOCKET_PORT_UNAVAILABLE"
+ WEBSOCKET_SERVER_UNAVAILABLE = "WEBSOCKET_SERVER_UNAVAILABLE"
+
+ # ============================================================================
+ # VRC連携エラー (VRC_*)
+ # ============================================================================
+ VRC_MIC_MUTE_SYNC_OSC_DISABLED = "VRC_MIC_MUTE_SYNC_OSC_DISABLED"
+
+ # ============================================================================
+ # 汎用エラー (GENERAL_*)
+ # ============================================================================
+ GENERAL_EXCEPTION = "GENERAL_EXCEPTION"
+ GENERAL_UNKNOWN = "GENERAL_UNKNOWN"
+
+
+class ErrorCategory(str, Enum):
+ """エラーカテゴリ"""
+ DEVICE = "device"
+ TRANSLATION = "translation"
+ TRANSCRIPTION = "transcription"
+ WEIGHT = "weight"
+ VALIDATION = "validation"
+ AUTH = "auth"
+ MODEL = "model"
+ CONNECTION = "connection"
+ WEBSOCKET = "websocket"
+ VRC = "vrc"
+ GENERAL = "general"
+
+
+# エラーコードのメタデータ定義
+ERROR_METADATA: Dict[ErrorCode, Dict[str, Any]] = {
+ # デバイスエラー
+ ErrorCode.DEVICE_NO_MIC: {
+ "category": ErrorCategory.DEVICE,
+ "message": "No mic device detected",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.DEVICE_NO_SPEAKER: {
+ "category": ErrorCategory.DEVICE,
+ "message": "No speaker device detected",
+ "severity": "error",
+ "user_action_required": True,
+ },
+
+ # 翻訳エラー
+ ErrorCode.TRANSLATION_ENGINE_LIMIT: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "Translation engine limit error",
+ "severity": "warning",
+ "user_action_required": False,
+ "auto_fallback": True,
+ },
+ ErrorCode.TRANSLATION_VRAM_CHAT: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "VRAM out of memory during translation of chat",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSLATION_VRAM_MIC: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "VRAM out of memory during translation of mic",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSLATION_VRAM_SPEAKER: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "VRAM out of memory during translation of speaker",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSLATION_VRAM_ENABLE: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "VRAM out of memory enabling translation",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSLATION_DISABLED_VRAM: {
+ "category": ErrorCategory.TRANSLATION,
+ "message": "Translation disabled due to VRAM overflow",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+
+ # 音声認識エラー
+ ErrorCode.TRANSCRIPTION_VRAM_MIC: {
+ "category": ErrorCategory.TRANSCRIPTION,
+ "message": "VRAM out of memory during mic transcription",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSCRIPTION_VRAM_SPEAKER: {
+ "category": ErrorCategory.TRANSCRIPTION,
+ "message": "VRAM out of memory during speaker transcription",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSCRIPTION_SEND_DISABLED_VRAM: {
+ "category": ErrorCategory.TRANSCRIPTION,
+ "message": "Transcription send disabled due to VRAM overflow",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+ ErrorCode.TRANSCRIPTION_RECEIVE_DISABLED_VRAM: {
+ "category": ErrorCategory.TRANSCRIPTION,
+ "message": "Transcription receive disabled due to VRAM overflow",
+ "severity": "critical",
+ "user_action_required": True,
+ },
+
+ # ウェイトダウンロードエラー
+ ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD: {
+ "category": ErrorCategory.WEIGHT,
+ "message": "CTranslate2 weight download error",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.WEIGHT_WHISPER_DOWNLOAD: {
+ "category": ErrorCategory.WEIGHT,
+ "message": "Whisper weight download error",
+ "severity": "error",
+ "user_action_required": True,
+ },
+
+ # バリデーションエラー
+ ErrorCode.VALIDATION_MIC_THRESHOLD: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Mic energy threshold value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_SPEAKER_THRESHOLD: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Speaker energy threshold value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Mic record timeout value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Mic phrase timeout value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_MIC_MAX_PHRASES: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Mic max phrases value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Speaker record timeout value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Speaker phrase timeout value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Speaker max phrases value is out of range",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_INVALID_IP: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Invalid IP address",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.VALIDATION_CANNOT_SET_IP: {
+ "category": ErrorCategory.VALIDATION,
+ "message": "Cannot set IP address",
+ "severity": "error",
+ "user_action_required": True,
+ },
+
+ # 認証エラー
+ ErrorCode.AUTH_DEEPL_LENGTH: {
+ "category": ErrorCategory.AUTH,
+ "message": "DeepL auth key length is not correct",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_DEEPL_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of deepL auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_PLAMO_LENGTH: {
+ "category": ErrorCategory.AUTH,
+ "message": "Plamo auth key length is not correct",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_PLAMO_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of plamo auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_GEMINI_LENGTH: {
+ "category": ErrorCategory.AUTH,
+ "message": "Gemini auth key length is not correct",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_GEMINI_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of gemini auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_OPENAI_INVALID: {
+ "category": ErrorCategory.AUTH,
+ "message": "OpenAI auth key is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_OPENAI_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of OpenAI auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_GROQ_INVALID: {
+ "category": ErrorCategory.AUTH,
+ "message": "Groq auth key is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_GROQ_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of Groq auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_OPENROUTER_INVALID: {
+ "category": ErrorCategory.AUTH,
+ "message": "OpenRouter auth key is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.AUTH_OPENROUTER_FAILED: {
+ "category": ErrorCategory.AUTH,
+ "message": "Authentication failure of OpenRouter auth key",
+ "severity": "error",
+ "user_action_required": True,
+ },
+
+ # モデル選択エラー
+ ErrorCode.MODEL_PLAMO_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "Plamo model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_GEMINI_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "Gemini model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_OPENAI_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "OpenAI model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_GROQ_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "Groq model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_OPENROUTER_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "OpenRouter model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_LMSTUDIO_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "LMStudio model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+ ErrorCode.MODEL_OLLAMA_INVALID: {
+ "category": ErrorCategory.MODEL,
+ "message": "ollama model is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+
+ # 接続エラー
+ ErrorCode.CONNECTION_LMSTUDIO_FAILED: {
+ "category": ErrorCategory.CONNECTION,
+ "message": "Cannot connect to LMStudio server",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.CONNECTION_OLLAMA_FAILED: {
+ "category": ErrorCategory.CONNECTION,
+ "message": "Cannot connect to ollama server",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID: {
+ "category": ErrorCategory.CONNECTION,
+ "message": "LMStudio URL is not valid",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+
+ # WebSocketエラー
+ ErrorCode.WEBSOCKET_HOST_INVALID: {
+ "category": ErrorCategory.WEBSOCKET,
+ "message": "WebSocket server host is not available",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.WEBSOCKET_PORT_UNAVAILABLE: {
+ "category": ErrorCategory.WEBSOCKET,
+ "message": "WebSocket server port is not available",
+ "severity": "error",
+ "user_action_required": True,
+ },
+ ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE: {
+ "category": ErrorCategory.WEBSOCKET,
+ "message": "WebSocket server host or port is not available",
+ "severity": "error",
+ "user_action_required": True,
+ },
+
+ # VRC連携エラー
+ ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED: {
+ "category": ErrorCategory.VRC,
+ "message": "Cannot enable VRC mic mute sync while OSC query is disabled",
+ "severity": "warning",
+ "user_action_required": True,
+ },
+
+ # 汎用エラー
+ ErrorCode.GENERAL_EXCEPTION: {
+ "category": ErrorCategory.GENERAL,
+ "message": "An error occurred",
+ "severity": "error",
+ "user_action_required": False,
+ },
+ ErrorCode.GENERAL_UNKNOWN: {
+ "category": ErrorCategory.GENERAL,
+ "message": "Unknown error",
+ "severity": "error",
+ "user_action_required": False,
+ },
+}
+
+
+class VRCTError:
+ """VRCTエラーハンドリングクラス"""
+
+ @staticmethod
+ def create_error_response(
+ error_code: ErrorCode,
+ data: Any = None,
+ details: Optional[Dict[str, Any]] = None,
+ custom_message: Optional[str] = None
+ ) -> Dict[str, Any]:
+ """統一されたエラーレスポンスを生成
+
+ Args:
+ error_code: エラーコード
+ data: エラー時に戻す値(通常は元の値)
+ details: 追加の詳細情報
+ custom_message: カスタムメッセージ(指定しない場合はデフォルトメッセージ)
+
+ Returns:
+ エラーレスポンス辞書
+ """
+ metadata = ERROR_METADATA.get(error_code, ERROR_METADATA[ErrorCode.GENERAL_UNKNOWN])
+
+ return {
+ "status": 400,
+ "result": {
+ "error_code": error_code.value,
+ "message": custom_message or metadata["message"],
+ "data": data,
+ "details": details or {},
+ "category": metadata["category"].value,
+ "severity": metadata["severity"],
+ }
+ }
+
+ @staticmethod
+ def create_exception_error_response(
+ exception: Exception,
+ data: Any = None,
+ error_code: ErrorCode = ErrorCode.GENERAL_EXCEPTION
+ ) -> Dict[str, Any]:
+ """例外からエラーレスポンスを生成
+
+ Args:
+ exception: 発生した例外
+ data: エラー時に戻す値
+ error_code: エラーコード
+
+ Returns:
+ エラーレスポンス辞書
+ """
+ return VRCTError.create_error_response(
+ error_code=error_code,
+ data=data,
+ custom_message=f"Error: {str(exception)}",
+ details={"exception_type": type(exception).__name__}
+ )
+
+
+# エンドポイントとエラーコードのマッピング
+# UIがエラーハンドリングする際の参照として使用
+ENDPOINT_ERROR_MAPPING: Dict[str, Dict[str, ErrorCode]] = {
+ # run_mapping経由のエラー通知
+ "/run/error_device": {
+ "NO_MIC": ErrorCode.DEVICE_NO_MIC,
+ "NO_SPEAKER": ErrorCode.DEVICE_NO_SPEAKER,
+ },
+ "/run/error_translation_engine": {
+ "LIMIT": ErrorCode.TRANSLATION_ENGINE_LIMIT,
+ },
+ "/run/error_translation_chat_vram_overflow": {
+ "VRAM": ErrorCode.TRANSLATION_VRAM_CHAT,
+ },
+ "/run/error_translation_mic_vram_overflow": {
+ "VRAM": ErrorCode.TRANSLATION_VRAM_MIC,
+ },
+ "/run/error_translation_speaker_vram_overflow": {
+ "VRAM": ErrorCode.TRANSLATION_VRAM_SPEAKER,
+ },
+ "/run/error_transcription_mic_vram_overflow": {
+ "VRAM": ErrorCode.TRANSCRIPTION_VRAM_MIC,
+ },
+ "/run/error_transcription_speaker_vram_overflow": {
+ "VRAM": ErrorCode.TRANSCRIPTION_VRAM_SPEAKER,
+ },
+ "/run/error_ctranslate2_weight": {
+ "DOWNLOAD": ErrorCode.WEIGHT_CTRANSLATE2_DOWNLOAD,
+ },
+ "/run/error_whisper_weight": {
+ "DOWNLOAD": ErrorCode.WEIGHT_WHISPER_DOWNLOAD,
+ },
+
+ # エンドポイント直接のエラーレスポンス
+ "/set/data/mic_threshold": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_THRESHOLD,
+ },
+ "/set/data/speaker_threshold": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_THRESHOLD,
+ },
+ "/set/data/mic_record_timeout": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_RECORD_TIMEOUT,
+ },
+ "/set/data/mic_phrase_timeout": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_PHRASE_TIMEOUT,
+ },
+ "/set/data/mic_max_phrases": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_MIC_MAX_PHRASES,
+ },
+ "/set/data/speaker_record_timeout": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_RECORD_TIMEOUT,
+ },
+ "/set/data/speaker_phrase_timeout": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_PHRASE_TIMEOUT,
+ },
+ "/set/data/speaker_max_phrases": {
+ "OUT_OF_RANGE": ErrorCode.VALIDATION_SPEAKER_MAX_PHRASES,
+ },
+ "/set/data/osc_ip_address": {
+ "INVALID": ErrorCode.VALIDATION_INVALID_IP,
+ "CANNOT_SET": ErrorCode.VALIDATION_CANNOT_SET_IP,
+ },
+ "/set/data/deepl_auth_key": {
+ "LENGTH": ErrorCode.AUTH_DEEPL_LENGTH,
+ "FAILED": ErrorCode.AUTH_DEEPL_FAILED,
+ },
+ "/set/data/plamo_auth_key": {
+ "LENGTH": ErrorCode.AUTH_PLAMO_LENGTH,
+ "FAILED": ErrorCode.AUTH_PLAMO_FAILED,
+ },
+ "/set/data/selected_plamo_model": {
+ "INVALID": ErrorCode.MODEL_PLAMO_INVALID,
+ },
+ "/set/data/gemini_auth_key": {
+ "LENGTH": ErrorCode.AUTH_GEMINI_LENGTH,
+ "FAILED": ErrorCode.AUTH_GEMINI_FAILED,
+ },
+ "/set/data/selected_gemini_model": {
+ "INVALID": ErrorCode.MODEL_GEMINI_INVALID,
+ },
+ "/set/data/openai_auth_key": {
+ "INVALID": ErrorCode.AUTH_OPENAI_INVALID,
+ "FAILED": ErrorCode.AUTH_OPENAI_FAILED,
+ },
+ "/set/data/selected_openai_model": {
+ "INVALID": ErrorCode.MODEL_OPENAI_INVALID,
+ },
+ "/set/data/groq_auth_key": {
+ "INVALID": ErrorCode.AUTH_GROQ_INVALID,
+ "FAILED": ErrorCode.AUTH_GROQ_FAILED,
+ },
+ "/set/data/selected_groq_model": {
+ "INVALID": ErrorCode.MODEL_GROQ_INVALID,
+ },
+ "/set/data/openrouter_auth_key": {
+ "INVALID": ErrorCode.AUTH_OPENROUTER_INVALID,
+ "FAILED": ErrorCode.AUTH_OPENROUTER_FAILED,
+ },
+ "/set/data/selected_openrouter_model": {
+ "INVALID": ErrorCode.MODEL_OPENROUTER_INVALID,
+ },
+ "/run/lmstudio_connection": {
+ "FAILED": ErrorCode.CONNECTION_LMSTUDIO_FAILED,
+ },
+ "/set/data/lmstudio_url": {
+ "INVALID": ErrorCode.CONNECTION_LMSTUDIO_URL_INVALID,
+ },
+ "/set/data/selected_lmstudio_model": {
+ "INVALID": ErrorCode.MODEL_LMSTUDIO_INVALID,
+ },
+ "/run/ollama_connection": {
+ "FAILED": ErrorCode.CONNECTION_OLLAMA_FAILED,
+ },
+ "/set/data/selected_ollama_model": {
+ "INVALID": ErrorCode.MODEL_OLLAMA_INVALID,
+ },
+ "/set/data/websocket_host": {
+ "INVALID_IP": ErrorCode.VALIDATION_INVALID_IP,
+ "UNAVAILABLE": ErrorCode.WEBSOCKET_HOST_INVALID,
+ },
+ "/set/data/websocket_port": {
+ "UNAVAILABLE": ErrorCode.WEBSOCKET_PORT_UNAVAILABLE,
+ },
+ "/set/enable/websocket_server": {
+ "UNAVAILABLE": ErrorCode.WEBSOCKET_SERVER_UNAVAILABLE,
+ },
+ "/set/enable/vrc_mic_mute_sync": {
+ "OSC_DISABLED": ErrorCode.VRC_MIC_MUTE_SYNC_OSC_DISABLED,
+ },
+}
+
+
+def get_error_metadata(error_code: ErrorCode) -> Dict[str, Any]:
+ """エラーコードのメタデータを取得
+
+ Args:
+ error_code: エラーコード
+
+ Returns:
+ メタデータ辞書
+ """
+ return ERROR_METADATA.get(error_code, ERROR_METADATA[ErrorCode.GENERAL_UNKNOWN])
+
+
+def is_critical_error(error_code: ErrorCode) -> bool:
+ """クリティカルエラーかどうかを判定
+
+ Args:
+ error_code: エラーコード
+
+ Returns:
+ クリティカルエラーの場合True
+ """
+ metadata = get_error_metadata(error_code)
+ return metadata.get("severity") == "critical"
+
+
+def requires_user_action(error_code: ErrorCode) -> bool:
+ """ユーザーアクションが必要なエラーかどうかを判定
+
+ Args:
+ error_code: エラーコード
+
+ Returns:
+ ユーザーアクションが必要な場合True
+ """
+ metadata = get_error_metadata(error_code)
+ return metadata.get("user_action_required", False)
From 312999a50b75c9898c2667d52b7c9a0edf059603 Mon Sep 17 00:00:00 2001
From: misyaguziya <53165965+misyaguziya@users.noreply.github.com>
Date: Sat, 3 Jan 2026 08:35:39 +0900
Subject: [PATCH 53/55] [Update] Optimize initialization process: reduce
startup time, implement caching for model weight checks, and enhance parallel
processing for AI model checks.
---
src-python/controller.py | 510 +++++++++++++++++---------
src-python/docs/controller.md | 9 +
src-python/docs/details/controller.md | 20 +
3 files changed, 364 insertions(+), 175 deletions(-)
diff --git a/src-python/controller.py b/src-python/controller.py
index 3b97b89f..ed95ed20 100644
--- a/src-python/controller.py
+++ b/src-python/controller.py
@@ -2,6 +2,7 @@ from typing import Callable, Any, List, Optional
from time import sleep
from subprocess import Popen
from threading import Thread
+from concurrent.futures import ThreadPoolExecutor, as_completed
import re
from device_manager import device_manager
from config import config
@@ -2815,8 +2816,14 @@ class Controller:
return cleaned_text
def updateDownloadedCTranslate2ModelWeight(self) -> None:
- for weight_type in config.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT.keys():
- config.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT[weight_type] = model.checkTranslatorCTranslate2ModelWeight(weight_type)
+ # キャッシュされた結果を使用(起動時の重複チェックを回避)
+ if hasattr(self, '_ctranslate2_available_cache'):
+ # 起動時のキャッシュを使用: 選択中の重みタイプのみ設定
+ config.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT[config.CTRANSLATE2_WEIGHT_TYPE] = self._ctranslate2_available_cache
+ else:
+ # 通常時は全重みタイプをチェック
+ for weight_type in config.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT.keys():
+ config.SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT[weight_type] = model.checkTranslatorCTranslate2ModelWeight(weight_type)
def updateTranslationEngineAndEngineList(self):
engines = config.SELECTED_TRANSLATION_ENGINES
@@ -2838,8 +2845,14 @@ class Controller:
self.run(200, self.run_mapping["translation_engines"], selectable_engines)
def updateDownloadedWhisperModelWeight(self) -> None:
- for weight_type in config.SELECTABLE_WHISPER_WEIGHT_TYPE_DICT.keys():
- config.SELECTABLE_WHISPER_WEIGHT_TYPE_DICT[weight_type] = model.checkTranscriptionWhisperModelWeight(weight_type)
+ # キャッシュされた結果を使用(起動時の重複チェックを回避)
+ if hasattr(self, '_whisper_available_cache'):
+ # 起動時のキャッシュを使用: 選択中の重みタイプのみ設定
+ config.SELECTABLE_WHISPER_WEIGHT_TYPE_DICT[config.WHISPER_WEIGHT_TYPE] = self._whisper_available_cache
+ else:
+ # 通常時は全重みタイプをチェック
+ for weight_type in config.SELECTABLE_WHISPER_WEIGHT_TYPE_DICT.keys():
+ config.SELECTABLE_WHISPER_WEIGHT_TYPE_DICT[weight_type] = model.checkTranscriptionWhisperModelWeight(weight_type)
def updateTranscriptionEngine(self):
weight_type = config.WHISPER_WEIGHT_TYPE
@@ -3038,19 +3051,27 @@ class Controller:
})
def init(self, *args, **kwargs) -> None:
+ import time
+ total_start_time = time.time()
+
removeLog()
printLog("Start Initialization")
+
+ # Network check
+ section_start = time.time()
connected_network = isConnectedNetwork()
if connected_network is True:
self.connectedNetwork()
else:
self.disconnectedNetwork()
printLog(f"Connected Network: {connected_network}")
+ printLog(f"[TIME] Network Check: {time.time() - section_start:.2f}s")
self.initializationProgress(1)
+ # Download weights
if connected_network is True:
- # download CTranslate2 Model Weight
+ section_start = time.time()
printLog("Download CTranslate2 Model Weight")
weight_type = config.CTRANSLATE2_WEIGHT_TYPE
th_download_ctranslate2 = None
@@ -3059,7 +3080,6 @@ class Controller:
th_download_ctranslate2.daemon = True
th_download_ctranslate2.start()
- # download Whisper Model Weight
printLog("Download Whisper Model Weight")
weight_type = config.WHISPER_WEIGHT_TYPE
th_download_whisper = None
@@ -3072,226 +3092,352 @@ class Controller:
th_download_ctranslate2.join()
if isinstance(th_download_whisper, Thread):
th_download_whisper.join()
+ printLog(f"[TIME] Weight Download: {time.time() - section_start:.2f}s")
- if (model.checkTranslatorCTranslate2ModelWeight(config.CTRANSLATE2_WEIGHT_TYPE) is False or
- model.checkTranscriptionWhisperModelWeight(config.WHISPER_WEIGHT_TYPE) is False):
+ # Check and disable/enable AI models (parallel)
+ section_start = time.time()
+
+ def check_ctranslate2() -> bool:
+ return model.checkTranslatorCTranslate2ModelWeight(config.CTRANSLATE2_WEIGHT_TYPE) is True
+
+ def check_whisper() -> bool:
+ return model.checkTranscriptionWhisperModelWeight(config.WHISPER_WEIGHT_TYPE) is True
+
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ future_ctranslate2 = executor.submit(check_ctranslate2)
+ future_whisper = executor.submit(check_whisper)
+ ctranslate2_available = future_ctranslate2.result()
+ whisper_available = future_whisper.result()
+
+ # インスタンス変数にキャッシュ(後続の処理で再利用)
+ self._ctranslate2_available_cache = ctranslate2_available
+ self._whisper_available_cache = whisper_available
+
+ if not ctranslate2_available or not whisper_available:
self.disableAiModels()
else:
self.enableAiModels()
+ printLog(f"[TIME] AI Models Check: {time.time() - section_start:.2f}s")
+ # Init Translation Engine Status (with parallel processing)
+ section_start = time.time()
printLog("Init Translation Engine Status")
- for engine in config.SELECTABLE_TRANSLATION_ENGINE_LIST:
- match engine:
- case "CTranslate2":
- if model.checkTranslatorCTranslate2ModelWeight(config.CTRANSLATE2_WEIGHT_TYPE) is True:
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- else:
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- case "DeepL_API":
- printLog("Start check DeepL API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("DeepL API Key is valid")
+
+ # バックグラウンドチェック対象エンジン(LMStudio/Ollama)
+ background_check_engines = {"LMStudio", "Ollama"}
+
+ def check_translation_engine(engine: str) -> tuple:
+ """翻訳エンジンのステータスをチェック(並列実行用)"""
+ engine_start = time.time()
+ status = False
+ auth_key_invalid = False
+ model_list = None
+ selected_model = None
+
+ try:
+ match engine:
+ case "CTranslate2":
+ # 既に前のステップでチェック済み、結果を再利用
+ status = ctranslate2_available
+ case "DeepL_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("DeepL API Key is invalid")
- case "Plamo_API":
- printLog("Start check Plamo API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList()
- if config.SELECTED_PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST:
- config.SELECTED_PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0]
- model.setTranslatorPlamoModel(config.SELECTED_PLAMO_MODEL)
- model.updateTranslatorPlamoClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("Plamo API Key is valid")
+ if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ status = True
+ else:
+ auth_key_invalid = True
+ case "Plamo_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("Plamo API Key is invalid")
- case "Gemini_API":
- printLog("Start check Gemini API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList()
- if config.SELECTED_GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST:
- config.SELECTED_GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0]
- model.setTranslatorGeminiModel(config.SELECTED_GEMINI_MODEL)
- model.updateTranslatorGeminiClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("Gemini API Key is valid")
+ if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ model_list = model.getTranslatorPlamoModelList()
+ selected_model = config.SELECTED_PLAMO_MODEL if config.SELECTED_PLAMO_MODEL in model_list else model_list[0]
+ status = True
+ else:
+ auth_key_invalid = True
+ case "Gemini_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("Gemini API Key is invalid")
- case "OpenAI_API":
- printLog("Start check OpenAI API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorOpenAIAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList()
- if config.SELECTED_OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST:
- config.SELECTED_OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0]
- model.setTranslatorOpenAIModel(config.SELECTED_OPENAI_MODEL)
- model.updateTranslatorOpenAIClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("OpenAI API Key is valid")
+ if model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ model_list = model.getTranslatorGeminiModelList()
+ selected_model = config.SELECTED_GEMINI_MODEL if config.SELECTED_GEMINI_MODEL in model_list else model_list[0]
+ status = True
+ else:
+ auth_key_invalid = True
+ case "OpenAI_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("OpenAI API Key is invalid")
- case "Groq_API":
- printLog("Start check Groq API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorGroqAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_GROQ_MODEL_LIST = model.getTranslatorGroqModelList()
- if config.SELECTED_GROQ_MODEL not in config.SELECTABLE_GROQ_MODEL_LIST:
- config.SELECTED_GROQ_MODEL = config.SELECTABLE_GROQ_MODEL_LIST[0]
- model.setTranslatorGroqModel(config.SELECTED_GROQ_MODEL)
- model.updateTranslatorGroqClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("Groq API Key is valid")
+ if model.authenticationTranslatorOpenAIAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ model_list = model.getTranslatorOpenAIModelList()
+ selected_model = config.SELECTED_OPENAI_MODEL if config.SELECTED_OPENAI_MODEL in model_list else model_list[0]
+ status = True
+ else:
+ auth_key_invalid = True
+ case "Groq_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("Groq API Key is invalid")
- case "OpenRouter_API":
- printLog("Start check OpenRouter API Key")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
- if config.AUTH_KEYS[engine] is not None:
- if model.authenticationTranslatorOpenRouterAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
- config.SELECTABLE_OPENROUTER_MODEL_LIST = model.getTranslatorOpenRouterModelList()
- if config.SELECTED_OPENROUTER_MODEL not in config.SELECTABLE_OPENROUTER_MODEL_LIST:
- config.SELECTED_OPENROUTER_MODEL = config.SELECTABLE_OPENROUTER_MODEL_LIST[0]
- model.setTranslatorOpenRouterModel(config.SELECTED_OPENROUTER_MODEL)
- model.updateTranslatorOpenRouterClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("OpenRouter API Key is valid")
+ if model.authenticationTranslatorGroqAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ model_list = model.getTranslatorGroqModelList()
+ selected_model = config.SELECTED_GROQ_MODEL if config.SELECTED_GROQ_MODEL in model_list else model_list[0]
+ status = True
+ else:
+ auth_key_invalid = True
+ case "OpenRouter_API":
+ if config.AUTH_KEYS[engine] is None:
+ status = False
else:
- # error update Auth key
- auth_keys = config.AUTH_KEYS
- auth_keys[engine] = None
- config.AUTH_KEYS = auth_keys
- printLog("OpenRouter API Key is invalid")
- case "LMStudio":
- printLog("Start check LMStudio Server")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
+ if model.authenticationTranslatorOpenRouterAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
+ model_list = model.getTranslatorOpenRouterModelList()
+ selected_model = config.SELECTED_OPENROUTER_MODEL if config.SELECTED_OPENROUTER_MODEL in model_list else model_list[0]
+ status = True
+ else:
+ auth_key_invalid = True
+ case "LMStudio":
+ # バックグラウンドチェックにスキップ
+ status = False
+ case "Ollama":
+ # バックグラウンドチェックにスキップ
+ status = False
+ case _:
+ status = connected_network is True
+ except Exception as e:
+ printLog(f"Error checking engine {engine}: {str(e)}")
+ errorLogging()
+ status = False
+
+ elapsed = time.time() - engine_start
+ return engine, status, auth_key_invalid, model_list, selected_model, elapsed
+
+ def check_local_server_engine_background(engine: str):
+ """ローカルサーバー系エンジンをバックグラウンドでチェック"""
+ try:
+ printLog(f"[Background] Start check {engine}")
+ engine_start = time.time()
+ status = False
+ model_list = None
+ selected_model = None
+
+ if engine == "LMStudio":
if config.LMSTUDIO_URL is not None:
if model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL) is True:
- config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
- if len(config.SELECTABLE_LMSTUDIO_MODEL_LIST) == 0:
- printLog("LMStudio model list is empty")
- break
- if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
- config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
- model.setTranslatorLMStudioModel(config.SELECTED_LMSTUDIO_MODEL)
- model.updateTranslatorLMStudioClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("LMStudio is available")
- else:
- printLog("LMStudio is not available")
- case "Ollama":
- printLog("Start check Ollama Server")
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
+ model_list = model.getTranslatorLMStudioModelList()
+ if len(model_list) > 0:
+ selected_model = config.SELECTED_LMSTUDIO_MODEL if config.SELECTED_LMSTUDIO_MODEL in model_list else model_list[0]
+ config.SELECTABLE_LMSTUDIO_MODEL_LIST = model_list
+ config.SELECTED_LMSTUDIO_MODEL = selected_model
+ model.setTranslatorLMStudioModel(selected_model)
+ model.updateTranslatorLMStudioClient()
+ status = True
+ elif engine == "Ollama":
if model.authenticationTranslatorOllama() is True:
- config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList()
- if len(config.SELECTABLE_OLLAMA_MODEL_LIST) == 0:
- printLog("Ollama model list is empty")
- break
- if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST:
- config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0]
- model.setTranslatorOllamaModel(config.SELECTED_OLLAMA_MODEL)
- model.updateTranslatorOllamaClient()
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- printLog("Ollama is available")
- else:
- printLog("Ollama is not available")
- case _:
- if connected_network is True:
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
- else:
- config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
+ model_list = model.getTranslatorOllamaModelList()
+ if len(model_list) > 0:
+ selected_model = config.SELECTED_OLLAMA_MODEL if config.SELECTED_OLLAMA_MODEL in model_list else model_list[0]
+ config.SELECTABLE_OLLAMA_MODEL_LIST = model_list
+ config.SELECTED_OLLAMA_MODEL = selected_model
+ model.setTranslatorOllamaModel(selected_model)
+ model.updateTranslatorOllamaClient()
+ status = True
+
+ config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = status
+ elapsed = time.time() - engine_start
+ printLog(f"[Background] {engine} check completed: {status} ({elapsed:.2f}s)")
+
+ # 更新通知(もしrun_mappingがあれば)
+ if status:
+ self.updateTranslationEngineAndEngineList()
+ except Exception as e:
+ printLog(f"[Background] Error checking {engine}: {str(e)}")
+ errorLogging()
+
+ # 並列実行(バックグラウンドチェック対象を除外)
+ engine_results = {}
+ engines_to_check = [e for e in config.SELECTABLE_TRANSLATION_ENGINE_LIST if e not in background_check_engines]
+
+ with ThreadPoolExecutor(max_workers=4) as executor:
+ future_to_engine = {executor.submit(check_translation_engine, engine): engine
+ for engine in engines_to_check}
+
+ for future in as_completed(future_to_engine):
+ engine, status, auth_key_invalid, model_list, selected_model, elapsed = future.result()
+ engine_results[engine] = (status, auth_key_invalid, model_list, selected_model, elapsed)
+
+ # バックグラウンドチェック対象エンジンは初期値Falseで即座に設定
+ for engine in background_check_engines:
+ if engine in config.SELECTABLE_TRANSLATION_ENGINE_LIST:
+ config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
+ printLog(f"Start check {engine}")
+ printLog(f"[TIME] Engine '{engine}': 0.00s (deferred to background)")
+ # バックグラウンドスレッドで実行
+ bg_thread = Thread(target=check_local_server_engine_background, args=(engine,))
+ bg_thread.daemon = True
+ bg_thread.start()
+
+ # 結果を順番に適用(メインスレッドで実行)
+ for engine in engines_to_check:
+ if engine not in engine_results:
+ continue
+
+ status, auth_key_invalid, model_list, selected_model, elapsed = engine_results[engine]
+
+ # ログ出力
+ printLog(f"Start check {engine}")
+
+ # ステータス設定
+ config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = status
+
+ # 認証キー無効化
+ if auth_key_invalid:
+ auth_keys = config.AUTH_KEYS
+ auth_keys[engine] = None
+ config.AUTH_KEYS = auth_keys
+ printLog(f"{engine} auth key is invalid")
+ elif status:
+ printLog(f"{engine} is valid/available")
+
+ # モデルリストと選択モデルの設定
+ if model_list is not None and status:
+ match engine:
+ case "Plamo_API":
+ config.SELECTABLE_PLAMO_MODEL_LIST = model_list
+ config.SELECTED_PLAMO_MODEL = selected_model
+ model.setTranslatorPlamoModel(selected_model)
+ model.updateTranslatorPlamoClient()
+ case "Gemini_API":
+ config.SELECTABLE_GEMINI_MODEL_LIST = model_list
+ config.SELECTED_GEMINI_MODEL = selected_model
+ model.setTranslatorGeminiModel(selected_model)
+ model.updateTranslatorGeminiClient()
+ case "OpenAI_API":
+ config.SELECTABLE_OPENAI_MODEL_LIST = model_list
+ config.SELECTED_OPENAI_MODEL = selected_model
+ model.setTranslatorOpenAIModel(selected_model)
+ model.updateTranslatorOpenAIClient()
+ case "Groq_API":
+ config.SELECTABLE_GROQ_MODEL_LIST = model_list
+ config.SELECTED_GROQ_MODEL = selected_model
+ model.setTranslatorGroqModel(selected_model)
+ model.updateTranslatorGroqClient()
+ case "OpenRouter_API":
+ config.SELECTABLE_OPENROUTER_MODEL_LIST = model_list
+ config.SELECTED_OPENROUTER_MODEL = selected_model
+ model.setTranslatorOpenRouterModel(selected_model)
+ model.updateTranslatorOpenRouterClient()
+
+ printLog(f"[TIME] Engine '{engine}': {elapsed:.2f}s")
+
+ printLog(f"[TIME] Translation Engine Status Init: {time.time() - section_start:.2f}s")
+ # Init Transcription Engine Status
+ section_start = time.time()
for engine in config.SELECTABLE_TRANSCRIPTION_ENGINE_LIST:
match engine:
case "Whisper":
- if model.checkTranscriptionWhisperModelWeight(config.WHISPER_WEIGHT_TYPE) is True:
- config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = True
- else:
- config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False
+ # キャッシュされた結果を使用(重複チェックを回避)
+ config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = self._whisper_available_cache
case _:
if connected_network is True:
config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = True
else:
config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False
+ printLog(f"[TIME] Transcription Engine Status Init: {time.time() - section_start:.2f}s")
self.initializationProgress(2)
- # set Translation Engine
+ # Set Translation Engine
+ section_start = time.time()
printLog("Set Translation Engine")
self.updateDownloadedCTranslate2ModelWeight()
self.updateTranslationEngineAndEngineList()
+ printLog(f"[TIME] Set Translation Engine: {time.time() - section_start:.2f}s")
- # set Transcription Engine
+ # Set Transcription Engine
+ section_start = time.time()
printLog("Set Transcription Engine")
self.updateDownloadedWhisperModelWeight()
self.updateTranscriptionEngine()
+ printLog(f"[TIME] Set Transcription Engine: {time.time() - section_start:.2f}s")
- # set Transliteration status
+ # Set Transliteration
+ section_start = time.time()
printLog("Set Transliteration")
if config.CONVERT_MESSAGE_TO_ROMAJI is True or config.CONVERT_MESSAGE_TO_HIRAGANA is True:
model.startTransliteration()
+ printLog(f"[TIME] Set Transliteration: {time.time() - section_start:.2f}s")
self.initializationProgress(3)
- # set word filter
+ # Set Word Filter
+ section_start = time.time()
printLog("Set Word Filter")
model.addKeywords()
+ printLog(f"[TIME] Set Word Filter: {time.time() - section_start:.2f}s")
- # check Software Updated
- printLog("Check Software Updated")
- self.checkSoftwareUpdated()
+ # Check Software Updated (Background)
+ section_start = time.time()
+ printLog("Check Software Updated (Background)")
+
+ def check_software_updated_background():
+ """ソフトウェア更新チェックをバックグラウンドで実行"""
+ bg_start = time.time()
+ try:
+ self.checkSoftwareUpdated()
+ printLog(f"[Background] Software update check completed: {time.time() - bg_start:.2f}s")
+ except Exception:
+ errorLogging()
+ printLog("[Background] Software update check failed")
+
+ bg_thread = Thread(target=check_software_updated_background)
+ bg_thread.daemon = True
+ bg_thread.start()
+ printLog(f"[TIME] Check Software Updated (Background): {time.time() - section_start:.2f}s")
- # init logger
+ # Init Logger
+ section_start = time.time()
printLog("Init Logger")
if config.LOGGER_FEATURE is True:
model.startLogger()
+ printLog(f"[TIME] Init Logger: {time.time() - section_start:.2f}s")
self.initializationProgress(4)
- # init OSC receive
- printLog("Init OSC Receive")
- model.startReceiveOSC()
- osc_query_enabled = model.getIsOscQueryEnabled()
- if osc_query_enabled is True:
- self.enableOscQuery()
- if config.VRC_MIC_MUTE_SYNC is True:
- self.setEnableVrcMicMuteSync()
- else:
- # OSC Query is disabled, so disable VRC some features
- mute_sync_info_flag = False
- if config.VRC_MIC_MUTE_SYNC is True:
- self.setDisableVrcMicMuteSync()
- mute_sync_info_flag = True
- self.disableOscQuery(mute_sync_info=mute_sync_info_flag)
+ # Init OSC Receive (Background)
+ section_start = time.time()
+ printLog("Init OSC Receive (Background)")
+
+ def init_osc_receive_background():
+ """OSC Receiveの初期化をバックグラウンドで実行"""
+ bg_start = time.time()
+ try:
+ model.startReceiveOSC()
+ osc_query_enabled = model.getIsOscQueryEnabled()
+ if osc_query_enabled is True:
+ self.enableOscQuery()
+ if config.VRC_MIC_MUTE_SYNC is True:
+ self.setEnableVrcMicMuteSync()
+ else:
+ # OSC Query is disabled, so disable VRC some features
+ mute_sync_info_flag = False
+ if config.VRC_MIC_MUTE_SYNC is True:
+ self.setDisableVrcMicMuteSync()
+ mute_sync_info_flag = True
+ self.disableOscQuery(mute_sync_info=mute_sync_info_flag)
+ printLog(f"[Background] OSC Receive initialization completed: {time.time() - bg_start:.2f}s")
+ except Exception:
+ errorLogging()
+ printLog("[Background] OSC Receive initialization failed")
+
+ bg_thread = Thread(target=init_osc_receive_background)
+ bg_thread.daemon = True
+ bg_thread.start()
+ printLog(f"[TIME] Init OSC Receive (Background): {time.time() - section_start:.2f}s")
- # init Auto device selection
+ # Init Device Manager
+ section_start = time.time()
printLog("Init Device Manager")
device_manager.setCallbackHostList(self.updateMicHostList)
device_manager.setCallbackMicDeviceList(self.updateMicDeviceList)
@@ -3302,11 +3448,17 @@ class Controller:
self.applyAutoMicSelect()
if config.AUTO_SPEAKER_SELECT is True:
self.applyAutoSpeakerSelect()
+ printLog(f"[TIME] Init Device Manager: {time.time() - section_start:.2f}s")
+ # Init Overlay
+ section_start = time.time()
printLog("Init Overlay")
if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True):
model.startOverlay()
+ printLog(f"[TIME] Init Overlay: {time.time() - section_start:.2f}s")
+ # Init WebSocket Server
+ section_start = time.time()
printLog("Init WebSocket Server")
if config.WEBSOCKET_SERVER is True:
if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True:
@@ -3315,12 +3467,20 @@ class Controller:
config.WEBSOCKET_SERVER = False
model.stopWebSocketServer()
printLog("WebSocket server host or port is not available")
+ printLog(f"[TIME] Init WebSocket Server: {time.time() - section_start:.2f}s")
+ # Revalidate Selected Models
+ section_start = time.time()
printLog("Revalidate Selected Models")
config.revalidate_selected_models()
+ printLog(f"[TIME] Revalidate Selected Models: {time.time() - section_start:.2f}s")
+ # Update Settings
+ section_start = time.time()
printLog("Update settings")
self.updateConfigSettings()
+ printLog(f"[TIME] Update settings: {time.time() - section_start:.2f}s")
printLog("End Initialization")
+ printLog(f"[TIME] Total Initialization: {time.time() - total_start_time:.2f}s")
self.startWatchdog()
\ No newline at end of file
diff --git a/src-python/docs/controller.md b/src-python/docs/controller.md
index ab789ee0..d34dc9e3 100644
--- a/src-python/docs/controller.md
+++ b/src-python/docs/controller.md
@@ -4,6 +4,15 @@
`controller.py` は VRCT アプリケーションのビジネスロジック層であり、フロントエンド(UI)とバックエンド(Model)の間の制御フローを担当する。音声認識、翻訳、OSC通信、オーバーレイ表示など、VRCT の全機能の調整役として動作し、各種設定の取得・更新、デバイス管理、エラーハンドリングを提供する。
+## 最近の更新 (2026-01-03)
+
+- 起動高速化: 初期化時間を約12.6s→8.9sに短縮
+- AI Models Check 並列化: CTranslate2/Whisperの重みチェックを2並列で実行
+- 翻訳エンジン判定の非同期化: LMStudio/Ollamaをバックグラウンド判定、他APIは4並列
+- 重みチェック結果のキャッシュ: `_ctranslate2_available_cache` / `_whisper_available_cache` を導入し後続処理で再利用
+- 音声認識エンジン判定の高速化: Whisperはキャッシュ結果を利用し0.56s→0.00s
+- ソフトウェア更新チェックの非同期化: GitHub APIチェックをバックグラウンド化
+
## アーキテクチャ上の位置づけ
```
diff --git a/src-python/docs/details/controller.md b/src-python/docs/details/controller.md
index 3dd3e73b..b2b272d1 100644
--- a/src-python/docs/details/controller.md
+++ b/src-python/docs/details/controller.md
@@ -4,6 +4,26 @@
VRCTアプリケーションのビジネスロジックを制御するコントローラークラスです。UI層とモデル層の間に位置し、ユーザーの入力を適切な処理に変換し、結果を UI に返す役割を担います。全ての機能制御、設定管理、状態管理を一元的に行います。
+## 最近の更新 (2026-01-03)
+
+### 起動高速化・非同期化
+
+- 初期化時間を約12.6s→8.9sに短縮(環境計測値)
+- AI Models Check を2並列化(CTranslate2/Whisper)し、結果を `_ctranslate2_available_cache` / `_whisper_available_cache` に保存
+- 翻訳エンジン判定を並列化(ThreadPoolExecutor, max_workers=4)し、LMStudio/Ollamaはバックグラウンド判定に変更
+- ソフトウェア更新チェックをバックグラウンド化
+- OSC受信初期化をバックグラウンド化し、OSCQueryサービス生成は接続成功まで継続リトライ
+- 翻訳/音声認識エンジンのセット処理で重みチェックキャッシュを再利用し再計測を排除(0.98s/0.52s→0.00s)
+
+### 影響
+
+| 項目 | 内容 |
+|------|------|
+| 起動時間 | 約3.7s短縮(12.6s→8.9s) |
+| 並列・非同期化 | 翻訳・音声認識エンジン判定を並列/バックグラウンド化 |
+| 安定性 | OSCQuery起動のリトライ上限でブロッキングを抑制 |
+| 再利用性 | 重みチェック結果をキャッシュし重複I/Oを削減 |
+
## 最近の更新 (2025-10-20)
### 新規ローカルLLM翻訳エンジン統合
From dd1c47069ce3621505c2cf0c02e2f4d621062155 Mon Sep 17 00:00:00 2001
From: misyaguziya <53165965+misyaguziya@users.noreply.github.com>
Date: Sat, 3 Jan 2026 09:48:14 +0900
Subject: [PATCH 54/55] =?UTF-8?q?[Update]=20=E3=82=A8=E3=83=A9=E3=83=BC?=
=?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AE?=
=?UTF-8?q?=E6=A7=8B=E9=80=A0=E3=82=92=E7=B5=B1=E4=B8=80=E3=81=97=E3=80=81?=
=?UTF-8?q?VRAM=E4=B8=8D=E8=B6=B3=E3=82=84=E6=8E=A5=E7=B6=9A=E3=82=A8?=
=?UTF-8?q?=E3=83=A9=E3=83=BC=E3=81=AE=E9=80=9A=E7=9F=A5=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src-python/docs/details/controller.md | 28 +++++++++++++++------------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/src-python/docs/details/controller.md b/src-python/docs/details/controller.md
index b2b272d1..79f7e751 100644
--- a/src-python/docs/details/controller.md
+++ b/src-python/docs/details/controller.md
@@ -419,25 +419,29 @@ speakerMessage(result: dict) -> None
## エラーハンドリング
-### VRAM不足エラー
+### エラー構造
+- すべて `VRCTError` で生成し、ステータス・コード・メッセージ・data を統一
+- `create_error_response()` / `create_exception_error_response()` を使用し、`self.run()` へそのまま渡す
+- 代表コード: デバイス系 (`DEVICE_NO_MIC` / `DEVICE_NO_SPEAKER`)、VRAM系 (`TRANSLATION_VRAM_*`)、認証系 (`AUTH_*`)、モデル不正 (`MODEL_*`)、バリデーション系 (`VALIDATION_*`)、接続系 (`CONNECTION_LMSTUDIO_FAILED` など)
-- 自動的にCTranslate2への切り替え
-- ユーザーへの適切な通知
+### VRAM不足エラー
+- 翻訳処理中に VRAM 例外を検出し `/run/error_translation_*_vram_overflow` で通知
+- 翻訳機能を自動で無効化し、`TRANSLATION_DISABLED_VRAM` を通知
+- マイク/スピーカー/チャット/有効化時の各パスで専用コードを返却
### デバイスエラー
+- マイク・スピーカー未検出時に `DEVICE_NO_MIC` / `DEVICE_NO_SPEAKER`
+- エネルギーしきい値/タイムアウト等のバリデーションに `VALIDATION_*` を使用
-- デバイス接続状態の監視
-- 自動復旧機能
+### 認証・モデルエラー
+- DeepL/Plamo/Gemini/OpenAI/Groq/OpenRouter の認証失敗やキー長不正を `AUTH_*` で通知
+- モデル未選択/不正時は `MODEL_*` で通知し、選択リストを再送
-### ネットワークエラー
-
-- 接続状態の定期確認
-- オフライン機能への切り替え
+### 接続エラー
+- LMStudio/Ollama 接続失敗を `CONNECTION_*` で通知し、翻訳エンジンリストを更新
### 設定エラー
-
-- 設定値の妥当性チェック
-- デフォルト値への復帰
+- IP アドレスやしきい値などの不正値を `VALIDATION_*` で統一し、リクエスト値を data に格納
## パフォーマンス考慮事項
From c60995a0fe03b1232012a5721444f8bd8b71bda2 Mon Sep 17 00:00:00 2001
From: misyaguziya <53165965+misyaguziya@users.noreply.github.com>
Date: Sun, 4 Jan 2026 14:58:02 +0900
Subject: [PATCH 55/55] =?UTF-8?q?[Update]=20=E5=88=9D=E6=9C=9F=E5=8C=96?=
=?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=BB=E3=82=B9=E3=81=AE=E3=83=AD=E3=82=B0?=
=?UTF-8?q?=E5=87=BA=E5=8A=9B=E3=82=92=E6=95=B4=E7=90=86=E3=81=97=E3=80=81?=
=?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E6=99=82=E9=96=93=E8=A8=88=E6=B8=AC?=
=?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src-python/controller.py | 117 +++++++++++++--------------------------
1 file changed, 37 insertions(+), 80 deletions(-)
diff --git a/src-python/controller.py b/src-python/controller.py
index ed95ed20..8d1c53c9 100644
--- a/src-python/controller.py
+++ b/src-python/controller.py
@@ -3051,27 +3051,21 @@ class Controller:
})
def init(self, *args, **kwargs) -> None:
- import time
- total_start_time = time.time()
-
removeLog()
printLog("Start Initialization")
-
+
# Network check
- section_start = time.time()
connected_network = isConnectedNetwork()
if connected_network is True:
self.connectedNetwork()
else:
self.disconnectedNetwork()
printLog(f"Connected Network: {connected_network}")
- printLog(f"[TIME] Network Check: {time.time() - section_start:.2f}s")
self.initializationProgress(1)
# Download weights
if connected_network is True:
- section_start = time.time()
printLog("Download CTranslate2 Model Weight")
weight_type = config.CTRANSLATE2_WEIGHT_TYPE
th_download_ctranslate2 = None
@@ -3092,10 +3086,8 @@ class Controller:
th_download_ctranslate2.join()
if isinstance(th_download_whisper, Thread):
th_download_whisper.join()
- printLog(f"[TIME] Weight Download: {time.time() - section_start:.2f}s")
# Check and disable/enable AI models (parallel)
- section_start = time.time()
def check_ctranslate2() -> bool:
return model.checkTranslatorCTranslate2ModelWeight(config.CTRANSLATE2_WEIGHT_TYPE) is True
@@ -3108,32 +3100,29 @@ class Controller:
future_whisper = executor.submit(check_whisper)
ctranslate2_available = future_ctranslate2.result()
whisper_available = future_whisper.result()
-
+
# インスタンス変数にキャッシュ(後続の処理で再利用)
self._ctranslate2_available_cache = ctranslate2_available
self._whisper_available_cache = whisper_available
-
+
if not ctranslate2_available or not whisper_available:
self.disableAiModels()
else:
self.enableAiModels()
- printLog(f"[TIME] AI Models Check: {time.time() - section_start:.2f}s")
# Init Translation Engine Status (with parallel processing)
- section_start = time.time()
printLog("Init Translation Engine Status")
-
+
# バックグラウンドチェック対象エンジン(LMStudio/Ollama)
background_check_engines = {"LMStudio", "Ollama"}
-
+
def check_translation_engine(engine: str) -> tuple:
"""翻訳エンジンのステータスをチェック(並列実行用)"""
- engine_start = time.time()
status = False
auth_key_invalid = False
model_list = None
selected_model = None
-
+
try:
match engine:
case "CTranslate2":
@@ -3209,19 +3198,17 @@ class Controller:
printLog(f"Error checking engine {engine}: {str(e)}")
errorLogging()
status = False
-
- elapsed = time.time() - engine_start
- return engine, status, auth_key_invalid, model_list, selected_model, elapsed
-
+
+ return engine, status, auth_key_invalid, model_list, selected_model
+
def check_local_server_engine_background(engine: str):
"""ローカルサーバー系エンジンをバックグラウンドでチェック"""
try:
printLog(f"[Background] Start check {engine}")
- engine_start = time.time()
status = False
model_list = None
selected_model = None
-
+
if engine == "LMStudio":
if config.LMSTUDIO_URL is not None:
if model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL) is True:
@@ -3243,54 +3230,53 @@ class Controller:
model.setTranslatorOllamaModel(selected_model)
model.updateTranslatorOllamaClient()
status = True
-
+
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = status
- elapsed = time.time() - engine_start
- printLog(f"[Background] {engine} check completed: {status} ({elapsed:.2f}s)")
-
+ printLog(f"[Background] {engine} check completed: {status}")
+
# 更新通知(もしrun_mappingがあれば)
if status:
self.updateTranslationEngineAndEngineList()
except Exception as e:
printLog(f"[Background] Error checking {engine}: {str(e)}")
errorLogging()
-
+
# 並列実行(バックグラウンドチェック対象を除外)
engine_results = {}
engines_to_check = [e for e in config.SELECTABLE_TRANSLATION_ENGINE_LIST if e not in background_check_engines]
-
+
with ThreadPoolExecutor(max_workers=4) as executor:
future_to_engine = {executor.submit(check_translation_engine, engine): engine
for engine in engines_to_check}
-
+
for future in as_completed(future_to_engine):
- engine, status, auth_key_invalid, model_list, selected_model, elapsed = future.result()
- engine_results[engine] = (status, auth_key_invalid, model_list, selected_model, elapsed)
-
+ engine, status, auth_key_invalid, model_list, selected_model = future.result()
+ engine_results[engine] = (status, auth_key_invalid, model_list, selected_model)
+
# バックグラウンドチェック対象エンジンは初期値Falseで即座に設定
for engine in background_check_engines:
if engine in config.SELECTABLE_TRANSLATION_ENGINE_LIST:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
printLog(f"Start check {engine}")
- printLog(f"[TIME] Engine '{engine}': 0.00s (deferred to background)")
+ printLog(f"Engine '{engine}' deferred to background check")
# バックグラウンドスレッドで実行
bg_thread = Thread(target=check_local_server_engine_background, args=(engine,))
bg_thread.daemon = True
bg_thread.start()
-
+
# 結果を順番に適用(メインスレッドで実行)
for engine in engines_to_check:
if engine not in engine_results:
continue
-
- status, auth_key_invalid, model_list, selected_model, elapsed = engine_results[engine]
-
+
+ status, auth_key_invalid, model_list, selected_model = engine_results[engine]
+
# ログ出力
printLog(f"Start check {engine}")
-
+
# ステータス設定
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = status
-
+
# 認証キー無効化
if auth_key_invalid:
auth_keys = config.AUTH_KEYS
@@ -3299,7 +3285,7 @@ class Controller:
printLog(f"{engine} auth key is invalid")
elif status:
printLog(f"{engine} is valid/available")
-
+
# モデルリストと選択モデルの設定
if model_list is not None and status:
match engine:
@@ -3328,13 +3314,12 @@ class Controller:
config.SELECTED_OPENROUTER_MODEL = selected_model
model.setTranslatorOpenRouterModel(selected_model)
model.updateTranslatorOpenRouterClient()
-
- printLog(f"[TIME] Engine '{engine}': {elapsed:.2f}s")
-
- printLog(f"[TIME] Translation Engine Status Init: {time.time() - section_start:.2f}s")
+
+ printLog(f"{engine} check completed")
+
+ printLog("Translation Engine Status Init completed")
# Init Transcription Engine Status
- section_start = time.time()
for engine in config.SELECTABLE_TRANSCRIPTION_ENGINE_LIST:
match engine:
case "Whisper":
@@ -3345,73 +3330,57 @@ class Controller:
config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = True
else:
config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False
- printLog(f"[TIME] Transcription Engine Status Init: {time.time() - section_start:.2f}s")
self.initializationProgress(2)
# Set Translation Engine
- section_start = time.time()
printLog("Set Translation Engine")
self.updateDownloadedCTranslate2ModelWeight()
self.updateTranslationEngineAndEngineList()
- printLog(f"[TIME] Set Translation Engine: {time.time() - section_start:.2f}s")
# Set Transcription Engine
- section_start = time.time()
printLog("Set Transcription Engine")
self.updateDownloadedWhisperModelWeight()
self.updateTranscriptionEngine()
- printLog(f"[TIME] Set Transcription Engine: {time.time() - section_start:.2f}s")
# Set Transliteration
- section_start = time.time()
printLog("Set Transliteration")
if config.CONVERT_MESSAGE_TO_ROMAJI is True or config.CONVERT_MESSAGE_TO_HIRAGANA is True:
model.startTransliteration()
- printLog(f"[TIME] Set Transliteration: {time.time() - section_start:.2f}s")
self.initializationProgress(3)
# Set Word Filter
- section_start = time.time()
printLog("Set Word Filter")
model.addKeywords()
- printLog(f"[TIME] Set Word Filter: {time.time() - section_start:.2f}s")
# Check Software Updated (Background)
- section_start = time.time()
printLog("Check Software Updated (Background)")
-
+
def check_software_updated_background():
"""ソフトウェア更新チェックをバックグラウンドで実行"""
- bg_start = time.time()
try:
self.checkSoftwareUpdated()
- printLog(f"[Background] Software update check completed: {time.time() - bg_start:.2f}s")
+ printLog("[Background] Software update check completed")
except Exception:
errorLogging()
printLog("[Background] Software update check failed")
-
+
bg_thread = Thread(target=check_software_updated_background)
bg_thread.daemon = True
bg_thread.start()
- printLog(f"[TIME] Check Software Updated (Background): {time.time() - section_start:.2f}s")
# Init Logger
- section_start = time.time()
printLog("Init Logger")
if config.LOGGER_FEATURE is True:
model.startLogger()
- printLog(f"[TIME] Init Logger: {time.time() - section_start:.2f}s")
self.initializationProgress(4)
# Init OSC Receive (Background)
- section_start = time.time()
printLog("Init OSC Receive (Background)")
-
+
def init_osc_receive_background():
"""OSC Receiveの初期化をバックグラウンドで実行"""
- bg_start = time.time()
try:
model.startReceiveOSC()
osc_query_enabled = model.getIsOscQueryEnabled()
@@ -3426,18 +3395,16 @@ class Controller:
self.setDisableVrcMicMuteSync()
mute_sync_info_flag = True
self.disableOscQuery(mute_sync_info=mute_sync_info_flag)
- printLog(f"[Background] OSC Receive initialization completed: {time.time() - bg_start:.2f}s")
+ printLog("[Background] OSC Receive initialization completed")
except Exception:
errorLogging()
printLog("[Background] OSC Receive initialization failed")
-
+
bg_thread = Thread(target=init_osc_receive_background)
bg_thread.daemon = True
bg_thread.start()
- printLog(f"[TIME] Init OSC Receive (Background): {time.time() - section_start:.2f}s")
# Init Device Manager
- section_start = time.time()
printLog("Init Device Manager")
device_manager.setCallbackHostList(self.updateMicHostList)
device_manager.setCallbackMicDeviceList(self.updateMicDeviceList)
@@ -3448,17 +3415,13 @@ class Controller:
self.applyAutoMicSelect()
if config.AUTO_SPEAKER_SELECT is True:
self.applyAutoSpeakerSelect()
- printLog(f"[TIME] Init Device Manager: {time.time() - section_start:.2f}s")
# Init Overlay
- section_start = time.time()
printLog("Init Overlay")
if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True):
model.startOverlay()
- printLog(f"[TIME] Init Overlay: {time.time() - section_start:.2f}s")
# Init WebSocket Server
- section_start = time.time()
printLog("Init WebSocket Server")
if config.WEBSOCKET_SERVER is True:
if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True:
@@ -3467,20 +3430,14 @@ class Controller:
config.WEBSOCKET_SERVER = False
model.stopWebSocketServer()
printLog("WebSocket server host or port is not available")
- printLog(f"[TIME] Init WebSocket Server: {time.time() - section_start:.2f}s")
# Revalidate Selected Models
- section_start = time.time()
printLog("Revalidate Selected Models")
config.revalidate_selected_models()
- printLog(f"[TIME] Revalidate Selected Models: {time.time() - section_start:.2f}s")
# Update Settings
- section_start = time.time()
printLog("Update settings")
self.updateConfigSettings()
- printLog(f"[TIME] Update settings: {time.time() - section_start:.2f}s")
printLog("End Initialization")
- printLog(f"[TIME] Total Initialization: {time.time() - total_start_time:.2f}s")
self.startWatchdog()
\ No newline at end of file