Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942a257e79 | ||
|
|
6b2be0f22b | ||
|
|
242c022e33 | ||
|
|
0765908184 | ||
|
|
f6a3b0e8fd | ||
|
|
5811158231 | ||
|
|
0cb2b7e6ae | ||
|
|
623d17e885 | ||
|
|
4c95b66dd8 | ||
|
|
1656620ce4 | ||
|
|
597b0e15b0 | ||
|
|
6743735086 | ||
|
|
096dd2c10b | ||
|
|
50c85ccd75 | ||
|
|
d4036e758c | ||
|
|
0921ef578b | ||
|
|
6d34d6b041 | ||
|
|
4e5acd4ac3 | ||
|
|
ba0f834fbd | ||
|
|
c257591b21 | ||
|
|
d39d5a198e | ||
|
|
19f5c4fa75 | ||
|
|
0f9f7ec3f3 | ||
|
|
7735d2b485 | ||
|
|
261a1deed4 | ||
|
|
9987ca8dde | ||
|
|
c6d678e29e | ||
|
|
44f7a414ad | ||
|
|
a31e47bf85 | ||
|
|
7dab5c4c06 | ||
|
|
4559d83766 | ||
|
|
40d2678c23 | ||
|
|
bf7d2e61a7 | ||
|
|
7834a98a54 | ||
|
|
bdd43eb3cb | ||
|
|
d51b069bb2 | ||
|
|
2d7f9b35ad | ||
|
|
c2a906fca8 | ||
|
|
6d827ff2fb | ||
|
|
c6d5b6c6a8 | ||
|
|
de79f2799f | ||
|
|
718706b2e4 | ||
|
|
1bbc9e6182 | ||
|
|
4f89f0d328 | ||
|
|
6415656853 | ||
|
|
c371eeff96 | ||
|
|
19af0ea918 | ||
|
|
7336d01469 | ||
|
|
4b29b283a1 | ||
|
|
c11999c933 | ||
|
|
3f646a97d8 | ||
|
|
d4a5a1ca6f | ||
|
|
d157dc8b7b | ||
|
|
4eb61fa4b7 | ||
|
|
300c0d4648 | ||
|
|
c60995a0fe | ||
|
|
dd1c47069c | ||
|
|
312999a50b | ||
|
|
b0cf8bf335 | ||
|
|
cab7d48de1 | ||
|
|
3a009ce336 | ||
|
|
457b5e04ee | ||
|
|
43f0bf6027 | ||
|
|
3168a8ad26 | ||
|
|
3d71dbacce | ||
|
|
5ec12a5b57 | ||
|
|
fdb1c0df75 | ||
|
|
0325619407 | ||
|
|
74ba63e787 | ||
|
|
263a8ad0d0 | ||
|
|
3771081210 | ||
|
|
2a5044b86a | ||
|
|
c806c0f93b | ||
|
|
01061cf98f | ||
|
|
588b95eebe | ||
|
|
6014c2d362 | ||
|
|
4031a5556d | ||
|
|
bf1c0236b5 | ||
|
|
063b79477f | ||
|
|
1a62ca04f0 | ||
|
|
214edb083d | ||
|
|
316843f875 | ||
|
|
bf9b28d199 | ||
|
|
22e6af860d | ||
|
|
4f2401551a | ||
|
|
b8c6d13dea | ||
|
|
97ea17f649 | ||
|
|
86afa24d5b | ||
|
|
f3bace5376 | ||
|
|
76134a465b | ||
|
|
dac903e07c | ||
|
|
9e88cff889 | ||
|
|
86a6de75ae | ||
|
|
07bcf041b5 | ||
|
|
da4845f9ce | ||
|
|
c2cd5ee973 | ||
|
|
658da465ea | ||
|
|
f327b8e4fa | ||
|
|
8b853f4c20 | ||
|
|
e59d52815c | ||
|
|
5144ddfa1b | ||
|
|
11ef81f170 | ||
|
|
27454c8a88 | ||
|
|
5030c88eea | ||
|
|
4862a885c9 | ||
|
|
318736553f | ||
|
|
83edccabad | ||
|
|
a85320b874 | ||
|
|
6c8620e7f7 | ||
|
|
14a4a1ed99 | ||
|
|
e8abeecd1b | ||
|
|
592149afed | ||
|
|
3ce8590ce2 | ||
|
|
318928d049 | ||
|
|
ea4863f195 | ||
|
|
e89da88c19 | ||
|
|
fca4be5951 | ||
|
|
ead734983c | ||
|
|
7f7413e050 | ||
|
|
ff6ac43fee | ||
|
|
54ec5ba45d | ||
|
|
9a35577ec6 | ||
|
|
8cc9255fd0 | ||
|
|
5ca809c8dd | ||
|
|
add96c1fda | ||
|
|
27b3006ffd | ||
|
|
4eac9654a7 | ||
|
|
ef06cd1c7a | ||
|
|
e1125ae241 | ||
|
|
e390adc651 | ||
|
|
e1662a4238 | ||
|
|
1dd470e9d5 | ||
|
|
fbbc1a8d45 | ||
|
|
3cd37cf458 | ||
|
|
d22c6c8f0c | ||
|
|
430c9f5da2 | ||
|
|
81e90a285b | ||
|
|
877f4cefa9 | ||
|
|
28b65deb95 | ||
|
|
2cc7052d7a | ||
|
|
bc6798714d | ||
|
|
11c16fadbd | ||
|
|
3105df3486 | ||
|
|
3a31b0a9fe | ||
|
|
ef4a19d0bb | ||
|
|
ddcf49e243 | ||
|
|
d6498a78ca | ||
|
|
44b04ddef6 | ||
|
|
215f5f6815 | ||
|
|
db60529466 | ||
|
|
ddcd19d5eb | ||
|
|
d529ed8962 | ||
|
|
cfc0e9fcc4 | ||
|
|
4a852374ab | ||
|
|
01327fa852 | ||
|
|
dc3c236791 | ||
|
|
b21a46e96e | ||
|
|
3612f2cb80 | ||
|
|
5d15b47b3e | ||
|
|
2062849887 | ||
|
|
1bdea275dc | ||
|
|
4770908831 | ||
|
|
9b69ffe8ff | ||
|
|
2cd4ddc4a3 | ||
|
|
e23bf223ae | ||
|
|
c76b2981c6 | ||
|
|
4adfc8fbc1 | ||
|
|
5cbd7618e3 | ||
|
|
db820375f1 | ||
|
|
62f7c6d534 | ||
|
|
3af594960a | ||
|
|
7f3db2f82c | ||
|
|
5f3dcbec81 | ||
|
|
8e73469e41 | ||
|
|
30f5b44168 | ||
|
|
66d102abf1 | ||
|
|
cf5ce23839 | ||
|
|
65729d8cad | ||
|
|
5ce281e99b | ||
|
|
73969c2fb6 | ||
|
|
a54538ee70 | ||
|
|
c5d075253d | ||
|
|
c97ceec3bc | ||
|
|
72a82d82d8 | ||
|
|
c9d09f1e08 | ||
|
|
a66459baa3 | ||
|
|
31e7ba2a08 | ||
|
|
0a9cb9952b | ||
|
|
ad8a5ce8a7 | ||
|
|
4fdaf6611f | ||
|
|
d3ef567ef2 | ||
|
|
66d3c09a0d | ||
|
|
e9287318c3 | ||
|
|
8be132abe6 | ||
|
|
9473bdef13 | ||
|
|
dfb08a96bb | ||
|
|
c8ffb329ba | ||
|
|
36fb857dbb | ||
|
|
98e2e50b34 | ||
|
|
dd0109d591 | ||
|
|
caeb8b6888 | ||
|
|
87d11c617c | ||
|
|
306f0af734 | ||
|
|
452f564c00 | ||
|
|
49aafadd1c | ||
|
|
bcfbf51696 | ||
|
|
83e72b37cc | ||
|
|
e71bf17e13 | ||
|
|
6596743b6b | ||
|
|
7e7b3505a1 | ||
|
|
965bee818a | ||
|
|
1b3c87bebe | ||
|
|
80a7fd7107 | ||
|
|
c18748f6bc | ||
|
|
a862805a05 | ||
|
|
526fd4d5aa | ||
|
|
f8466bd6e4 | ||
|
|
7d0f63c118 | ||
|
|
6fc89fbb3e | ||
|
|
b0403cfa70 | ||
|
|
d1aef28c7a | ||
|
|
fcb1295302 | ||
|
|
d4f89a734d | ||
|
|
914789c9cb | ||
|
|
2da132eebc | ||
|
|
127ff3e84a | ||
|
|
69a3d36b20 | ||
|
|
0e786cd04f | ||
|
|
0130792682 | ||
|
|
6f33f8afbd | ||
|
|
2b6611ef8e | ||
|
|
ba13443d1c | ||
|
|
61cbe07f0f | ||
|
|
eca5e31429 | ||
|
|
35e8d7dda9 | ||
|
|
944577eaf4 | ||
|
|
690a2f081b | ||
|
|
b26129af68 | ||
|
|
7d24b3839c | ||
|
|
7255722b67 | ||
|
|
569d8e3f76 | ||
|
|
c1cf78cda4 | ||
|
|
e67242a0c4 | ||
|
|
5efa9c37d6 | ||
|
|
b0fd63afbd | ||
|
|
28e952ada3 | ||
|
|
1a7c0b1e1d | ||
|
|
faec6e91ba | ||
|
|
b23b40d8e4 | ||
|
|
78a580f521 | ||
|
|
6c655b6043 | ||
|
|
2d6beef2ed | ||
|
|
a167e57ff0 | ||
|
|
cfd674bc5c | ||
|
|
4572aee2b7 | ||
|
|
e2d401b848 | ||
|
|
b632d66225 | ||
|
|
c7bae73363 | ||
|
|
4b0c7e9775 | ||
|
|
eb08ed6ff6 | ||
|
|
ca07aef201 | ||
|
|
3ee7246224 | ||
|
|
7b1e9136ee | ||
|
|
3d674c64db | ||
|
|
741cfed8c3 | ||
|
|
df1ac3aaa0 | ||
|
|
c1011c4520 | ||
|
|
4b55a9bca2 | ||
|
|
fe3fea34ff | ||
|
|
957cddfe18 | ||
|
|
e32e5c1b5f | ||
|
|
c5a5fc62f7 | ||
|
|
cde8b4b806 | ||
|
|
e7cd872fcc | ||
|
|
9a11c6ff9f | ||
|
|
e9067c05c6 | ||
|
|
6effedcce2 | ||
|
|
5366622fca | ||
|
|
8c5f1b5db2 | ||
|
|
92f9d645f8 | ||
|
|
0bb6bb5977 | ||
|
|
9e2c91905c | ||
|
|
361c7ca49a | ||
|
|
272ed8629f | ||
|
|
9fe6107ae3 | ||
|
|
96ffd5509f | ||
|
|
93bdec7922 | ||
|
|
89a027a8e0 | ||
|
|
53f1b95843 | ||
|
|
1b0e93b8f8 | ||
|
|
c38e474385 | ||
|
|
95cf247e2e | ||
|
|
d9f1dabecb | ||
|
|
a4656a5081 | ||
|
|
1689a45e3e | ||
|
|
29ca5bfbe1 | ||
|
|
46584a0e3e | ||
|
|
c171588ba9 | ||
|
|
9d94fd6a5e | ||
|
|
9fd61677f9 | ||
|
|
4808dcbc96 | ||
|
|
ed02493a77 | ||
|
|
6501e0c1d7 | ||
|
|
1c5ca0b7a5 | ||
|
|
56af62029a | ||
|
|
17e450310e | ||
|
|
7274c514a7 | ||
|
|
9cf4a378f1 | ||
|
|
396d5d7d88 | ||
|
|
c9dd352565 | ||
|
|
5ed0d555b9 | ||
|
|
4617954928 | ||
|
|
3d34b50793 | ||
|
|
fad01f6f90 | ||
|
|
5f0f918642 | ||
|
|
245855d0ca | ||
|
|
224eaf3cef | ||
|
|
d3c206fb26 | ||
|
|
0d121ecaad | ||
|
|
2efedfb572 | ||
|
|
5a9a13146c | ||
|
|
e3555d204e | ||
|
|
4e53584cff | ||
|
|
6bf09970a4 | ||
|
|
484d201cb0 | ||
|
|
bbfc61b335 | ||
|
|
8f9968df33 | ||
|
|
ffcd868724 | ||
|
|
06e1e73018 | ||
|
|
7bed19c81f | ||
|
|
38f479656a | ||
|
|
59630ccc03 | ||
|
|
684d9c3283 | ||
|
|
bc269bc1fb | ||
|
|
05a05b0589 | ||
|
|
9096d05e65 | ||
|
|
2efba7247e | ||
|
|
d76a5b9e07 | ||
|
|
f5ebcd7bd8 | ||
|
|
7ffef350fe | ||
|
|
83a61e2e87 | ||
|
|
6fb7ecbc52 | ||
|
|
7a0913928e | ||
|
|
3fca9ad736 | ||
|
|
cad0f796fa | ||
|
|
c9dbd13a3a | ||
|
|
9c2123fe31 | ||
|
|
8867d180d2 | ||
|
|
e7ae70add0 | ||
|
|
7a6ed850b5 | ||
|
|
8ae56563ca | ||
|
|
21e4714658 | ||
|
|
e6bd0e32ed | ||
|
|
a79b87b17f | ||
|
|
b1fabc37a4 | ||
|
|
d12c157b66 | ||
|
|
c8ac60144f | ||
|
|
3403b52fb9 | ||
|
|
0258dd8ad0 | ||
|
|
5ca7d73be4 | ||
|
|
780af9a019 | ||
|
|
600faaf608 | ||
|
|
c2f40d2f5d | ||
|
|
e3994851ea | ||
|
|
2524b174aa | ||
|
|
49366791fb | ||
|
|
b15a26c3d5 | ||
|
|
db61f33578 | ||
|
|
e02ed91721 | ||
|
|
1623352c92 | ||
|
|
824be74b6e | ||
|
|
50bd182d8f | ||
|
|
e480c373cd | ||
|
|
65b5ffb2fa | ||
|
|
172617a523 | ||
|
|
7e6ffad5fe | ||
|
|
5813ede67b | ||
|
|
c1157d30cf | ||
|
|
a3384ff235 | ||
|
|
c12f51f802 | ||
|
|
f86f3fb4f0 | ||
|
|
e25cd62891 | ||
|
|
ab5c6b7bb5 | ||
|
|
00c5258b57 | ||
|
|
cbfce55c16 | ||
|
|
d96c9a5151 | ||
|
|
5f4baaf48d | ||
|
|
3f38bfbba8 | ||
|
|
acae7644eb | ||
|
|
d454c9b13a | ||
|
|
4886552639 | ||
|
|
fb2b224231 | ||
|
|
1fcb765ca0 | ||
|
|
f229d9ec95 | ||
|
|
66e9fc9126 | ||
|
|
2b281dfcfd | ||
|
|
1fe8a512f0 | ||
|
|
5c7c730809 | ||
|
|
bc7f70729a | ||
|
|
f6216dbdbe | ||
|
|
35369f7394 | ||
|
|
8289c9f1b3 | ||
|
|
1f52c3625a | ||
|
|
851e3b7640 | ||
|
|
5facadf139 | ||
|
|
05daf17a74 | ||
|
|
67f61aa7e7 | ||
|
|
636eb02392 | ||
|
|
f2ac3dc768 | ||
|
|
69af7fce22 | ||
|
|
1be2a1939f | ||
|
|
205b5262da | ||
|
|
a9cc7480ea | ||
|
|
dfdb0812c3 | ||
|
|
0abeca4533 | ||
|
|
fed5a08e3d | ||
|
|
7ff5ba8973 | ||
|
|
ad0e87f5a1 | ||
|
|
bcef981955 | ||
|
|
547b186a9d | ||
|
|
620085054b | ||
|
|
2118ec9d01 | ||
|
|
dfc90508d1 | ||
|
|
e32dd7014b | ||
|
|
cda42a0901 | ||
|
|
807aa90549 | ||
|
|
2ea0a3b529 | ||
|
|
09865d02be | ||
|
|
7b42387a19 | ||
|
|
32e4ec0682 | ||
|
|
ccc19f4c9c | ||
|
|
4a47a95884 | ||
|
|
51c90b3b74 | ||
|
|
149c3b1093 | ||
|
|
88cb4f72b5 | ||
|
|
ae38ed165d | ||
|
|
b1c68679cd | ||
|
|
29ef8f401d | ||
|
|
3593a4d8c0 | ||
|
|
50cd701ca5 | ||
|
|
6c5c9d2c9c | ||
|
|
67d06ab1e1 | ||
|
|
238bd41109 | ||
|
|
8dac3da933 | ||
|
|
88c27a9c70 | ||
|
|
ed27a8c7ba | ||
|
|
bdaccdf421 | ||
|
|
23fc46bd0f | ||
|
|
279fc1f66d | ||
|
|
f6927b9ae7 | ||
|
|
de4f22f704 | ||
|
|
63d853bf7d | ||
|
|
f310e6d6d5 | ||
|
|
e59bfcecf2 | ||
|
|
cfc26c4767 | ||
|
|
acee8c442e | ||
|
|
60fb58f6d5 | ||
|
|
7289770f9c | ||
|
|
0b48631363 | ||
|
|
eb0ab29b37 | ||
|
|
989abbe2c2 | ||
|
|
f0f0dd5ea5 | ||
|
|
1cc22d35c3 | ||
|
|
e7304247c7 | ||
|
|
ed5ebcee90 | ||
|
|
d940097e44 | ||
|
|
e7eff7753f | ||
|
|
f9d1405a85 | ||
|
|
949486c447 | ||
|
|
1ab2cd2283 | ||
|
|
f5e33be7f6 | ||
|
|
d0e0fc3c28 | ||
|
|
2d057debe6 | ||
|
|
a619cd6114 | ||
|
|
fac33e06ba | ||
|
|
39d52f6570 | ||
|
|
bc412024da | ||
|
|
a94fff401b | ||
|
|
b2df674026 | ||
|
|
df231ad618 | ||
|
|
0d1a59a5ce | ||
|
|
83686f3073 | ||
|
|
8ea7fb05c0 | ||
|
|
bdf59a9932 | ||
|
|
b2bc1e62cb | ||
|
|
f45fe90de0 | ||
|
|
61e333401f | ||
|
|
8d6da2eacc | ||
|
|
9d65156d92 | ||
|
|
a556486c83 | ||
|
|
38e31695a3 | ||
|
|
23764340bd | ||
|
|
c6bcb36ab1 | ||
|
|
e9715651d9 | ||
|
|
504c8c6f49 | ||
|
|
37d78e1d87 | ||
|
|
8266048933 | ||
|
|
da577b5116 | ||
|
|
2e424fcc2a | ||
|
|
82458ca992 | ||
|
|
8f687d0a55 | ||
|
|
af17e057e8 | ||
|
|
9118a54be7 | ||
|
|
b5364cc705 | ||
|
|
8316c24d0d | ||
|
|
27b177b903 | ||
|
|
6f1ab32019 | ||
|
|
c20b58a011 | ||
|
|
3210d5c898 | ||
|
|
c6f669336a | ||
|
|
0d96c7c690 | ||
|
|
24ee335768 | ||
|
|
a599c39814 | ||
|
|
e0895ad653 | ||
|
|
7cdd3f8603 | ||
|
|
e3b75f2cfb | ||
|
|
56936d0285 | ||
|
|
c665f1d355 | ||
|
|
819783ea09 | ||
|
|
249c686442 | ||
|
|
81a422b07d | ||
|
|
b9d9edb7c3 | ||
|
|
e9aa3aa8ac | ||
|
|
5aa5c0068e | ||
|
|
3e010376f4 | ||
|
|
1f4c4e7640 | ||
|
|
1b7e315c2b | ||
|
|
d0dbdf69f0 | ||
|
|
ec33d4020d | ||
|
|
e20497bb87 | ||
|
|
63e34f747c | ||
|
|
066c4fa4e4 | ||
|
|
ff4f40328a | ||
|
|
2f27e5a491 | ||
|
|
144293636f | ||
|
|
4eca60d495 | ||
|
|
1a9e961d71 | ||
|
|
33f1e54f4d | ||
|
|
cd63e2e3ca | ||
|
|
7bcbefaf06 | ||
|
|
74e38f9318 | ||
|
|
0be9674f69 | ||
|
|
01b7d6c945 | ||
|
|
9ef56db2dd | ||
|
|
fd59c0b28f | ||
|
|
b8c1688172 | ||
|
|
1c56167dba | ||
|
|
ee4bbf772f | ||
|
|
4c12e8b946 | ||
|
|
379ca86b45 | ||
|
|
ddc6408828 | ||
|
|
1bea61c45a | ||
|
|
d4219b5ce0 | ||
|
|
2157d5952c | ||
|
|
7e637b795d | ||
|
|
67f32ad7b9 | ||
|
|
c02d7c49e9 | ||
|
|
9d98f8c4f2 | ||
|
|
b0b68233aa | ||
|
|
5681038c22 | ||
|
|
a59b9282df | ||
|
|
0c6829616c | ||
|
|
1ebdefcd43 | ||
|
|
824a9fa0a9 | ||
|
|
92ea06eb77 | ||
|
|
e65e9be052 | ||
|
|
0148f9bee0 | ||
|
|
cb2d0d5455 | ||
|
|
377899d5e1 | ||
|
|
dcc0a27215 | ||
|
|
a5e59af885 | ||
|
|
7a3a2cfe07 | ||
|
|
df344baa06 | ||
|
|
3d53652b2d | ||
|
|
863baddad0 | ||
|
|
d091f1b603 | ||
|
|
84eccb1912 | ||
|
|
b8ade54e85 | ||
|
|
4da99ab4d4 | ||
|
|
cbd8a77eb6 | ||
|
|
6f01ee2206 | ||
|
|
f3d2de54b5 | ||
|
|
1e6c2762c2 | ||
|
|
4003687e73 | ||
|
|
92752d5953 | ||
|
|
f40f916bec | ||
|
|
e670672e15 | ||
|
|
8ea37eb39a | ||
|
|
c9e40fc682 | ||
|
|
adf98b8c33 | ||
|
|
e5e1d2ca39 | ||
|
|
c82a89a7fb | ||
|
|
84a116291c | ||
|
|
371d2ed5b2 | ||
|
|
ab39421c79 | ||
|
|
fa2c851c5b | ||
|
|
fec499cfad | ||
|
|
3ebd605092 | ||
|
|
b0f5751e11 | ||
|
|
77795192a0 | ||
|
|
48c6e7d69f | ||
|
|
22ada89fa6 | ||
|
|
a2cc69b8ee | ||
|
|
caad079961 | ||
|
|
63d6f33ada | ||
|
|
8e97621fca | ||
|
|
3205dea576 | ||
|
|
f68a0231c2 | ||
|
|
9c84c9905d | ||
|
|
6caf247d1c | ||
|
|
2ee1c2d51e | ||
|
|
62571f55a1 | ||
|
|
5b6199b226 | ||
|
|
fd0069cb6b | ||
|
|
132bb08582 | ||
|
|
6fe403315a | ||
|
|
1ca14ca33e | ||
|
|
4b61178b0e | ||
|
|
a9c5ccdbb8 | ||
|
|
ae62642945 | ||
|
|
8360a7e955 | ||
|
|
36f25b7a21 | ||
|
|
8c117d5b1f | ||
|
|
1fc039a409 | ||
|
|
eb81fae2a0 | ||
|
|
3e26cb29f6 | ||
|
|
058e72b52b | ||
|
|
31deeae53f | ||
|
|
65ee75b49e | ||
|
|
60f87e42f8 | ||
|
|
bdf67ab7c8 | ||
|
|
26a3e868e2 | ||
|
|
9f728f46d3 | ||
|
|
945b2a83a8 | ||
|
|
ab5eeae4bf | ||
|
|
3228f0bb50 | ||
|
|
9daa50294e | ||
|
|
a36dc93522 | ||
|
|
7746cffb08 | ||
|
|
0592695f1f | ||
|
|
07723d495a | ||
|
|
78d79f4a54 | ||
|
|
81e3241e38 | ||
|
|
2637e8eef9 | ||
|
|
d3f87d7801 | ||
|
|
8ed5922465 | ||
|
|
69d9e9bad3 | ||
|
|
483e6a1d32 | ||
|
|
2953f5c7fb | ||
|
|
539d360ebe | ||
|
|
4df958caf1 | ||
|
|
52a43b1780 | ||
|
|
37aaf1aad0 | ||
|
|
34f6bb215a | ||
|
|
0955aaa12c | ||
|
|
4fb675943f | ||
|
|
182c277ef5 | ||
|
|
ec590240c0 | ||
|
|
225c9c73b9 | ||
|
|
e39f77704e | ||
|
|
87b890652b | ||
|
|
1b2a9a4efd | ||
|
|
5869cbae2f | ||
|
|
59fe407834 | ||
|
|
2caf04fcd1 | ||
|
|
e48d1a09c7 | ||
|
|
ae0b21e233 | ||
|
|
ef5ec8732a | ||
|
|
eb9519cfdd | ||
|
|
ccb8425f97 | ||
|
|
473ac08572 | ||
|
|
5fc00a53dc | ||
|
|
e3f118254d | ||
|
|
fe16a94262 | ||
|
|
43951cf9ba | ||
|
|
5eeeb97286 | ||
|
|
9fc03a7a5f | ||
|
|
a1be11ae24 | ||
|
|
d42bbb2c21 | ||
|
|
93a6d304c0 | ||
|
|
16b41c8102 | ||
|
|
710fccc1c0 | ||
|
|
2c6e952ff9 | ||
|
|
d074b3e881 | ||
|
|
3e23509e68 | ||
|
|
21235068f7 | ||
|
|
04f7c1f556 | ||
|
|
bac7bb15d3 | ||
|
|
34d221c437 | ||
|
|
a76ea77d5d | ||
|
|
ff57454073 | ||
|
|
9e5af73ccb | ||
|
|
47126bf5c8 | ||
|
|
ffa1319b5b | ||
|
|
abeeb69afb | ||
|
|
31fb57baad | ||
|
|
b6b73b2d01 | ||
|
|
4502f30b29 | ||
|
|
227aacd005 | ||
|
|
0d6c73fba4 | ||
|
|
5d0e9e0559 | ||
|
|
93975aab36 | ||
|
|
d798562de7 | ||
|
|
567907fc4d | ||
|
|
e9d33b6e42 | ||
|
|
7616a11a46 | ||
|
|
5b21aef481 | ||
|
|
4007731342 | ||
|
|
a1e2bc2e66 | ||
|
|
e595dc4025 | ||
|
|
70014cbdf1 | ||
|
|
5673225ecc | ||
|
|
dfc9b66059 | ||
|
|
9ee1275162 | ||
|
|
806e65e85e | ||
|
|
f5b0252e79 | ||
|
|
c69cce00b6 | ||
|
|
b032e101e3 | ||
|
|
3edca119dd | ||
|
|
02e2624842 | ||
|
|
9a4911f6d5 | ||
|
|
a21fd5aa96 | ||
|
|
f7638366ee | ||
|
|
24a90007f3 | ||
|
|
97f06c66f3 | ||
|
|
0283a74bfe | ||
|
|
07fa8b6720 | ||
|
|
c2868e445f | ||
|
|
e5d4307436 | ||
|
|
94d46cf094 | ||
|
|
aadda48d3b | ||
|
|
01fc0d1fc8 | ||
|
|
530f78c35e | ||
|
|
d771ada3fd | ||
|
|
3719f5142e | ||
|
|
2e0fbda239 | ||
|
|
6d28f62aba | ||
|
|
b6166bfc3f | ||
|
|
a8d54768c6 | ||
|
|
da406b55b4 | ||
|
|
833fa24545 | ||
|
|
9e057d4395 | ||
|
|
0f226da217 | ||
|
|
afdb9d6914 | ||
|
|
306c84c4b8 | ||
|
|
dfe03ca71c | ||
|
|
72b6679012 | ||
|
|
146da42b2d | ||
|
|
bfa661aa72 | ||
|
|
32699e4585 | ||
|
|
26c3e10050 | ||
|
|
8b65434790 | ||
|
|
a36cd988c2 | ||
|
|
648e0d954a | ||
|
|
92cb5801da | ||
|
|
775e92daf5 | ||
|
|
3991747dad | ||
|
|
7202b86e78 | ||
|
|
d6aff05ebf | ||
|
|
c15c11fa63 | ||
|
|
fcdb38c9cf | ||
|
|
01bff554e6 | ||
|
|
1107d575be | ||
|
|
939db21e00 | ||
|
|
55b9bfd69c | ||
|
|
50f3c85548 | ||
|
|
b545abce44 | ||
|
|
0c500effba | ||
|
|
215ce618ba | ||
|
|
59a9b893b5 | ||
|
|
b7e37980e0 | ||
|
|
899c4c08ef | ||
|
|
61920a19db | ||
|
|
c506a1f08f | ||
|
|
2aab58d1ea | ||
|
|
56fda283b4 | ||
|
|
64c14fab2f | ||
|
|
ff5aa80633 | ||
|
|
798a4a87fc | ||
|
|
5a498fc98c | ||
|
|
9b41bfd1ee | ||
|
|
1d615ab97d | ||
|
|
d3cfdea855 | ||
|
|
c74b5f0929 | ||
|
|
0eec985a62 | ||
|
|
10651f75e5 | ||
|
|
309869c494 | ||
|
|
616125895b | ||
|
|
4d2fd379a8 | ||
|
|
0b3e17c331 | ||
|
|
81ae79dc92 | ||
|
|
c429a82b0f | ||
|
|
8cc66ca0cc | ||
|
|
8629d9aa4a | ||
|
|
01d9c27728 | ||
|
|
02673f772a | ||
|
|
a1b9aed8a9 | ||
|
|
0f7bc29069 | ||
|
|
3c9c4366ea | ||
|
|
988a42f15d | ||
|
|
6a8a09a3b7 | ||
|
|
ea59cdc63c | ||
|
|
e8d195410e | ||
|
|
1afbd8fec6 | ||
|
|
857c28db22 | ||
|
|
04b746923c | ||
|
|
95855d8191 | ||
|
|
813f2d20f9 | ||
|
|
d381c0a474 | ||
|
|
3fa819df3f | ||
|
|
13deaffee4 | ||
|
|
030b8a9f01 | ||
|
|
8a4721b61d | ||
|
|
a78d63398d | ||
|
|
78decb1ad0 | ||
|
|
0f0dcb560c | ||
|
|
0678e3e8bc | ||
|
|
cf04771d32 | ||
|
|
44f2354483 | ||
|
|
9b0a71cccc | ||
|
|
b3a4a25504 | ||
|
|
62131b1f3c | ||
|
|
9f3ff15499 | ||
|
|
a5ad2408be | ||
|
|
43b481c185 | ||
|
|
5cbd8c46ab | ||
|
|
6d84cabf88 | ||
|
|
b570034e5b | ||
|
|
dc1f6d52c8 | ||
|
|
62edaf552f | ||
|
|
3473d40b89 | ||
|
|
949270b8ca | ||
|
|
0e74833f22 | ||
|
|
941c24d9b8 | ||
|
|
266eb6319b | ||
|
|
461c59f5e8 | ||
|
|
192d6ec0e6 | ||
|
|
a7d094bbcb | ||
|
|
607c04d7ba | ||
|
|
1d2b41be13 | ||
|
|
fb6e758607 | ||
|
|
e2a30d9371 | ||
|
|
fbf0a79085 | ||
|
|
ae3e359fd6 | ||
|
|
b635d452b1 | ||
|
|
bd38d220c8 | ||
|
|
3cadad4a71 | ||
|
|
6a30c62769 | ||
|
|
656b7c414c | ||
|
|
5ad4207f29 | ||
|
|
057e2ced63 | ||
|
|
d7243190cb | ||
|
|
b4eacc7175 | ||
|
|
a2bcbed780 | ||
|
|
e7b8dac36d | ||
|
|
c65bb4578c | ||
|
|
9c5625ac85 | ||
|
|
2a9a905794 | ||
|
|
043208f58f | ||
|
|
a55e1dd59f | ||
|
|
f4aea309cf | ||
|
|
451391bcdb | ||
|
|
6367140708 | ||
|
|
cae9589427 | ||
|
|
90c6daeb29 | ||
|
|
2adf451dbe | ||
|
|
1c4a832afc | ||
|
|
358ea324a6 | ||
|
|
2a4ef9ba13 | ||
|
|
1f148d5979 | ||
|
|
7d8a2cacd0 | ||
|
|
e6a7ff03aa | ||
|
|
ddf7fadc91 | ||
|
|
37c53a9c36 | ||
|
|
1a1ea80d13 | ||
|
|
d1e124b392 | ||
|
|
b729cfe653 | ||
|
|
455d464f80 | ||
|
|
becff72cfa | ||
|
|
36943fd6cd | ||
|
|
f0bf6aee8d | ||
|
|
bf49715209 | ||
|
|
1083ee6ca2 | ||
|
|
c0942b0fbc | ||
|
|
e25abe11eb | ||
|
|
9d62906903 | ||
|
|
6017f013a1 | ||
|
|
93a1a5230b | ||
|
|
3bd0010539 | ||
|
|
7d1ea8755f | ||
|
|
94f2078fff | ||
|
|
8429d9b122 | ||
|
|
474ab3d4b4 | ||
|
|
f721c3c7ad | ||
|
|
b88efcdf08 | ||
|
|
5ddc77a9cc | ||
|
|
eefb24280d | ||
|
|
0639be0ea3 | ||
|
|
663534acf8 | ||
|
|
7df1eb04d0 | ||
|
|
b019e37b31 | ||
|
|
6572fb96a3 | ||
|
|
11bee2309a | ||
|
|
6e0455ef78 | ||
|
|
1ebd32fea9 | ||
|
|
2d5d148f17 | ||
|
|
61c8a5a652 | ||
|
|
3a360fcbf0 | ||
|
|
44888b7e05 | ||
|
|
bb170d795c | ||
|
|
8cb81878db | ||
|
|
21be71da80 | ||
|
|
01d7858a59 | ||
|
|
70e411daf5 | ||
|
|
83ff143064 | ||
|
|
347b71561f | ||
|
|
f468cda043 | ||
|
|
81232fdefc | ||
|
|
819d13dc4e | ||
|
|
2e26038eab | ||
|
|
de5c3b3ccf | ||
|
|
8526d36b37 | ||
|
|
d2084bdacd | ||
|
|
9f163391f2 | ||
|
|
650ec2cce7 | ||
|
|
3e883dfeb7 | ||
|
|
1b1a2773a1 | ||
|
|
10bac3bbdb | ||
|
|
282ab0c2e1 | ||
|
|
e3138bb8e9 | ||
|
|
f2487e2ea6 | ||
|
|
737cdf15cf | ||
|
|
9e58bff26e | ||
|
|
7e16b2f101 | ||
|
|
c05b5d18fa | ||
|
|
3d054e4d72 | ||
|
|
ba408af32a | ||
|
|
bec6578778 | ||
|
|
f41c7ad8dc | ||
|
|
1dbc4cc422 | ||
|
|
69e248fb07 | ||
|
|
28c645e636 | ||
|
|
2e54648ee6 | ||
|
|
57cc83b332 | ||
|
|
59f544396c | ||
|
|
ea12cbc083 | ||
|
|
d47b47cc5a | ||
|
|
d8e256ed98 | ||
|
|
cd0aa41e6f | ||
|
|
d024062a05 | ||
|
|
e68cdfbf19 | ||
|
|
b3dda2cecf | ||
|
|
b37d9248c3 | ||
|
|
e147f68a19 | ||
|
|
5d118fc73b | ||
|
|
36e5c66e9b | ||
|
|
fddf4d10dc | ||
|
|
c5cd80b542 | ||
|
|
2c7bb53b64 | ||
|
|
a3d257fa18 | ||
|
|
53b169804d | ||
|
|
3bdfe0a389 | ||
|
|
d7231e17b8 | ||
|
|
547a410ecc | ||
|
|
b6760877e3 | ||
|
|
2d77f15c41 | ||
|
|
bd69261397 | ||
|
|
b4fe4961f7 | ||
|
|
4b92c210d6 | ||
|
|
c374a2dc03 | ||
|
|
b05c952148 | ||
|
|
4b8b098c57 | ||
|
|
7cacdae46b | ||
|
|
7bb64daccb | ||
|
|
9d620e9834 | ||
|
|
4db5306178 | ||
|
|
6c0c67ab40 | ||
|
|
dd21623b59 | ||
|
|
a1c29117a5 | ||
|
|
8fb73cfc56 | ||
|
|
1a20c899d1 | ||
|
|
a1c7b9e1e3 | ||
|
|
f835a3be80 | ||
|
|
b503c843b2 | ||
|
|
8367ccca9e | ||
|
|
96ed2a9afc | ||
|
|
36c53c49a2 | ||
|
|
6750655237 | ||
|
|
6f8cc814a5 | ||
|
|
711190bfab | ||
|
|
194568aa4e | ||
|
|
b64f7799bd | ||
|
|
50315128a7 | ||
|
|
b7ac2ac89c | ||
|
|
3721a55704 | ||
|
|
2df88bf698 | ||
|
|
109b7e62ba | ||
|
|
d5066cb60d | ||
|
|
7f1b635f63 | ||
|
|
d4f58fdb90 | ||
|
|
78dad96124 | ||
|
|
43b061b007 | ||
|
|
768debfb2b | ||
|
|
2a8c4a6772 | ||
|
|
825811b3b6 | ||
|
|
b9160c4ecd | ||
|
|
c00542ef57 | ||
|
|
7648ebe493 | ||
|
|
1acb0a9565 | ||
|
|
9beba4905b | ||
|
|
834d5886ab | ||
|
|
901e829829 | ||
|
|
0f599ce679 | ||
|
|
ef5799cd9b | ||
|
|
8d9013ef51 | ||
|
|
21bd59c915 | ||
|
|
7350910c7b | ||
|
|
f79735f13c | ||
|
|
e21661df1e | ||
|
|
f380616bdb | ||
|
|
ccc7fec756 | ||
|
|
623366ab24 | ||
|
|
b5e113f20e | ||
|
|
a63f55f736 | ||
|
|
4ddcad1e3e | ||
|
|
98c14e795a | ||
|
|
65f0739645 | ||
|
|
113881ad52 | ||
|
|
22d83dd3de | ||
|
|
5e2aea8f7e | ||
|
|
4a1723a322 | ||
|
|
bef4a78d70 | ||
|
|
30de46cf2c | ||
|
|
51bf5a2e27 | ||
|
|
a3230937f3 | ||
|
|
3c76902124 | ||
|
|
d3e90a8b53 | ||
|
|
fd1140c813 | ||
|
|
7d38b13a39 | ||
|
|
c57fcff7db | ||
|
|
ac2a974947 | ||
|
|
2da19efb19 | ||
|
|
1bc87e623a | ||
|
|
d777339110 | ||
|
|
7fd3fad3ea | ||
|
|
134ef373af | ||
|
|
192507a605 | ||
|
|
ee150b692d | ||
|
|
66da155d8a | ||
|
|
31e2edf3eb | ||
|
|
82355003d4 | ||
|
|
a1164ade51 | ||
|
|
afed3fffcd | ||
|
|
68d05b38a8 | ||
|
|
fea2cdae61 | ||
|
|
4fff111173 | ||
|
|
2dd4419bb1 | ||
|
|
69d9de686e | ||
|
|
ca7ba5ce09 | ||
|
|
bd0431f79c | ||
|
|
33bb2c7620 | ||
|
|
5438874668 | ||
|
|
ed24dbd428 | ||
|
|
e9b3024210 | ||
|
|
1ab715ac49 | ||
|
|
0c7a59c5d8 | ||
|
|
b99c3925a4 | ||
|
|
984f989646 | ||
|
|
69953eec1b | ||
|
|
04f13c4c88 | ||
|
|
5b44ddefde | ||
|
|
af3fe1f0f9 | ||
|
|
2136865493 | ||
|
|
c480b3694c | ||
|
|
5fba6e9e38 | ||
|
|
68e25f503a | ||
|
|
6950347ffb | ||
|
|
2fadf7f1e9 | ||
|
|
2504bb5832 | ||
|
|
8d03251be1 | ||
|
|
5ba595cefe | ||
|
|
147b5cc396 | ||
|
|
ec64b64535 | ||
|
|
2f6a9d4f2b | ||
|
|
3e88604fd7 | ||
|
|
e201cee24a | ||
|
|
0198da50d6 | ||
|
|
2cb4a41e64 | ||
|
|
2c460bc8e5 | ||
|
|
fd0cadab8c | ||
|
|
4d99207acc | ||
|
|
3cc4365697 | ||
|
|
49128d2657 | ||
|
|
e1e06febd2 | ||
|
|
aeb15f237a | ||
|
|
6c41c5477c | ||
|
|
313ab2c988 | ||
|
|
2c67841b11 | ||
|
|
22a10f6f0d | ||
|
|
c5b3f667c1 | ||
|
|
62be4c5bca | ||
|
|
5629192760 | ||
|
|
d7cf2425a1 | ||
|
|
743e73b9ed | ||
|
|
0582091908 | ||
|
|
7076e27fc6 | ||
|
|
a7a0c74a2d | ||
|
|
e96d267c92 | ||
|
|
2f5586227f | ||
|
|
9351a015f3 | ||
|
|
e75458a312 | ||
|
|
bca654a27c | ||
|
|
58ad71c3ae | ||
|
|
b619a8dd2c | ||
|
|
0c3b36a43d | ||
|
|
0d8eb887f7 | ||
|
|
99cd3d1df0 | ||
|
|
31d8b503d0 | ||
|
|
d44c2dd7d9 | ||
|
|
4de48edbc1 | ||
|
|
ae34ff8f64 | ||
|
|
5d9da2bf1c | ||
|
|
a1ef81e5cb | ||
|
|
1d6f177660 | ||
|
|
a637226b4d | ||
|
|
21aa407a50 | ||
|
|
61f50215d7 | ||
|
|
98d57bdc13 | ||
|
|
537e11c7c4 | ||
|
|
ec52828c6e | ||
|
|
db85854efc | ||
|
|
45ddacf4d0 | ||
|
|
bf283affe1 | ||
|
|
a6e18bc2f6 | ||
|
|
86c7ec26d5 | ||
|
|
8ae0bf8e82 | ||
|
|
4c5aa46b7e | ||
|
|
42c336cb5e | ||
|
|
be3d34cd64 | ||
|
|
ebb07ceef5 | ||
|
|
ecab9036b6 | ||
|
|
94889f5b30 | ||
|
|
9abf8742f7 | ||
|
|
b5db201e70 | ||
|
|
4f509d06db | ||
|
|
b3d8750975 | ||
|
|
c953661046 | ||
|
|
65f25725e9 | ||
|
|
d183c56736 | ||
|
|
47d0103dda | ||
|
|
3be8c0d715 | ||
|
|
9f7101e233 | ||
|
|
b1aadc7cda | ||
|
|
faa6656e00 | ||
|
|
92f21c32f6 | ||
|
|
3570fffbf6 | ||
|
|
6828c255a6 | ||
|
|
d2e03e0093 | ||
|
|
4072c020a6 | ||
|
|
ad6dc82594 | ||
|
|
edbc1f8391 | ||
|
|
00af80b830 | ||
|
|
0b29ad8321 | ||
|
|
12a2eb420a | ||
|
|
107320ad28 | ||
|
|
74bcfb428d | ||
|
|
c32a186e88 | ||
|
|
c1165a2185 | ||
|
|
14843066bb | ||
|
|
0611ed21e3 | ||
|
|
98f53d4866 | ||
|
|
304d97ae22 | ||
|
|
a0407afa54 | ||
|
|
71225bf045 | ||
|
|
4bd659103b | ||
|
|
d1384f6c6e | ||
|
|
726dff60bd | ||
|
|
09241e1eeb | ||
|
|
fbce381e32 | ||
|
|
7d14effde2 | ||
|
|
11e4c12f85 | ||
|
|
321c9cbe45 | ||
|
|
08c79d8ed8 | ||
|
|
8f7300598c | ||
|
|
a43b4df8ef | ||
|
|
d6c6694700 | ||
|
|
6367c43cce | ||
|
|
69d835855c | ||
|
|
53a3d8c3d6 | ||
|
|
cd8bf2e684 | ||
|
|
b3d7d3f87e | ||
|
|
e3abc40701 | ||
|
|
03165cb3b7 | ||
|
|
14470e5217 | ||
|
|
a1cb3dc029 | ||
|
|
2a27192dc4 | ||
|
|
1e6259d09c | ||
|
|
3557ac4761 | ||
|
|
6708f9b026 | ||
|
|
033906f0e0 | ||
|
|
e119647545 | ||
|
|
5bdc3a1a09 | ||
|
|
fb1ee9fcfb | ||
|
|
1266c94c5f | ||
|
|
69fb7e994e | ||
|
|
e129fd308b | ||
|
|
f0685e206f | ||
|
|
c0c826b443 | ||
|
|
97b047a09e | ||
|
|
465e97d206 | ||
|
|
c0ba530d32 | ||
|
|
fab5fbc070 | ||
|
|
7dbf324e31 | ||
|
|
80b57612bc | ||
|
|
72b01be3af | ||
|
|
0616092eff | ||
|
|
586aaf0091 | ||
|
|
8e8eeeb239 | ||
|
|
1fea7b659a | ||
|
|
e20a4eecd1 | ||
|
|
7573e99617 | ||
|
|
79e4d28176 | ||
|
|
66e3267798 | ||
|
|
c857e2c57a | ||
|
|
0bf479bdf1 | ||
|
|
de44599827 | ||
|
|
ca6514b090 | ||
|
|
d3b66b7000 | ||
|
|
16f63c3e20 | ||
|
|
52fcd2bc29 | ||
|
|
ae18c92a5a | ||
|
|
c752196a1c | ||
|
|
11ba04379d | ||
|
|
adfaed1eb1 | ||
|
|
a288b8730e | ||
|
|
e7a39b6fa8 | ||
|
|
f662e21c5d | ||
|
|
e409e333e0 | ||
|
|
7de36849cd | ||
|
|
aa30c248d5 | ||
|
|
f34d5183d1 | ||
|
|
d7d41c2e66 | ||
|
|
d21e0a09fc | ||
|
|
24d7e7540e | ||
|
|
0e623b73fd | ||
|
|
e751f5ddbd | ||
|
|
9ceb6bcee2 | ||
|
|
a439f7e3da | ||
|
|
7b39bfd044 | ||
|
|
17ecab50f4 | ||
|
|
418d9f4ad7 | ||
|
|
0309757028 | ||
|
|
e560d9a6f1 | ||
|
|
f0883e51f0 | ||
|
|
ca649cf8c6 | ||
|
|
045bedc6ad | ||
|
|
1be7783266 | ||
|
|
5deeb4da2d | ||
|
|
79c97cf5a6 | ||
|
|
6adc416d5f | ||
|
|
b7b1aa2d2f | ||
|
|
b56b182357 | ||
|
|
1424abe44e | ||
|
|
551bc424b8 | ||
|
|
a40529aaa5 | ||
|
|
b6b67e9bd1 | ||
|
|
8198263b02 | ||
|
|
6254a93b20 | ||
|
|
f74ebc1942 | ||
|
|
8bc6160ce8 | ||
|
|
2d89ce71e0 | ||
|
|
319843f235 | ||
|
|
a4ac82fe7e | ||
|
|
1eceb8e8e8 | ||
|
|
62e6064fd9 | ||
|
|
ad84c10391 | ||
|
|
0d74cc3607 | ||
|
|
3dbedca97d | ||
|
|
e8c65ed81f | ||
|
|
8a1c17f994 | ||
|
|
fc8eb374b3 | ||
|
|
53edb45825 | ||
|
|
e1aa72715d | ||
|
|
c83715e9f6 | ||
|
|
f001bebce7 | ||
|
|
441ab8b374 | ||
|
|
8e35f93a58 | ||
|
|
8f23292a2c | ||
|
|
2b5a03efa1 | ||
|
|
2f10f03ec8 | ||
|
|
3777da6a0f | ||
|
|
2201e5bcce | ||
|
|
d0c3e6dd1c | ||
|
|
39f3c5a84e | ||
|
|
24ac8e1081 | ||
|
|
6cfaf29ba0 | ||
|
|
bdb3ac8954 | ||
|
|
eeabb8088a | ||
|
|
6b3ff1f3b2 | ||
|
|
b12908f01f | ||
|
|
84ca8bc2db | ||
|
|
6e2b29e84c | ||
|
|
c0029e7e62 | ||
|
|
67e73d0dfa | ||
|
|
d47fcda679 | ||
|
|
4ca27d98ed | ||
|
|
7d8ffaa15b | ||
|
|
4a3d5b142f | ||
|
|
72ed196329 | ||
|
|
ff981622be | ||
|
|
437d9eb644 | ||
|
|
394daac730 | ||
|
|
fbbf3189fc | ||
|
|
b1751f69cb | ||
|
|
ce28366278 | ||
|
|
166759075a | ||
|
|
c422a13df3 | ||
|
|
f6eaf84f85 | ||
|
|
f2f9b7d1c9 | ||
|
|
f0cffa7127 | ||
|
|
7068167866 | ||
|
|
8345945bdc | ||
|
|
0e226f7328 | ||
|
|
ab2dc073b9 | ||
|
|
ccdd070fed | ||
|
|
4d9a6fedb7 | ||
|
|
e1d8f59890 | ||
|
|
b4d164e116 | ||
|
|
be7e77787e | ||
|
|
f0cadb1335 | ||
|
|
ac6b898a46 | ||
|
|
5a7de58525 | ||
|
|
3a4ae5401a | ||
|
|
ac14d2c76b | ||
|
|
37989d5f7a | ||
|
|
467b02b3d2 | ||
|
|
4eaa3c5c89 | ||
|
|
49bb7d7c5f | ||
|
|
1f172c8127 | ||
|
|
1ef829c33b | ||
|
|
da028da20b | ||
|
|
095f95bbff | ||
|
|
5ccedb8cd0 | ||
|
|
0773d0e75f | ||
|
|
24a0bc0b55 | ||
|
|
5eaaea535c | ||
|
|
a4d7e886d5 | ||
|
|
2b9061d74d | ||
|
|
6aa427baec | ||
|
|
28a602e01e | ||
|
|
33cd7b442d | ||
|
|
e0d9c99075 | ||
|
|
5c6f51bdaa | ||
|
|
ef5eb2fb40 | ||
|
|
619532f03b | ||
|
|
a2b52658a8 | ||
|
|
73abc9b5e9 | ||
|
|
480d0f3f11 | ||
|
|
04378c5937 | ||
|
|
cb0e6e12f9 | ||
|
|
9d7819b186 | ||
|
|
93ba4a0155 | ||
|
|
4d3b0b96dc | ||
|
|
1fc2ad6824 | ||
|
|
9b7a614b8b | ||
|
|
7ae86268a9 | ||
|
|
fb0a4a8f26 | ||
|
|
87d2ecef34 | ||
|
|
f34eaa6ee2 | ||
|
|
18672d6dd1 | ||
|
|
96708a27a6 | ||
|
|
aed9faa72f | ||
|
|
87cda49994 | ||
|
|
2a148f3c75 | ||
|
|
9a829c162d | ||
|
|
bf21a20315 | ||
|
|
26691f773f | ||
|
|
e45fc340e8 | ||
|
|
2fd40aeb23 | ||
|
|
6c68f3744f | ||
|
|
45c6562717 | ||
|
|
61c1ad624a | ||
|
|
e5e01f5939 | ||
|
|
b5f8a0d560 | ||
|
|
5697f3273f | ||
|
|
231125e406 | ||
|
|
1f5a27343d | ||
|
|
fdbaaaaca9 | ||
|
|
597fee16df | ||
|
|
ed26d157b5 | ||
|
|
e9d4ce2012 | ||
|
|
91c2a9cb5b | ||
|
|
0528bfe181 | ||
|
|
dfbc1f92e7 | ||
|
|
3446862eda | ||
|
|
1fa46fea12 | ||
|
|
bde07be4e2 | ||
|
|
c724f7d7ae | ||
|
|
4c2e4a6e04 | ||
|
|
8f2c81a7d7 | ||
|
|
5ff0e45b24 | ||
|
|
6886241154 | ||
|
|
bc59cf6089 | ||
|
|
0a7e5e0a93 | ||
|
|
090483543b | ||
|
|
652e336d86 | ||
|
|
9110ab1ce9 | ||
|
|
caab8f746b | ||
|
|
519ab57137 | ||
|
|
9d278c87aa | ||
|
|
4ef575f2a9 | ||
|
|
41199221f6 | ||
|
|
2135b1a561 | ||
|
|
1042c5e2b9 | ||
|
|
aa0902ee15 | ||
|
|
95c0634300 | ||
|
|
2e77206080 | ||
|
|
29585076ed | ||
|
|
614905fa6a | ||
|
|
c0411ef4a8 | ||
|
|
f96b834de5 | ||
|
|
67e3b77c39 | ||
|
|
6d85fb2826 | ||
|
|
31ad2fec27 | ||
|
|
88ee4468be | ||
|
|
10476bd523 | ||
|
|
1541bed3ef | ||
|
|
0890711911 | ||
|
|
70b4ab838b | ||
|
|
aa57c5d911 | ||
|
|
ed54509383 | ||
|
|
83bf21c20e | ||
|
|
a8f6a31128 | ||
|
|
633cdf246c | ||
|
|
3924b2eb3b | ||
|
|
c7ed586bb3 | ||
|
|
87148af4e5 | ||
|
|
2218d4615e | ||
|
|
be6e29515d | ||
|
|
294b519d02 | ||
|
|
3a1e5b8d89 | ||
|
|
d4d68d17cf | ||
|
|
8bb5b27e9f | ||
|
|
9a17bbccbb | ||
|
|
8be398e0c0 | ||
|
|
3ed3d5eb40 | ||
|
|
af1cdd2d7b | ||
|
|
8780624689 | ||
|
|
e8fd6b724d | ||
|
|
e9de5df8a5 | ||
|
|
9511e2e201 | ||
|
|
f23a9425c3 | ||
|
|
8494bd2197 | ||
|
|
3f814d123c | ||
|
|
1fdccbcb0d | ||
|
|
1276298187 | ||
|
|
67248bfa09 | ||
|
|
ca2154b3d8 | ||
|
|
9bd82baf53 | ||
|
|
0e9ff561ac | ||
|
|
352222f49d | ||
|
|
eb38d6a2b1 | ||
|
|
524f4d79a4 | ||
|
|
891e750a97 | ||
|
|
19ef8d3226 | ||
|
|
0d7eb9eb8c | ||
|
|
29276fae23 | ||
|
|
88b911bfc2 | ||
|
|
1d2df69507 | ||
|
|
4679804500 | ||
|
|
ef94d755c6 | ||
|
|
112ace95d7 | ||
|
|
329fb36219 | ||
|
|
072a85750b | ||
|
|
e4fe2dd907 | ||
|
|
209070f90e | ||
|
|
14e033b2e9 | ||
|
|
6bd5eba707 | ||
|
|
dd1448a145 | ||
|
|
4e7667e277 | ||
|
|
4c82f72c8f | ||
|
|
60471eacde | ||
|
|
ae1bb8cce1 | ||
|
|
080939a853 | ||
|
|
73954d12fe | ||
|
|
3f499b243b | ||
|
|
245ca210bc | ||
|
|
a32d53975a | ||
|
|
a1da98d29c | ||
|
|
04a68fe074 | ||
|
|
1be04cb571 | ||
|
|
03d689ac18 | ||
|
|
7ce3bc9be9 | ||
|
|
ebd1a8d70d | ||
|
|
25899b63da | ||
|
|
ebe284bcb2 | ||
|
|
d80260c113 | ||
|
|
da5a71c93b | ||
|
|
ea3a1c8e57 | ||
|
|
4df87a58e0 | ||
|
|
94705e1fec | ||
|
|
ff637e74d5 | ||
|
|
f94f0d1d71 | ||
|
|
9aa4b1447f | ||
|
|
5bb3152d02 | ||
|
|
44b36801ed | ||
|
|
2f3c34ca43 | ||
|
|
593eed7988 | ||
|
|
e132883103 | ||
|
|
bf77195210 | ||
|
|
01f73bc1f8 | ||
|
|
5178e5aeba | ||
|
|
19c5e6b0a7 | ||
|
|
cc34301e7f | ||
|
|
3d785a62b9 | ||
|
|
7ced3652e5 | ||
|
|
3cdc12b6d0 | ||
|
|
2110c8ef29 | ||
|
|
32195ef778 | ||
|
|
099a57cb23 | ||
|
|
af0da7ca88 | ||
|
|
d94fc06236 | ||
|
|
7e67a1f5a1 | ||
|
|
064dab994c | ||
|
|
85f64aa656 | ||
|
|
9393797e80 | ||
|
|
0f8f4b8952 | ||
|
|
eef9a2ae6c | ||
|
|
70efd85d85 | ||
|
|
de27ae8302 | ||
|
|
f40928dc0e | ||
|
|
fe01d57457 | ||
|
|
f2a602cc81 | ||
|
|
7d96366924 | ||
|
|
57acfae45d | ||
|
|
5c5962df50 | ||
|
|
6a83c41198 | ||
|
|
d80832fe34 | ||
|
|
9b086e711d | ||
|
|
6366dc1c82 | ||
|
|
88ae7fb857 | ||
|
|
8587c009ee | ||
|
|
171fd05ecb | ||
|
|
0265211149 | ||
|
|
035a13acff | ||
|
|
49a11ce0df | ||
|
|
de8674284f | ||
|
|
89c32dfc57 | ||
|
|
2d90a847cc | ||
|
|
c29ac5da5c | ||
|
|
e38987d916 | ||
|
|
67ba3f62e2 | ||
|
|
1ca9c6d936 | ||
|
|
2f619d9114 | ||
|
|
f4be54d1d1 | ||
|
|
bd6ea56627 | ||
|
|
811734c50b | ||
|
|
46a7880ba9 | ||
|
|
818fa307b9 | ||
|
|
34a372686b | ||
|
|
85a1decfb4 | ||
|
|
dd643daa78 | ||
|
|
095eaac420 | ||
|
|
212a3e62d1 | ||
|
|
2e1d0591b7 | ||
|
|
ea21b1b644 | ||
|
|
27f99e01d6 | ||
|
|
943f1178f9 | ||
|
|
cdea680f08 | ||
|
|
6a589a43c4 | ||
|
|
1aeeda4828 | ||
|
|
bbde617414 | ||
|
|
7339cf10a3 | ||
|
|
23c2fa1119 | ||
|
|
4c02a75221 | ||
|
|
6bd2ad4133 | ||
|
|
fa87946d4c | ||
|
|
e76d42172d | ||
|
|
37cab3fe3c | ||
|
|
d1f4688b5c | ||
|
|
4b9255d3dd | ||
|
|
f24127f631 | ||
|
|
bc7a763487 | ||
|
|
79054b4472 | ||
|
|
be629f519a | ||
|
|
32ad466d9d | ||
|
|
457c4a48f9 | ||
|
|
f449b4ebf1 | ||
|
|
b78c4b8d4d | ||
|
|
7535565009 | ||
|
|
835f4739fd | ||
|
|
6ace1ed524 | ||
|
|
ade7ae2649 | ||
|
|
db56019747 | ||
|
|
28073fbd81 | ||
|
|
564167a980 | ||
|
|
d7f48d0366 | ||
|
|
0e4ad7eec3 | ||
|
|
8b866e1577 | ||
|
|
1401562ddb | ||
|
|
9241a7cf1f | ||
|
|
7f6c9a53ac | ||
|
|
b926fb856c | ||
|
|
047a3a35bf | ||
|
|
7c07949f4b | ||
|
|
39faa8707f | ||
|
|
6552dc4aa8 | ||
|
|
5566ebe401 | ||
|
|
3ea40e1bd3 | ||
|
|
313682cbbd | ||
|
|
a5962f699b | ||
|
|
0577756dc1 | ||
|
|
6f318a8b05 | ||
|
|
b97273eace | ||
|
|
0eba5443b4 | ||
|
|
ee8c25dea9 | ||
|
|
bd0a0e4592 | ||
|
|
cc7e21a609 | ||
|
|
3aa39bd1ee | ||
|
|
d5c98aabe4 | ||
|
|
e9afde0be4 | ||
|
|
adf0272038 | ||
|
|
726221df5a | ||
|
|
63dd15c10e | ||
|
|
e9cb9629ef | ||
|
|
74f311fb2a | ||
|
|
29d3257dd3 | ||
|
|
9aad53d753 | ||
|
|
3dc0a3aa41 | ||
|
|
342ea45951 | ||
|
|
3d5985de73 | ||
|
|
64741a966c | ||
|
|
55d79ae007 | ||
|
|
773e0aca60 | ||
|
|
799e649e51 | ||
|
|
a4a73c0c9e | ||
|
|
d3bc434d38 | ||
|
|
61b7084939 | ||
|
|
61ec79a39d | ||
|
|
a8ab055147 | ||
|
|
dcdcf1aeea | ||
|
|
587edf6ae1 | ||
|
|
10f498daf2 | ||
|
|
fc5ba74a83 | ||
|
|
a43f089adb | ||
|
|
0026102f45 | ||
|
|
f508dd6114 | ||
|
|
2feb1a6fba | ||
|
|
e20fbfa139 | ||
|
|
ebca8116e3 | ||
|
|
9bd3e71477 | ||
|
|
e5ec5ac5dd | ||
|
|
bae638b7d7 | ||
|
|
44cba3f3ae | ||
|
|
14cde02b86 | ||
|
|
6ec46e42ea | ||
|
|
d1cd96cca9 | ||
|
|
6d93ca0f42 | ||
|
|
d3920e730e | ||
|
|
fc39dee2e0 | ||
|
|
da83041fca | ||
|
|
c70d86e1c7 | ||
|
|
c808cb91a5 | ||
|
|
7655b3e0ee | ||
|
|
12052d796a | ||
|
|
721ee76923 | ||
|
|
d5d5a4a9d7 | ||
|
|
763f8c655c | ||
|
|
143107b7f9 | ||
|
|
5b38db9557 | ||
|
|
e74140dc08 | ||
|
|
b8d7358ebd | ||
|
|
62d01d4168 | ||
|
|
352c87da5c | ||
|
|
0f4fe7c350 | ||
|
|
d715f0b6d6 | ||
|
|
136feb768e | ||
|
|
68cdbe1684 | ||
|
|
e19cc9bccf | ||
|
|
54dddd29ec | ||
|
|
b6b04a99a0 | ||
|
|
680ca8a955 | ||
|
|
192b48accc | ||
|
|
3413f6e498 | ||
|
|
497de5bcff | ||
|
|
1c19236d62 | ||
|
|
77f76514c4 | ||
|
|
ca6e037086 | ||
|
|
2e2c237a26 | ||
|
|
46fae4da0c | ||
|
|
058ae48798 | ||
|
|
cae1406d2b | ||
|
|
f0bcc56483 | ||
|
|
8faf0c27e1 | ||
|
|
935ec22c4b | ||
|
|
2b9717535b | ||
|
|
fe1c0a45d7 | ||
|
|
e51fd602bf | ||
|
|
ce0ca2844b | ||
|
|
ceb3267821 | ||
|
|
a6554b604c | ||
|
|
cd9b631d8c | ||
|
|
8b3e3cb7b0 | ||
|
|
0a80ba4e0d | ||
|
|
7c2e966784 | ||
|
|
1c9ddfddee | ||
|
|
2f7cdbb670 | ||
|
|
c63e9a0668 | ||
|
|
406d846b6b | ||
|
|
84c3904de8 | ||
|
|
a2949e60c4 | ||
|
|
1b6fa4c3e8 | ||
|
|
a9d0ca531e | ||
|
|
e61cbd00aa | ||
|
|
0d7741a427 | ||
|
|
3916144723 | ||
|
|
6bba5acd6a | ||
|
|
5e757f81dc | ||
|
|
c160b1a13d | ||
|
|
766323bc31 | ||
|
|
34c81cec18 | ||
|
|
c3b2f08b9b | ||
|
|
14bb05ad4e | ||
|
|
fc5f9b24c3 | ||
|
|
24a93d2ab3 | ||
|
|
dddfb4367c | ||
|
|
a9bce6458a | ||
|
|
501defe6f1 | ||
|
|
4d70e18dc1 | ||
|
|
c5c817a7b7 | ||
|
|
5ae5a8717a | ||
|
|
e698aff97a | ||
|
|
285ff0a7dd | ||
|
|
6e6cf7183d | ||
|
|
8056e9d668 | ||
|
|
4a85823439 | ||
|
|
cb78942b8d | ||
|
|
7791f371b4 | ||
|
|
3703406ff7 | ||
|
|
d0934cd6b0 | ||
|
|
202e459aad | ||
|
|
d0d34fbbd4 | ||
|
|
dc36fb6c2b | ||
|
|
c34a6c1c9f | ||
|
|
e91561431a | ||
|
|
42df4f4eef | ||
|
|
c1437de575 | ||
|
|
4b8e36794b | ||
|
|
10a18edd24 | ||
|
|
e46603ac4f | ||
|
|
9e12d43fe0 | ||
|
|
f0bbffe31d | ||
|
|
3a06ee5cf1 | ||
|
|
420804d9fe | ||
|
|
099b36e1bf | ||
|
|
98c838352b | ||
|
|
e012f92e06 | ||
|
|
0985363be6 | ||
|
|
d3ea3c2c54 | ||
|
|
4eb811aac4 | ||
|
|
bc0d2f246b | ||
|
|
7cdd9d19d7 | ||
|
|
07b3c92f1b | ||
|
|
5a0c6392e0 | ||
|
|
158b139415 | ||
|
|
34c5a05fda | ||
|
|
badffc234e | ||
|
|
8ae5ee4de9 | ||
|
|
0072f2741b | ||
|
|
c2715adab7 | ||
|
|
fe768be48d | ||
|
|
ed4c0e8942 | ||
|
|
c34aac28f6 | ||
|
|
b46a9d5664 | ||
|
|
d5554487f4 | ||
|
|
1b44ac995c | ||
|
|
e08a3ab42d | ||
|
|
2857d8f543 | ||
|
|
2de67e9bb9 | ||
|
|
4b11ef3499 | ||
|
|
45e77aa7a7 | ||
|
|
fe1054212b | ||
|
|
5734bd1fb1 | ||
|
|
c46901802d | ||
|
|
c459b97ca3 | ||
|
|
ea10573a3d | ||
|
|
52682a5c2e | ||
|
|
d4164d7c58 | ||
|
|
61a6eb792b | ||
|
|
7aafce6e2e | ||
|
|
801d948513 | ||
|
|
11c8f6eb4d | ||
|
|
ee5c4c05ce | ||
|
|
78b8cb5909 | ||
|
|
a22e4b9b91 | ||
|
|
6d949858b8 | ||
|
|
6fbc2ede3a | ||
|
|
1de239549f | ||
|
|
7cb8c473d4 | ||
|
|
e4c685d382 | ||
|
|
10b8d115a1 | ||
|
|
9cd1831ecb | ||
|
|
ba12e39bbc | ||
|
|
4ae35e51a9 | ||
|
|
1ba580302a | ||
|
|
5e79cb5539 | ||
|
|
82eab0db3c | ||
|
|
86e43371e6 | ||
|
|
564300d07c | ||
|
|
f8fe2053aa | ||
|
|
d6e05acaca | ||
|
|
91b2634472 | ||
|
|
fad843224a | ||
|
|
f20286b43b | ||
|
|
c784969324 | ||
|
|
3f40a77b02 | ||
|
|
49b53aee2e | ||
|
|
ae133891be | ||
|
|
8ea9d841f0 | ||
|
|
7c4cdbbdf6 | ||
|
|
aca1091650 | ||
|
|
70ec62e56b | ||
|
|
0a09297cef | ||
|
|
91685c5a7c | ||
|
|
88a34e456e | ||
|
|
204710ea58 | ||
|
|
efb54e730e | ||
|
|
96c8d071f1 | ||
|
|
79733f62d5 | ||
|
|
159ee161b3 | ||
|
|
fd7aeebd25 | ||
|
|
127ce6fb3f | ||
|
|
edaca7edc2 | ||
|
|
f2734e726c | ||
|
|
46aafa4a4d | ||
|
|
3bdf179a72 | ||
|
|
6ed71f1a4a | ||
|
|
d965dcf171 | ||
|
|
b381e2d8ba | ||
|
|
59079def6a | ||
|
|
9208bb3ac3 | ||
|
|
b0def64cf1 | ||
|
|
548668e8d4 | ||
|
|
ef76621a86 | ||
|
|
3fc3ae02bb | ||
|
|
9dd10f14a8 | ||
|
|
82293ca88f | ||
|
|
f48581dcd8 | ||
|
|
0ae206f4bd | ||
|
|
43b09de167 | ||
|
|
379c5bfdb6 | ||
|
|
ee7589ba93 | ||
|
|
a1b53dc2b0 | ||
|
|
a24e77be98 | ||
|
|
c20f27fadf | ||
|
|
5fb9f5a6ec | ||
|
|
d0a9be1b79 | ||
|
|
a067bfa733 | ||
|
|
6e64edb1c6 | ||
|
|
8b9ffaab1f | ||
|
|
369506013f | ||
|
|
6ac55ee0dd | ||
|
|
e34a4e630b | ||
|
|
52b39e92d1 | ||
|
|
b9ae2e107a | ||
|
|
8dfc702325 | ||
|
|
e3a1cd0a37 | ||
|
|
f2321fcbc4 | ||
|
|
6cd56991c3 | ||
|
|
22e24514e0 | ||
|
|
d65829175e | ||
|
|
135f634c36 | ||
|
|
3783a0fff2 | ||
|
|
12bbec013a | ||
|
|
d03330ea03 | ||
|
|
7f9faf3676 | ||
|
|
8c64b91839 | ||
|
|
017963bb0b | ||
|
|
5e40fde99a | ||
|
|
bf95873bd0 | ||
|
|
ae45ff3e44 | ||
|
|
c6a15dca14 | ||
|
|
bf72e417d5 | ||
|
|
5f1eb70516 | ||
|
|
2aaae11e97 | ||
|
|
0a410d2c1a | ||
|
|
9e961e5b60 | ||
|
|
16a1d3a0ec | ||
|
|
5df44b4fb8 | ||
|
|
7a4c4b8f76 | ||
|
|
38a8a17b10 | ||
|
|
46fa1d2528 | ||
|
|
24f7823f44 | ||
|
|
caf9f87db6 | ||
|
|
bcb8d9c394 | ||
|
|
1ab619680b | ||
|
|
65299a6e94 | ||
|
|
05e606ba65 | ||
|
|
01cbb9c30b | ||
|
|
130d371c44 | ||
|
|
534aa02b84 | ||
|
|
29def026fc | ||
|
|
827e92e6f3 | ||
|
|
2d9228034d | ||
|
|
5128fa590c | ||
|
|
de66233b90 | ||
|
|
fe3b38c5fd | ||
|
|
82dde149ee | ||
|
|
0742d635af | ||
|
|
9556998518 | ||
|
|
ad92e1b5f7 | ||
|
|
bed525297e | ||
|
|
90e631726c | ||
|
|
a5eb0a874f | ||
|
|
c2a37057f1 | ||
|
|
5bb1a83ed9 | ||
|
|
f6414843cf | ||
|
|
9e91be145e | ||
|
|
aac8f08da9 | ||
|
|
7729a1dcb8 | ||
|
|
3891cafc15 | ||
|
|
31755c5031 | ||
|
|
9bc2756ac5 | ||
|
|
94b928e194 | ||
|
|
fa32d0ff87 | ||
|
|
1777988294 | ||
|
|
3175f44a9a | ||
|
|
6152f5e91a | ||
|
|
be049d9145 | ||
|
|
053b8080a4 | ||
|
|
38edeaebf3 | ||
|
|
4eeb76f9fb | ||
|
|
0728192ca5 | ||
|
|
1d380264e4 | ||
|
|
7c23adfc24 | ||
|
|
a82435dc29 | ||
|
|
8463ccda69 | ||
|
|
5e16d319e3 | ||
|
|
b668d6fd21 | ||
|
|
2e6523625a | ||
|
|
dccbfb7e5d | ||
|
|
3fcaca917a | ||
|
|
d982a76048 | ||
|
|
6855413109 | ||
|
|
74bd06c768 | ||
|
|
1eb84517db | ||
|
|
870e276ec5 | ||
|
|
8a0b2de540 | ||
|
|
6a9732aaa8 | ||
|
|
f34abbe184 | ||
|
|
8146b8b988 | ||
|
|
8cc95078e9 | ||
|
|
69f6e44720 | ||
|
|
52abfb8a97 | ||
|
|
0311d4cdfc | ||
|
|
92ea7914b8 | ||
|
|
194ec9241c | ||
|
|
8fe5536ea7 | ||
|
|
0d7642f727 | ||
|
|
1b6074c60b | ||
|
|
5d0db4a8fa | ||
|
|
89b8a47a76 | ||
|
|
e6e62cf350 | ||
|
|
8bdce3c1b3 | ||
|
|
60d4e2ad52 | ||
|
|
dcb6c07eee | ||
|
|
1a766cfde3 | ||
|
|
1f5a2e92cc | ||
|
|
ad7080438f | ||
|
|
64e601c70d | ||
|
|
80940fccb2 | ||
|
|
3d917a8f57 | ||
|
|
74e28ca101 | ||
|
|
76e86c64ab | ||
|
|
7555a7c27e | ||
|
|
a5d03a7d8e | ||
|
|
750a338d53 | ||
|
|
37f75caacf | ||
|
|
8487e74dde | ||
|
|
5736fee505 | ||
|
|
af4968618a | ||
|
|
37cc3f63d2 | ||
|
|
42cb19e818 | ||
|
|
cbd717666a | ||
|
|
5e1a293e9f | ||
|
|
9eb157b871 | ||
|
|
4351122cb4 | ||
|
|
02acc4a191 | ||
|
|
9768f3e724 | ||
|
|
9905a4f2d0 | ||
|
|
a3492ad72b | ||
|
|
f8db9c927b | ||
|
|
fc4bf7648f | ||
|
|
c8415d8370 | ||
|
|
f21ffea568 | ||
|
|
5d6902c196 | ||
|
|
d396a56721 | ||
|
|
74c8a253e8 | ||
|
|
ef747a2f40 | ||
|
|
72877ab98b | ||
|
|
44baaa84b8 | ||
|
|
f5b3b503e4 | ||
|
|
4cbc449eb1 | ||
|
|
0dca7ff6a2 | ||
|
|
2f27ae3464 | ||
|
|
9951398c36 | ||
|
|
8d90ca0cc6 | ||
|
|
c98912f31b | ||
|
|
57a040f648 | ||
|
|
033a64a725 | ||
|
|
bfd718ce76 | ||
|
|
a25fe345ed | ||
|
|
676a64d0f5 | ||
|
|
266b809353 | ||
|
|
c85b312504 | ||
|
|
6e54b541dd | ||
|
|
1efd7d0330 | ||
|
|
20f48999d1 | ||
|
|
2707332ec8 | ||
|
|
958cd45b0c | ||
|
|
5fd156545f | ||
|
|
7fe43cc882 | ||
|
|
161e4b9543 | ||
|
|
b4479e570a | ||
|
|
85f285271e | ||
|
|
ad3a8f38ae | ||
|
|
fc8b8ac5ee | ||
|
|
0ac41060ac | ||
|
|
a6f5e30e39 | ||
|
|
6119e254a6 | ||
|
|
5132e6252a | ||
|
|
b7af569996 | ||
|
|
c6d52f05c5 | ||
|
|
8f2934e510 | ||
|
|
8cffee569a | ||
|
|
64ac7d2c61 | ||
|
|
7bbf539f14 | ||
|
|
78c9a290cc | ||
|
|
4526c2a28e | ||
|
|
688a43691b | ||
|
|
0ffbe17b7e | ||
|
|
c18eb02a04 | ||
|
|
ffc505a712 | ||
|
|
7c269e3e17 | ||
|
|
e2f87e94dd | ||
|
|
bea88879af | ||
|
|
d022cbf124 | ||
|
|
f3b218e86e | ||
|
|
2e97ade02b | ||
|
|
e52321132d | ||
|
|
e1d927cfae | ||
|
|
c001e5c069 | ||
|
|
5d99002aec | ||
|
|
5f3f3afeb7 | ||
|
|
2bd891f7ba | ||
|
|
46ebd84c39 | ||
|
|
0662339124 | ||
|
|
b63990d4ce | ||
|
|
ff0e13caf0 | ||
|
|
12175a0788 | ||
|
|
d9e9c4d2ba | ||
|
|
e8ee8891b0 | ||
|
|
55db1c2e75 | ||
|
|
f668686daf | ||
|
|
2a7bcb8872 | ||
|
|
9b974b8632 | ||
|
|
780bd32cf4 | ||
|
|
9428a19f4f | ||
|
|
bfe177aa17 | ||
|
|
6207d5a807 | ||
|
|
7aefe652e5 | ||
|
|
d6dfd0ae9a | ||
|
|
40f999f556 | ||
|
|
b007fb1c55 | ||
|
|
2b0476c8b9 | ||
|
|
7eee38f107 | ||
|
|
8e55a5426c | ||
|
|
62652dccd4 | ||
|
|
7cc7b5a7c3 | ||
|
|
5abfe3c20b | ||
|
|
98ac2232a3 | ||
|
|
7430bd25f8 | ||
|
|
239b0acce3 | ||
|
|
63b9bc9f8e | ||
|
|
f4f5ef07e7 | ||
|
|
ae986b215e | ||
|
|
bd5968c5cb | ||
|
|
80c043f707 | ||
|
|
f5f635cac4 | ||
|
|
314e0c86b6 | ||
|
|
9eafb5f340 | ||
|
|
3b6222c6f7 | ||
|
|
fa1854f2d3 | ||
|
|
8ebfb99401 | ||
|
|
785f9b5c7f | ||
|
|
6714833b0f | ||
|
|
29b30bba92 | ||
|
|
4a62b31db0 | ||
|
|
bd71a96ccb | ||
|
|
a054f05b1b | ||
|
|
0519a2bbfb | ||
|
|
91cc9802a3 | ||
|
|
574a84cdb2 | ||
|
|
de1c0a5c96 | ||
|
|
16cac0d557 | ||
|
|
528a94455b | ||
|
|
7e416406f2 | ||
|
|
d99a10f052 | ||
|
|
f1f803f8e3 | ||
|
|
c051d663f9 | ||
|
|
acbdfa501b | ||
|
|
6c96673966 | ||
|
|
8e93080a8e | ||
|
|
465f9f9628 | ||
|
|
e5238fd4a7 | ||
|
|
808007ece6 | ||
|
|
5152b3679c | ||
|
|
39a63cb0e2 | ||
|
|
033cece3a7 | ||
|
|
38b3bc22e1 | ||
|
|
8f0ccdda5b | ||
|
|
f8ce0ee0e6 | ||
|
|
6ed59df596 | ||
|
|
c0fb949a38 | ||
|
|
6d6e678f72 | ||
|
|
ff675330c6 | ||
|
|
fb67930ed1 | ||
|
|
31ea7ebf8a | ||
|
|
6968f81366 | ||
|
|
27eb498793 | ||
|
|
db328692ef | ||
|
|
e5aadddbf1 | ||
|
|
f9a1ec5feb | ||
|
|
db97346752 | ||
|
|
5283741f30 | ||
|
|
9ae3bf3530 | ||
|
|
1bd98c7769 | ||
|
|
869654084f | ||
|
|
bfc5ff04f8 | ||
|
|
a490416544 | ||
|
|
f24ca9c30d | ||
|
|
ca692ce9bf | ||
|
|
708bcea533 | ||
|
|
376f5e7b5a | ||
|
|
a2c090f234 | ||
|
|
0ea376df10 | ||
|
|
84ef79e598 | ||
|
|
a49f382b16 | ||
|
|
1b5515cc95 | ||
|
|
1380d66da1 | ||
|
|
638b5c25ec | ||
|
|
bd851ac8d3 | ||
|
|
949813da8a | ||
|
|
508cca8215 | ||
|
|
77500adb8d | ||
|
|
8260043333 | ||
|
|
36d750501e | ||
|
|
5678fec290 | ||
|
|
94c3b72a52 | ||
|
|
bdb9023f89 | ||
|
|
717a56a580 | ||
|
|
f185b2f475 | ||
|
|
67611af116 | ||
|
|
0bad71decc | ||
|
|
5ac4897fa9 | ||
|
|
f82d5bfd54 | ||
|
|
07ffb89a5d | ||
|
|
2d43061f8e | ||
|
|
01ade68d4e | ||
|
|
46e406bfc4 | ||
|
|
17b33d57df | ||
|
|
a9b137be02 | ||
|
|
318f55065e | ||
|
|
fba51e5847 | ||
|
|
bbb5fcce30 | ||
|
|
fd433754fc | ||
|
|
a7644fce23 | ||
|
|
243492ac91 | ||
|
|
475da85b51 | ||
|
|
3e6bce367c | ||
|
|
8185050ea2 | ||
|
|
1a7cf32dc6 | ||
|
|
379e916428 | ||
|
|
1b554c7c0c | ||
|
|
61ecbe1e84 | ||
|
|
a870efc44b | ||
|
|
fe34d269dc | ||
|
|
3d59e3b265 | ||
|
|
b76ae66b79 | ||
|
|
2996c4e67e | ||
|
|
e860a4d912 | ||
|
|
61fed5d898 | ||
|
|
153764f48a | ||
|
|
0efecc6daf | ||
|
|
724f1f4eff | ||
|
|
33ffa14edf | ||
|
|
7b804aba44 | ||
|
|
7d33a6dff2 | ||
|
|
ee524bc57e | ||
|
|
998fa6d3dc | ||
|
|
0b5b3a62fe | ||
|
|
7fe1cb329a | ||
|
|
8957e65e61 | ||
|
|
1722cfecff | ||
|
|
00561ae3ba | ||
|
|
7fd8b089d5 | ||
|
|
6657903c57 | ||
|
|
f6283e9c93 | ||
|
|
d59a1a05a9 | ||
|
|
944f852285 | ||
|
|
ad1e1c98bc | ||
|
|
c266a098d4 | ||
|
|
0bc021cbe8 | ||
|
|
776501b363 | ||
|
|
040ca9c8e9 | ||
|
|
f98bab23a7 | ||
|
|
305b061f1c | ||
|
|
54ff9296b2 | ||
|
|
4f8bad129b | ||
|
|
d83bb4cf5a | ||
|
|
aa3ca4036e | ||
|
|
8ac248bf3d | ||
|
|
b8994ffd87 | ||
|
|
58ec4cd183 | ||
|
|
ddbf15abce | ||
|
|
7be50d0010 | ||
|
|
c87c3fa4da | ||
|
|
9fce7f6a94 | ||
|
|
1e220281ba | ||
|
|
e80ba886d7 | ||
|
|
95758919bd | ||
|
|
39472e64c4 | ||
|
|
b47738b424 | ||
|
|
7e33f04baa | ||
|
|
e7b362bfe9 | ||
|
|
c4cd3772cd | ||
|
|
8dd1ddd6ac | ||
|
|
29e106ce9a | ||
|
|
a393f80e23 | ||
|
|
b1058efebe | ||
|
|
ff4460620b | ||
|
|
be02b948aa | ||
|
|
5270c680a5 | ||
|
|
5e110cea8b | ||
|
|
53cb8d9088 | ||
|
|
f95656ab8d | ||
|
|
a936740aa4 | ||
|
|
584eb7e33a | ||
|
|
491893acc4 | ||
|
|
54a1174d9e | ||
|
|
913b18c5cc | ||
|
|
c287df5c10 | ||
|
|
ecabd835c2 | ||
|
|
6c04d496c9 | ||
|
|
494324fd8f | ||
|
|
49ac0a62d4 | ||
|
|
ebdb326af7 | ||
|
|
3a4be19bb5 | ||
|
|
af07c85bf4 | ||
|
|
d6edea6be3 | ||
|
|
2270cff006 | ||
|
|
5a5d4ad3b8 | ||
|
|
e64269dcc4 | ||
|
|
4c0cf3043a | ||
|
|
c42257394b | ||
|
|
0efd82b049 | ||
|
|
9495df1016 | ||
|
|
cae92e2252 | ||
|
|
d58ad09708 | ||
|
|
b0d734e19b | ||
|
|
7eee987e0f | ||
|
|
14cb8f11ca | ||
|
|
b91c31f793 | ||
|
|
e86a9c6048 | ||
|
|
abcc7ef80e | ||
|
|
1f3cac717a | ||
|
|
17504793ef | ||
|
|
755b4f580c | ||
|
|
06ba68ecdd | ||
|
|
b259a2a3d8 | ||
|
|
25d6e03cb0 | ||
|
|
bfda7a6e25 | ||
|
|
80e8c0d2b3 | ||
|
|
95a9adadef | ||
|
|
7662dd4bf5 | ||
|
|
a9692a2ee9 | ||
|
|
c532861091 | ||
|
|
ed795cfd9f | ||
|
|
c072a5a406 | ||
|
|
79e70089cd | ||
|
|
4d9600159b | ||
|
|
34c61291bb | ||
|
|
2fedd8b0bc | ||
|
|
bcf9eba5e3 | ||
|
|
213d08bc7e | ||
|
|
9410dc8291 | ||
|
|
6618cc6129 | ||
|
|
0344c9ae34 | ||
|
|
617f1a4d08 | ||
|
|
43055d493e | ||
|
|
48fd43b2a9 | ||
|
|
bd9f0bb58c | ||
|
|
4cd62309c6 | ||
|
|
4176ec4cb0 | ||
|
|
cfca2be71e | ||
|
|
c3b7c735dd | ||
|
|
0826d9ef0c | ||
|
|
4cd3bf741e | ||
|
|
c80fcabe67 | ||
|
|
acd9373938 | ||
|
|
fee6c5c62d | ||
|
|
cd7f4aa614 | ||
|
|
8c3f2cb243 | ||
|
|
e29d5ae9d4 | ||
|
|
58d60c20da | ||
|
|
f0abb8bcfa | ||
|
|
bc6547cf5c | ||
|
|
d90bd7fcfa | ||
|
|
6e0efd6607 | ||
|
|
5e97741812 | ||
|
|
6f5b7983a9 | ||
|
|
43133ac7ad | ||
|
|
c719f7b510 | ||
|
|
5e7174501d | ||
|
|
7f888ea08a | ||
|
|
10adecec7a | ||
|
|
7a8aa701d2 | ||
|
|
f6071edd75 | ||
|
|
e84764dcd9 | ||
|
|
b31cfc0487 | ||
|
|
5fc4f04adc | ||
|
|
fa9b2f6f60 | ||
|
|
d375df1720 | ||
|
|
7ebc06d049 | ||
|
|
5f0cca062d | ||
|
|
5a95fccd37 | ||
|
|
a4e21f02bb | ||
|
|
4b0b573645 | ||
|
|
44463faa7b | ||
|
|
5d7c34a6bf | ||
|
|
cdc8495f99 | ||
|
|
0ea26a5387 | ||
|
|
23b15a8a67 | ||
|
|
41d8e9ca87 | ||
|
|
34cd261128 | ||
|
|
4c0f356ee1 | ||
|
|
94433fe3e2 | ||
|
|
10594cd204 | ||
|
|
cbef891fe8 | ||
|
|
63d2f4962c | ||
|
|
9aeea21498 | ||
|
|
9eb8c305d2 | ||
|
|
4e4ca0076a | ||
|
|
c258156a54 | ||
|
|
af1396876d | ||
|
|
bf263c91d5 | ||
|
|
f4026816ee | ||
|
|
f27e4985cd | ||
|
|
6af238b754 | ||
|
|
2fe9de0d19 | ||
|
|
ccf5605b4a | ||
|
|
43a880775b | ||
|
|
0e09634c3f | ||
|
|
504cd6e802 | ||
|
|
4b1482fade | ||
|
|
fbd940310a | ||
|
|
d5dfe2219e | ||
|
|
7b15aff02e | ||
|
|
ecfd278f96 | ||
|
|
0b1e731246 | ||
|
|
3714162340 | ||
|
|
bbfbb5e5dc | ||
|
|
60e73760a7 | ||
|
|
f071be5cb6 | ||
|
|
658cddc9d6 | ||
|
|
d352e4ded6 | ||
|
|
9975dc6ba0 | ||
|
|
6c88a541e1 | ||
|
|
ae4e77d390 | ||
|
|
73d8df9166 | ||
|
|
e15b711464 | ||
|
|
80a1ad5d1a | ||
|
|
62e297822c | ||
|
|
73c9ea4e03 | ||
|
|
7c46520380 | ||
|
|
7a5860442c | ||
|
|
cd46a16178 | ||
|
|
cd78cadbbb | ||
|
|
9b3a347c2a | ||
|
|
ac37851c7d | ||
|
|
5a257c93fd | ||
|
|
9c04034cc8 | ||
|
|
7adc08ed93 | ||
|
|
7baed52999 | ||
|
|
99f3d8551c | ||
|
|
7404ea80ea | ||
|
|
c841ceda18 | ||
|
|
9f9cd1e64c | ||
|
|
5222f7cc68 | ||
|
|
0c43fd8965 | ||
|
|
7b9baf09a9 | ||
|
|
b596f6df54 | ||
|
|
cd16666aa7 | ||
|
|
128878c1cf | ||
|
|
9583203a35 | ||
|
|
83fab83f5b | ||
|
|
10b22d9aa3 | ||
|
|
ec5732ec2e | ||
|
|
1133334d36 | ||
|
|
74769883de | ||
|
|
f12a5d10a5 | ||
|
|
89352885a0 | ||
|
|
5db665ecf1 | ||
|
|
9ce41dad10 | ||
|
|
93a130801f | ||
|
|
4df19b6a53 | ||
|
|
0ad9652d3e | ||
|
|
f6fc7bc5cb | ||
|
|
372c7eebf1 | ||
|
|
143af06b91 | ||
|
|
ca3333d717 | ||
|
|
bcde7b6c4d | ||
|
|
408a4d7996 | ||
|
|
2536b3897c | ||
|
|
80bb888775 | ||
|
|
bfed9a2d26 | ||
|
|
e7350038a0 | ||
|
|
74b759ad6b | ||
|
|
dc440d53b6 | ||
|
|
84ebe47b89 | ||
|
|
936e2d9721 | ||
|
|
fab05ae888 | ||
|
|
8e01229369 | ||
|
|
08fbbd1b59 | ||
|
|
670a651e57 | ||
|
|
e1466efc51 | ||
|
|
f44b5cd7f9 | ||
|
|
8a91cffcf4 | ||
|
|
52b1939ae7 | ||
|
|
ca669f9fec | ||
|
|
69fae94707 | ||
|
|
fb22ffa31e | ||
|
|
90668b1e7a | ||
|
|
e71309f272 | ||
|
|
394c715b5b | ||
|
|
c819b956b4 | ||
|
|
83492f0a38 | ||
|
|
26403461b4 | ||
|
|
a6137149a0 | ||
|
|
aabfa535bf | ||
|
|
6b4d78230a | ||
|
|
d9eb202b6f | ||
|
|
fffdaa803d | ||
|
|
768e1d4de4 | ||
|
|
ec565d5365 | ||
|
|
77e5971c72 | ||
|
|
f44c7c99dd | ||
|
|
45d5dffd3c | ||
|
|
b014b1f3cc | ||
|
|
434faafe94 | ||
|
|
4466e01458 | ||
|
|
9be2ca29a1 | ||
|
|
c232f5e5cd | ||
|
|
38870a6638 | ||
|
|
f38ff130d2 | ||
|
|
63b220a2d0 | ||
|
|
01245d9c63 | ||
|
|
53356dd58c | ||
|
|
cabe79ecbb | ||
|
|
3c172db7d1 | ||
|
|
a4cdc7da6a | ||
|
|
ce65a36b8d | ||
|
|
fa82961eb3 | ||
|
|
cc94added0 | ||
|
|
66413a60dd | ||
|
|
a4507eea18 | ||
|
|
2ee8eca63b | ||
|
|
3affcc1ee6 | ||
|
|
b60eb501c3 | ||
|
|
55ee9e6325 | ||
|
|
19b2cbb010 | ||
|
|
f3f549494e | ||
|
|
799a1a27bb | ||
|
|
ed88a5b886 | ||
|
|
94590aa7e2 | ||
|
|
250be654bd | ||
|
|
31e6f615bc | ||
|
|
4db1acc455 | ||
|
|
23b03654fd | ||
|
|
fac28edba6 | ||
|
|
a11e5f906f | ||
|
|
c3459c1ffe | ||
|
|
08d3f04738 | ||
|
|
cb434717e0 | ||
|
|
9bb9c0e480 | ||
|
|
be5a0e386f | ||
|
|
fde7ea6a75 | ||
|
|
36dde7f0b3 | ||
|
|
13d4a84e7f | ||
|
|
28d1dbb47e | ||
|
|
572889990a | ||
|
|
ff306ed53d | ||
|
|
338a29baa3 | ||
|
|
fcb66fa0c8 | ||
|
|
47b80da502 | ||
|
|
246409fe2c | ||
|
|
88cd7352c8 | ||
|
|
957c48caeb | ||
|
|
4b9a90e6d3 | ||
|
|
6b593b99d6 | ||
|
|
78ef7404aa | ||
|
|
bcc2f69b83 | ||
|
|
f5049b3f65 | ||
|
|
c8e4df0349 | ||
|
|
18275b5700 | ||
|
|
d13603b3db | ||
|
|
213b42fd56 | ||
|
|
cf4600acd8 | ||
|
|
d99beb104d | ||
|
|
3a52c9ad1e | ||
|
|
3cc8fa885a | ||
|
|
38eb3ae5c6 | ||
|
|
84d76ed9a4 | ||
|
|
9ec37061e5 | ||
|
|
6e4dd14c3e | ||
|
|
ec9f491a0d | ||
|
|
32b01cf99c | ||
|
|
644fc4dbb1 | ||
|
|
cd415df824 | ||
|
|
4c1c2e4649 | ||
|
|
419766a2fc | ||
|
|
11ea8949ae | ||
|
|
a9b7073a08 | ||
|
|
f6eb6dc922 | ||
|
|
c1b5154c97 | ||
|
|
8011e7d96a | ||
|
|
a0cabdd8d2 | ||
|
|
d668a37174 | ||
|
|
0c81d65fe4 | ||
|
|
d597032231 | ||
|
|
0780c7d75f | ||
|
|
644ce9afa4 | ||
|
|
cd4e431be2 | ||
|
|
2bcf096c88 | ||
|
|
b2021aeaab | ||
|
|
7919f2ed77 | ||
|
|
b7d6477228 | ||
|
|
2be54f9aee | ||
|
|
1470187c02 | ||
|
|
02d8fbdb1c | ||
|
|
31e786f2a8 | ||
|
|
e3a37c576c | ||
|
|
027e9397d5 | ||
|
|
19b29b4955 | ||
|
|
99f06e5935 | ||
|
|
b13af36727 | ||
|
|
65f5b6bc0e | ||
|
|
65d1427876 | ||
|
|
1231fbd83c | ||
|
|
6ada555c5f | ||
|
|
b7896336fc | ||
|
|
beb108ba4d | ||
|
|
83e2d4bed8 | ||
|
|
8125407e4d | ||
|
|
944346d608 | ||
|
|
4701b1bf39 | ||
|
|
61837ae6de | ||
|
|
115cf8fab9 | ||
|
|
4286fe6aab | ||
|
|
b16ea1e7bf | ||
|
|
c77983509f | ||
|
|
f58b08c5a4 | ||
|
|
49a0687947 | ||
|
|
023e902297 | ||
|
|
9518f83668 | ||
|
|
970f8eb306 | ||
|
|
aab21dc9b4 | ||
|
|
1b89f1a40e | ||
|
|
3b0ee5325d | ||
|
|
a1fcf0702b | ||
|
|
546eb7e1fc | ||
|
|
aea8700ebb | ||
|
|
f83cfa79c8 | ||
|
|
470e1cbabe | ||
|
|
9be6e3de41 | ||
|
|
6627b5e7e6 | ||
|
|
a6a88065ef | ||
|
|
6bf2b4d975 | ||
|
|
01c8dc95ae | ||
|
|
e0d0c4f048 | ||
|
|
97caa41fee | ||
|
|
9d95c508b7 | ||
|
|
972353fec2 | ||
|
|
89f563c41b | ||
|
|
36374d6979 | ||
|
|
53ddab3fc0 | ||
|
|
42cddcfe80 | ||
|
|
67df35360d | ||
|
|
8e31472978 | ||
|
|
311ea80555 | ||
|
|
d13f6a1444 | ||
|
|
4adbf290c4 | ||
|
|
48d98fce97 | ||
|
|
8cb1a1ed16 | ||
|
|
bf60e6f549 | ||
|
|
4937d7b1f1 | ||
|
|
4942193b47 | ||
|
|
058b978357 | ||
|
|
731eb76040 | ||
|
|
3a8e02f986 | ||
|
|
bbd7e0d6ea | ||
|
|
b569e9b9ec | ||
|
|
94c751af85 | ||
|
|
f670925d6a | ||
|
|
b69476b246 | ||
|
|
28be2b3840 | ||
|
|
97a9958d1b | ||
|
|
ee600cfd3d | ||
|
|
6006357381 | ||
|
|
60ce296289 | ||
|
|
c3d9a66fda | ||
|
|
90814371d2 | ||
|
|
e2d13d0997 | ||
|
|
e42f0acddf | ||
|
|
b40c074e4d | ||
|
|
31c8ce9f09 | ||
|
|
baf74949cc | ||
|
|
7a8e522a19 | ||
|
|
b9df31dab3 | ||
|
|
be84fa234d | ||
|
|
c6ac2eff81 | ||
|
|
7843608b7b | ||
|
|
5de6a22347 | ||
|
|
9ebb452c3d | ||
|
|
aa65721acd | ||
|
|
08b5496f6d | ||
|
|
101f236123 | ||
|
|
631fb1e6fb | ||
|
|
931fe674a6 | ||
|
|
ba8ab43901 | ||
|
|
41d87fbf59 | ||
|
|
bb1a37f282 | ||
|
|
c7cc2ff551 | ||
|
|
2cc9d68303 | ||
|
|
d11f9d716f | ||
|
|
88155e5b01 | ||
|
|
83967cd55d | ||
|
|
0755b41cf0 | ||
|
|
1097f6d776 | ||
|
|
7ea247ced2 | ||
|
|
829e1de2b3 | ||
|
|
ffbcc4d940 | ||
|
|
8ffe90e387 | ||
|
|
ab5f89b3f3 | ||
|
|
4025e0ae70 | ||
|
|
79cbf69955 | ||
|
|
0b0ec88b8e | ||
|
|
1b8cbbd01d | ||
|
|
0d4ed64590 | ||
|
|
59e77ef7bf | ||
|
|
81f2c9e44e | ||
|
|
adbed5969a | ||
|
|
e0d3e321e9 | ||
|
|
4f926d6ba9 | ||
|
|
57a92893fe | ||
|
|
1987128b0e | ||
|
|
a0a7b59732 | ||
|
|
35f7ec1a1c | ||
|
|
f4099e5e76 | ||
|
|
44b3b7de33 | ||
|
|
d6e950f662 | ||
|
|
9d7401392f | ||
|
|
4cfc4d0d2e | ||
|
|
574133034d | ||
|
|
14d3a4a33e | ||
|
|
41df105bab | ||
|
|
0a8c7dd0d0 | ||
|
|
3c6d51f835 | ||
|
|
ac3fc3f29a | ||
|
|
a5c5090335 | ||
|
|
4f8e6e0014 | ||
|
|
401bc7f504 | ||
|
|
d3cd3e4cf1 | ||
|
|
cac3b2189a | ||
|
|
945299037b | ||
|
|
03d6e0facd | ||
|
|
355eb2544a | ||
|
|
7b7ff20c7d | ||
|
|
38ff47f1d6 | ||
|
|
821e043fef | ||
|
|
e15b71a89b | ||
|
|
6dabae6b85 | ||
|
|
d67b383716 | ||
|
|
92b80c1e1d | ||
|
|
4ef73793d8 | ||
|
|
248317a1a5 | ||
|
|
6b7230ff4f | ||
|
|
e7899a6b1d | ||
|
|
b8b04f1be4 | ||
|
|
a28f91a8b3 | ||
|
|
b4c8d79734 | ||
|
|
f322849c12 | ||
|
|
c45250a2df | ||
|
|
707d64c473 | ||
|
|
eedd77762d | ||
|
|
2cac67e9ad | ||
|
|
94fc82a613 | ||
|
|
55e7fab281 | ||
|
|
ba6838a488 | ||
|
|
5374591ef8 | ||
|
|
98cf5a81f8 | ||
|
|
aca449349a | ||
|
|
c89da3e7ae | ||
|
|
868c84a9eb | ||
|
|
a2bed56323 | ||
|
|
bc300eae4c | ||
|
|
524926c9b4 | ||
|
|
c4f7c3e7a6 | ||
|
|
9446da21aa | ||
|
|
06604f9767 | ||
|
|
e8be38dc02 | ||
|
|
6976a82ee5 | ||
|
|
b6c6011397 | ||
|
|
f7742046b2 | ||
|
|
e0a68c8f04 | ||
|
|
00499e5eba | ||
|
|
be9a355be4 | ||
|
|
dda25b81b8 | ||
|
|
e6e4e3e305 | ||
|
|
aa18458ed5 | ||
|
|
80ab946d5f | ||
|
|
b99b818cee | ||
|
|
dfe38d4046 | ||
|
|
1a91482541 | ||
|
|
555006f65a | ||
|
|
071e845f1c | ||
|
|
2a74897628 | ||
|
|
159c3222a8 | ||
|
|
ab8a8e8fae | ||
|
|
48ab3a1ca2 | ||
|
|
5cb7d16dfe | ||
|
|
7b50eaa372 | ||
|
|
be59d75f1a | ||
|
|
19ae8e05b1 | ||
|
|
00af9b9d2b | ||
|
|
c99f49ca7b | ||
|
|
ef1dfbaa36 | ||
|
|
2c1d75c55f | ||
|
|
971701cc88 | ||
|
|
ab1c9f5254 | ||
|
|
3e246b7aa6 | ||
|
|
92e6ed9205 | ||
|
|
2009b613ec | ||
|
|
156019e5a4 | ||
|
|
c495321021 | ||
|
|
c571aa0e52 | ||
|
|
71d6d00971 | ||
|
|
3f59e80ea1 | ||
|
|
edb20b4da7 | ||
|
|
3e9ee2d2a5 | ||
|
|
edefa119e9 | ||
|
|
cb5b4f4e49 | ||
|
|
35cba754cc | ||
|
|
0f4b63e67b | ||
|
|
96472dfc81 | ||
|
|
115d3a75c6 | ||
|
|
dbc322ac15 | ||
|
|
61f769ef06 | ||
|
|
073603ed91 | ||
|
|
6e31404e2c | ||
|
|
63ed087986 | ||
|
|
f3d955e8fd | ||
|
|
a0004f0070 | ||
|
|
edc7f5116f | ||
|
|
5c822de80b | ||
|
|
1e6c5ad1bc | ||
|
|
eb7faea978 | ||
|
|
dfd41fe787 | ||
|
|
6940983478 | ||
|
|
1ba2bcc9e1 | ||
|
|
6dd1734e53 | ||
|
|
723d7d73ab | ||
|
|
1b28499972 | ||
|
|
21a9edd43e | ||
|
|
f8e2ab861c | ||
|
|
cd2ef8cd65 | ||
|
|
8040bb66a6 | ||
|
|
a2b62699e0 | ||
|
|
f88ffd826c | ||
|
|
8a9e01a5c7 | ||
|
|
b42c5fd7e4 | ||
|
|
a4f0d9b322 | ||
|
|
fa3f400fcb | ||
|
|
0bc4582f2c | ||
|
|
6ae92ff7fc | ||
|
|
7d1e546df4 | ||
|
|
2e8e3a6c97 | ||
|
|
028ad8f92a | ||
|
|
3718ea32d1 | ||
|
|
ca476a74bc | ||
|
|
046d249179 | ||
|
|
14155d5d83 | ||
|
|
8f5151df80 | ||
|
|
b004f07a26 | ||
|
|
d6d675f981 | ||
|
|
554ecc1fe9 | ||
|
|
ba337cc04e | ||
|
|
05b70e52ab | ||
|
|
8788b51813 | ||
|
|
9a141bf4e4 | ||
|
|
e72e9302b6 | ||
|
|
55df568fcc | ||
|
|
06742dc875 | ||
|
|
3e71bd3926 | ||
|
|
2fad9abbab | ||
|
|
fb5c9dbb2e | ||
|
|
64042d40f2 | ||
|
|
de782e1042 | ||
|
|
1c4f06af81 | ||
|
|
6412f7df5a | ||
|
|
458252631c | ||
|
|
b77afccf54 | ||
|
|
4f43a81b92 | ||
|
|
d679d74fb5 | ||
|
|
9d93c66fcc | ||
|
|
c33012f137 | ||
|
|
a0caa45004 | ||
|
|
74ea836d10 | ||
|
|
60cdb9124d | ||
|
|
a94785a4bd | ||
|
|
52df4d3f7a | ||
|
|
4803c7c7c8 | ||
|
|
45451f06e7 | ||
|
|
35984f01a9 | ||
|
|
27e049f4ad | ||
|
|
86f21c6989 | ||
|
|
f404dbffe9 | ||
|
|
85f14db21f | ||
|
|
64b65efeff | ||
|
|
0b0bf988ee | ||
|
|
d6e356f80c | ||
|
|
ebb9d20a2a | ||
|
|
7b1c12487c | ||
|
|
2a0b2e45fc | ||
|
|
554c71873a | ||
|
|
97b03d0725 | ||
|
|
6a3ccb627a | ||
|
|
939870a64a | ||
|
|
230d2f6ac9 | ||
|
|
41a3b876b6 | ||
|
|
84f1e75100 | ||
|
|
8bd0c22670 | ||
|
|
591f230bf5 | ||
|
|
099f75d32b | ||
|
|
b239e1d724 | ||
|
|
41a2a331b7 | ||
|
|
e0d10dfe05 |
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true // Allows for the parsing of JSX
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"@typescript-eslint/no-empty-interface": 0,
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"semi": ["error", "always"]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [misyaguziya]
|
||||||
|
patreon: vrct_dev
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: vrct_dev
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
custom: ["https://misyaguziya.booth.pm", "https://vrct-dev.fanbox.cc"]
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
title: "[Bug]: (Please note that the title must be in English.)"
|
||||||
|
assignees:
|
||||||
|
- misyaguziya
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: checkboxes
|
||||||
|
id: debug-tips
|
||||||
|
attributes:
|
||||||
|
label: Debug Tips
|
||||||
|
description: To improve debug efficiency, please read [Issues of README](https://github.com/misyaguziya/VRCT/issues) first.
|
||||||
|
options:
|
||||||
|
- label: I'm sure I've read this project's Issues of README.
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Also tell us, what did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "A bug happened!"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: APP Version
|
||||||
|
description: What version of App are you running?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: runtime-environment
|
||||||
|
attributes:
|
||||||
|
label: Runtime Environment
|
||||||
|
description: What runtime environment(OS) are you running?
|
||||||
|
options:
|
||||||
|
- "Windows 10"
|
||||||
|
- "Windows 11"
|
||||||
|
- "Other"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: region
|
||||||
|
attributes:
|
||||||
|
label: Country/Region
|
||||||
|
description: What Country(Region) are you running?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "Qatar (example)"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste the relevant log output. Error logs are generated in the folder where VRCT.exe is stored.
|
||||||
|
render: sh
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshot
|
||||||
|
attributes:
|
||||||
|
label: "Screenshots"
|
||||||
|
description: If applicable, add screenshots to help explain your problem.
|
||||||
|
value: |
|
||||||
|

|
||||||
|
render: Markdown
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: File a feature request
|
||||||
|
title: "[Feature]: (Please note that the title must be in English)"
|
||||||
|
assignees:
|
||||||
|
- misyaguziya
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request!
|
||||||
|
- type: textarea
|
||||||
|
id: expect-to-happened
|
||||||
|
attributes:
|
||||||
|
label: Expect to happened
|
||||||
|
description: What happened, and what did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "A feature I have expected ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: expected-app-version
|
||||||
|
attributes:
|
||||||
|
label: Expected APP Version
|
||||||
|
description: What version of App did you expect?
|
||||||
|
options:
|
||||||
|
- next newest version
|
||||||
|
- a future version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: expected-runtime-environment
|
||||||
|
attributes:
|
||||||
|
label: Expected Runtime Environment
|
||||||
|
description: What runtime environment(OS) did you expect?
|
||||||
|
options:
|
||||||
|
- "Windows 10"
|
||||||
|
- "Windows 11"
|
||||||
|
- "Other"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: region
|
||||||
|
attributes:
|
||||||
|
label: Country/Region
|
||||||
|
description: What Country(Region) did you expect?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "Qatar(example)"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected-output
|
||||||
|
attributes:
|
||||||
|
label: Expected Output
|
||||||
|
render: sh
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshot
|
||||||
|
attributes:
|
||||||
|
label: "Sharing features in images"
|
||||||
|
value: |
|
||||||
|

|
||||||
|
render: Markdown
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
description: 'プロジェクト解析に基づくREADME自動生成専用モード'
|
||||||
|
tools: ['edit', 'search', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'fetch', 'ms-python.python/getPythonEnvironmentInfo', 'ms-python.python/getPythonExecutableCommand', 'ms-python.python/installPythonPackage', 'ms-python.python/configurePythonEnvironment', 'todos']
|
||||||
|
model: 'Auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
# README自動生成モード ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
あなたは優秀な技術文書ライターです。プロジェクトのコードベースを詳細に分析し、正確で包括的なREADMEファイルを生成することが役割です。
|
||||||
|
|
||||||
|
## 基本方針 ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
### 分析対象
|
||||||
|
- プロジェクトの全ファイル構成
|
||||||
|
- 主要なソースコード(HTML、JavaScript、CSS、Python等)
|
||||||
|
- 設定ファイル(package.json、requirements.txt等)
|
||||||
|
- 既存のドキュメント
|
||||||
|
|
||||||
|
### README構成要件 ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
必須セクション(この順序で記載):
|
||||||
|
1. **プロジェクト概要** - 1-2文でプロジェクトの目的を説明
|
||||||
|
2. **主要機能** - 実装されている機能のリスト
|
||||||
|
3. **使用方法** - エンドユーザー向けの操作手順
|
||||||
|
4. **動作環境** - 必要な環境・依存関係
|
||||||
|
5. **インストール・実行方法** - 開発者向けセットアップ手順
|
||||||
|
6. **ファイル構成** - 主要ファイルの説明
|
||||||
|
7. **制限事項・既知の問題** - 現在できないこと、バグ
|
||||||
|
8. **技術仕様** - 使用技術・ライブラリ
|
||||||
|
9. **ライセンス** - 適用されるライセンス情報
|
||||||
|
|
||||||
|
### 重要な制約事項 ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
1. **事実のみ記載**: コードベースに存在しない機能は記載しない
|
||||||
|
2. **検証可能な情報**: 実際のファイルから確認できる内容のみ
|
||||||
|
3. **ユーザー目線**: エンドユーザーが最初に知りたい情報を優先
|
||||||
|
4. **簡潔で明確**: 冗長な説明は避け、必要な情報を効率的に伝達
|
||||||
|
5. **制限事項の明記**: 実装されていない機能や既知の問題を正直に記載
|
||||||
|
|
||||||
|
### 文体・フォーマット ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
- 敬語は使用せず、簡潔で読みやすい文体
|
||||||
|
- コードブロックやリストを適切に活用
|
||||||
|
- 見出しは階層的に整理
|
||||||
|
- 技術用語は必要に応じて補足説明
|
||||||
|
|
||||||
|
### 分析手順 ※下記コードでは絵文字をカットしています
|
||||||
|
|
||||||
|
1. プロジェクトの全体構造を把握
|
||||||
|
2. メインファイル(index.html、main.js等)の機能を解析
|
||||||
|
3. 設定ファイルから依存関係や環境要件を抽出
|
||||||
|
4. 実際の動作フローを追跡
|
||||||
|
5. 未実装部分や制限事項を特定
|
||||||
|
6. 上記構成要件に従ってREADMEを生成
|
||||||
|
|
||||||
|
コードベースの事実に基づいた、正確で有用なREADMEファイルの生成を最優先に作業してください。
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
name: Release VRCT
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*' # タグが 'v' で始まる場合にトリガー (例: v1.0.0)
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build VRCT
|
||||||
|
runs-on: windows-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22.15.0'
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Setup Python environment
|
||||||
|
run: npm run setup-python
|
||||||
|
|
||||||
|
- name: Install Python script dependencies
|
||||||
|
run: pip install tqdm
|
||||||
|
|
||||||
|
- name: Build and package
|
||||||
|
run: npm run release-all
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: get_version
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$TAG_WITH_V = $env:GITHUB_REF -replace "^refs/tags/", ""
|
||||||
|
$VERSION_NUM = $TAG_WITH_V -replace "^v", ""
|
||||||
|
echo "VERSION=$VERSION_NUM" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: Upload to Hugging Face Hub
|
||||||
|
env:
|
||||||
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
pip install huggingface_hub==0.34.4
|
||||||
|
|
||||||
|
$UPLOAD_DIR = "./hf_upload_temp"
|
||||||
|
New-Item -ItemType Directory -Force -Path $UPLOAD_DIR
|
||||||
|
Copy-Item -Path ./VRCT.zip -Destination "$UPLOAD_DIR/VRCT.zip"
|
||||||
|
Copy-Item -Path ./VRCT_cuda.zip -Destination "$UPLOAD_DIR/VRCT_cuda.zip"
|
||||||
|
|
||||||
|
hf upload --repo-type model --commit-message "👍️[Update] ${{ env.VERSION }} Release" ms-software/VRCT $UPLOAD_DIR
|
||||||
|
|
||||||
|
hf repo tag create ms-software/VRCT ${{ github.ref_name }} --repo-type model --message "Release ${{ github.ref_name }}"
|
||||||
|
|
||||||
|
Remove-Item -Recurse -Force $UPLOAD_DIR
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ env.VERSION }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset (NSIS Installer)
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./src-tauri/target/release/bundle/nsis/VRCT_${{ env.VERSION }}_x64-setup.exe
|
||||||
|
asset_name: VRCT_${{ env.VERSION }}_x64-setup.exe
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
tmp/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
/config.json
|
||||||
|
memo.txt
|
||||||
|
*.pyc
|
||||||
|
logs/
|
||||||
|
.venv/
|
||||||
|
.venv_cuda/
|
||||||
|
weights/
|
||||||
|
.vscode
|
||||||
|
error.log
|
||||||
|
*.exe
|
||||||
|
*.ipynb
|
||||||
|
VRCT.zip
|
||||||
|
VRCT_cuda.zip
|
||||||
|
|
||||||
|
.qoder/
|
||||||
|
|
||||||
|
# Added by WebUI migration
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
.vscode
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Customize
|
||||||
|
/build
|
||||||
|
error.txt
|
||||||
|
config.json
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 misyaguziya
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
call .venv/Scripts/activate
|
||||||
|
pyinstaller spec/backend.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
call .venv_cuda/Scripts/activate
|
||||||
|
pyinstaller spec/backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
REM .venv exists
|
||||||
|
if exist .venv (
|
||||||
|
rmdir /s /q .venv
|
||||||
|
)
|
||||||
|
|
||||||
|
REM make .venv
|
||||||
|
python -m venv .venv
|
||||||
|
|
||||||
|
REM install packages for .venv
|
||||||
|
call .venv/Scripts/activate
|
||||||
|
python.exe -m pip install --upgrade pip
|
||||||
|
pip install --no-cache-dir --force-reinstall -r requirements.txt
|
||||||
|
|
||||||
|
REM if .venv_cuda exists
|
||||||
|
if exist .venv_cuda (
|
||||||
|
rmdir /s /q .venv_cuda
|
||||||
|
)
|
||||||
|
|
||||||
|
REM make .venv_cuda
|
||||||
|
python -m venv .venv_cuda
|
||||||
|
|
||||||
|
REM install packages for .venv_cuda
|
||||||
|
call .venv_cuda/Scripts/activate
|
||||||
|
python.exe -m pip install --upgrade pip
|
||||||
|
pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 182 B |
|
After Width: | Height: | Size: 186 B |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1,422 @@
|
|||||||
|
# VRCTビルドガイド
|
||||||
|
|
||||||
|
このドキュメントでは、VRCTプロジェクトのビルド方法について説明します。
|
||||||
|
|
||||||
|
## 目次
|
||||||
|
|
||||||
|
- [必要な環境](#必要な環境)
|
||||||
|
- [初回セットアップ](#初回セットアップ)
|
||||||
|
- [ビルドの種類](#ビルドの種類)
|
||||||
|
- [開発ビルド](#開発ビルド)
|
||||||
|
- [リリースビルド](#リリースビルド)
|
||||||
|
- [ビルドプロセスの詳細](#ビルドプロセスの詳細)
|
||||||
|
- [トラブルシューティング](#トラブルシューティング)
|
||||||
|
|
||||||
|
## 必要な環境
|
||||||
|
|
||||||
|
### 必須ソフトウェア
|
||||||
|
|
||||||
|
- **Node.js** (npm含む)
|
||||||
|
- **Python 3.x**
|
||||||
|
- **Rust** (Tauri用)
|
||||||
|
- **Git**
|
||||||
|
|
||||||
|
### 推奨環境
|
||||||
|
|
||||||
|
- Windows 10/11
|
||||||
|
- メモリ: 8GB以上
|
||||||
|
- ストレージ: 5GB以上の空き容量
|
||||||
|
|
||||||
|
## 初回セットアップ
|
||||||
|
|
||||||
|
### 1. リポジトリのクローン
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd VRCT
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Node.js依存関係のインストール
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Python環境のセットアップ
|
||||||
|
|
||||||
|
以下のコマンドで、CPU版とCUDA版の両方の仮想環境を作成します:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run setup-python
|
||||||
|
```
|
||||||
|
|
||||||
|
このコマンドは以下の処理を実行します:
|
||||||
|
- `.venv` (CPU版) の作成と依存関係のインストール
|
||||||
|
- `.venv_cuda` (CUDA版) の作成と依存関係のインストール
|
||||||
|
|
||||||
|
> **注意**: CUDA版を使用する場合は、NVIDIAのGPUとCUDA Toolkit 12.8が必要です。
|
||||||
|
|
||||||
|
## ビルドの種類
|
||||||
|
|
||||||
|
VRCTでは、以下の2種類のビルドが可能です:
|
||||||
|
|
||||||
|
### CPU版
|
||||||
|
標準的なCPUで動作するバージョン。GPUは不要。
|
||||||
|
|
||||||
|
### CUDA版
|
||||||
|
NVIDIA GPUを活用した高速処理版。CUDA対応GPUが必要。
|
||||||
|
|
||||||
|
## 開発ビルド
|
||||||
|
|
||||||
|
開発中にアプリケーションを実行・テストするためのビルドです。
|
||||||
|
|
||||||
|
### CPU版の開発ビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
このコマンドは以下を実行します:
|
||||||
|
1. 実行中のプロセスを終了 (`task-kill`)
|
||||||
|
2. ビルドファイルのクリーンアップ (`clean`)
|
||||||
|
3. バージョン情報の更新 (`update-version`)
|
||||||
|
4. Pythonバックエンドのビルド (`build-python`)
|
||||||
|
5. ViteとTauriの開発サーバー起動
|
||||||
|
|
||||||
|
### CUDA版の開発ビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev-cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
CPU版と同様ですが、CUDA対応のPythonバックエンドをビルドします。
|
||||||
|
|
||||||
|
### UIのみの開発
|
||||||
|
|
||||||
|
バックエンドのビルドをスキップして、UIのみを開発する場合:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## リリースビルド
|
||||||
|
|
||||||
|
配布用のインストーラーを作成するビルドです。
|
||||||
|
|
||||||
|
### CPU版のリリースビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
または、ZIP形式でパッケージング:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
生成されるファイル:
|
||||||
|
- インストーラー: `src-tauri/target/release/bundle/nsis/`
|
||||||
|
- ZIPファイル: `VRCT.zip` (releaseコマンド使用時)
|
||||||
|
|
||||||
|
### CUDA版のリリースビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build-cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
または、ZIP形式でパッケージング:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release-cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
生成されるファイル:
|
||||||
|
- インストーラー: `src-tauri/target/release/bundle/nsis/`
|
||||||
|
- ZIPファイル: `VRCT_cuda.zip` (release-cudaコマンド使用時)
|
||||||
|
|
||||||
|
### 両バージョンの同時ビルド
|
||||||
|
|
||||||
|
CPU版とCUDA版の両方をビルドする場合:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## ビルドプロセスの詳細
|
||||||
|
|
||||||
|
### バージョン管理
|
||||||
|
|
||||||
|
バージョンは `package.json` で一元管理され、以下のファイルに自動で同期されます:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run update-version
|
||||||
|
```
|
||||||
|
|
||||||
|
更新されるファイル:
|
||||||
|
- `src-tauri/tauri.conf.json`
|
||||||
|
- `src-python/config.py`
|
||||||
|
|
||||||
|
### どこにバージョンを設定すればReleaseに反映されるか
|
||||||
|
|
||||||
|
- **設定箇所**: `package.json` の `version` が唯一のソース・オブ・トゥルース。
|
||||||
|
- **反映方法**: `npm run update-version`(`build`/`build-cuda`/`release`コマンド内でも自動実行)により、
|
||||||
|
- `src-tauri/tauri.conf.json` の `version` に同期(Tauri/NSISインストーラーの表示・メタデータに使用)
|
||||||
|
- `src-python/config.py` の `self._VERSION` に同期(ランタイム表示等に使用)
|
||||||
|
- **成果物への影響**:
|
||||||
|
- インストーラー(NSIS)は `tauri.conf.json` の `version` を取り込み、プロダクトバージョンとして反映。
|
||||||
|
- ZIPパッケージ名はスクリプト既定では固定(`VRCT.zip`/`VRCT_cuda.zip`)。ファイル名にバージョンを含めたい場合は、`package.json` の `release` スクリプトを調整してください。
|
||||||
|
|
||||||
|
### Pythonバックエンドのビルド
|
||||||
|
|
||||||
|
#### CPU版
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build-python
|
||||||
|
```
|
||||||
|
|
||||||
|
実行内容:
|
||||||
|
|
||||||
|
- `.venv` 環境をアクティベート
|
||||||
|
- PyInstallerで `spec/backend.spec` を使用してビルド
|
||||||
|
- 出力先: `src-tauri/bin/`
|
||||||
|
|
||||||
|
#### CUDA版
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build-python-cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
実行内容:
|
||||||
|
|
||||||
|
- `.venv_cuda` 環境をアクティベート
|
||||||
|
- PyInstallerで `spec/backend_cuda.spec` を使用してビルド
|
||||||
|
- 出力先: `src-tauri/bin/`
|
||||||
|
|
||||||
|
### フロントエンドのビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run vite-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Viteを使用してフロントエンド(React)をビルドし、`dist/` ディレクトリに出力します。
|
||||||
|
|
||||||
|
### Tauriアプリケーションのビルド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
Tauriを使用して最終的なデスクトップアプリケーションをビルドします。
|
||||||
|
|
||||||
|
## GitHub ActionsでのRelease自動化
|
||||||
|
|
||||||
|
Windows用のReleaseをGitHub Actionsで自動生成・公開する例です。`package.json` のバージョンをタグ・リリース名に使い、TauriのNSISインストーラーとZIPを添付します。
|
||||||
|
|
||||||
|
### 推奨トリガー
|
||||||
|
|
||||||
|
- タグプッシュ(例: `v*`)または手動実行(`workflow_dispatch`)
|
||||||
|
|
||||||
|
### サンプルワークフロー(Windows)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Release (Windows)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Setup Python envs (.venv/.venv_cuda)
|
||||||
|
run: |
|
||||||
|
npm run setup-python
|
||||||
|
|
||||||
|
- name: Sync versions from package.json
|
||||||
|
run: |
|
||||||
|
npm run update-version
|
||||||
|
|
||||||
|
- name: Build (CPU)
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Package ZIP (CPU)
|
||||||
|
run: |
|
||||||
|
python utils/zip.py --zip_name VRCT.zip
|
||||||
|
|
||||||
|
- name: Read version from package.json
|
||||||
|
id: pkg
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$version = (Get-Content package.json | ConvertFrom-Json).version
|
||||||
|
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: VRCT-windows-${{ steps.pkg.outputs.version }}
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/nsis/**/*
|
||||||
|
VRCT.zip
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ steps.pkg.outputs.version }}
|
||||||
|
name: VRCT v${{ steps.pkg.outputs.version }}
|
||||||
|
files: |
|
||||||
|
src-tauri/target/release/bundle/nsis/**/*
|
||||||
|
VRCT.zip
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ポイント
|
||||||
|
- ビルド前に必ず `npm run update-version` を実行して、`tauri.conf.json` と `config.py` にバージョンを同期します。
|
||||||
|
- アーティファクトのパスは既定構成に合わせています:
|
||||||
|
- インストーラー: `src-tauri/target/release/bundle/nsis/`
|
||||||
|
- ZIP: ルート直下の `VRCT.zip`
|
||||||
|
- CUDA版も同様にビルドする場合は、`npm run build-cuda` と `python utils/zip.py --zip_name VRCT_cuda.zip` を追加して、別アーティファクト名でアップロード・添付してください。
|
||||||
|
|
||||||
|
## ユーティリティコマンド
|
||||||
|
|
||||||
|
### クリーンアップ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run clean
|
||||||
|
```
|
||||||
|
|
||||||
|
以下のディレクトリを削除します:
|
||||||
|
- `build/`
|
||||||
|
- `dist/`
|
||||||
|
- `src-tauri/bin/`
|
||||||
|
- `src-tauri/target/`
|
||||||
|
|
||||||
|
### プロセスの強制終了
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run task-kill
|
||||||
|
```
|
||||||
|
|
||||||
|
VRCTに関連する実行中のプロセスを終了します。
|
||||||
|
|
||||||
|
## ディレクトリ構成
|
||||||
|
|
||||||
|
```
|
||||||
|
VRCT/
|
||||||
|
├── bat/ # バッチスクリプト
|
||||||
|
│ ├── build.bat # CPU版Pythonビルド
|
||||||
|
│ ├── build_cuda.bat # CUDA版Pythonビルド
|
||||||
|
│ └── install.bat # Python環境セットアップ
|
||||||
|
├── spec/ # PyInstallerスペックファイル
|
||||||
|
│ ├── backend.spec # CPU版ビルド設定
|
||||||
|
│ └── backend_cuda.spec # CUDA版ビルド設定
|
||||||
|
├── src-python/ # Pythonバックエンドソースコード
|
||||||
|
├── src-tauri/ # Tauriアプリケーション設定
|
||||||
|
│ ├── bin/ # ビルド済みPythonバイナリ(生成)
|
||||||
|
│ └── target/ # Tauriビルド出力(生成)
|
||||||
|
├── src-ui/ # Reactフロントエンドソースコード
|
||||||
|
├── utils/ # ユーティリティスクリプト
|
||||||
|
│ ├── clean.py # クリーンアップスクリプト
|
||||||
|
│ ├── task_kill.py # プロセス終了スクリプト
|
||||||
|
│ ├── update_version.py # バージョン更新スクリプト
|
||||||
|
│ └── zip.py # ZIPパッケージング
|
||||||
|
├── package.json # Node.js設定とバージョン管理
|
||||||
|
├── requirements.txt # Python依存関係(CPU版)
|
||||||
|
└── requirements_cuda.txt # Python依存関係(CUDA版)
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### Python環境のエラー
|
||||||
|
|
||||||
|
仮想環境を再作成してください:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run setup-python
|
||||||
|
```
|
||||||
|
|
||||||
|
### ビルドが失敗する
|
||||||
|
|
||||||
|
1. クリーンアップを実行:
|
||||||
|
```bash
|
||||||
|
npm run clean
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Node.js依存関係を再インストール:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 再度ビルド:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### CUDA版が動作しない
|
||||||
|
|
||||||
|
- CUDA Toolkit 12.8がインストールされているか確認
|
||||||
|
- NVIDIA GPUドライバーが最新か確認
|
||||||
|
- `requirements_cuda.txt` の依存関係が正しくインストールされているか確認
|
||||||
|
|
||||||
|
### プロセスが残っている
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run task-kill
|
||||||
|
```
|
||||||
|
|
||||||
|
を実行して、すべてのVRCTプロセスを終了してください。
|
||||||
|
|
||||||
|
## 参考情報
|
||||||
|
|
||||||
|
### PyInstallerスペックファイル
|
||||||
|
|
||||||
|
- `spec/backend.spec` - CPU版の設定
|
||||||
|
- `spec/backend_cuda.spec` - CUDA版の設定
|
||||||
|
|
||||||
|
これらのファイルでは、以下を設定しています:
|
||||||
|
- エントリーポイント: `src-python/mainloop.py`
|
||||||
|
- データファイル(フォント、プロンプト、言語ファイル等)のパス
|
||||||
|
- 依存ライブラリのパス
|
||||||
|
|
||||||
|
### バージョン管理フロー
|
||||||
|
|
||||||
|
1. `package.json` のバージョンを更新
|
||||||
|
2. `npm run update-version` を実行
|
||||||
|
3. 自動的に `tauri.conf.json` と `config.py` が更新される
|
||||||
|
|
||||||
|
### リリースパッケージの内容
|
||||||
|
|
||||||
|
ZIPファイルには以下が含まれます:
|
||||||
|
- `VRCT.exe` - メインアプリケーション
|
||||||
|
- `VRCT-sidecar.exe` - Pythonバックエンド
|
||||||
|
- `_internal/` - 必要な依存ファイル
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
プロジェクトのライセンスについては、`LICENSE` ファイルを参照してください。
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
|
||||||
|
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
|
||||||
|
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
[](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
[](https://github.com/sponsors/misyaguziya)
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
Become a VRCT Supporter on:
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<a href="https://vrct-dev.fanbox.cc">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
|
||||||
|
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<a href="https://patreon.com/vrct_dev">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
|
||||||
|
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
|
||||||
|
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
| **English** | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
VRCT is software that supports VRChat conversations with translation and transcription.
|
||||||
|
</h3>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
# Download & Install
|
||||||
|
Download from anywhere you like.
|
||||||
|
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
|
||||||
|
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
|
||||||
|
Just download and run the exe.
|
||||||
|
|
||||||
|
# What is VRCT?
|
||||||
|
VRCT is software that supports conversations between people who speak different languages by providing chat or voice translation.
|
||||||
|
These features are designed for use within VRChat.
|
||||||
|
*Although not supported, it is also used for other purposes such as watching movies.
|
||||||
|
|
||||||
|
VRCT supports your conversations with
|
||||||
|
- 💬 **Send chat to VRChat**
|
||||||
|
- 🌐 **Translation**
|
||||||
|
- 🎙 **Transcription of audio from microphone**
|
||||||
|
- 🔈 **Transcription of audio from Speaker**
|
||||||
|
|
||||||
|
# Documents
|
||||||
|
Initial setup, basic functions, and other features are also described.
|
||||||
|
- [Documents Link](https://misyaguziya.github.io/VRCT-Docs/)
|
||||||
|
|
||||||
|
# How to Use (YouTube)
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=rUTad037n8Q)
|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
## Author
|
||||||
|
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development)
|
||||||
|
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support)
|
||||||
|
- [レラ](https://github.com/soumt-r) (Technical Advisor)
|
||||||
|
- [どね](https://twitter.com/done_vrc) (Logo Design)
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
VRCT collects anonymous telemetry data via [Aptabase](https://aptabase.com) to help improve the app. The collected data includes app starts, session duration, and feature usage. No personally identifiable information is collected.
|
||||||
|
|
||||||
|
You can opt out of telemetry in the app settings at any time. See the [Aptabase Privacy Policy](https://aptabase.com/legal/privacy) for more details.
|
||||||
|
|
||||||
|
## Thanks to our contributors
|
||||||
|
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
|
||||||
|
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
|
||||||
|
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
|
||||||
|
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
[](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
[](https://github.com/sponsors/misyaguziya)
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
Become a VRCT Supporter on:
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<a href="https://vrct-dev.fanbox.cc">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
|
||||||
|
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<a href="https://patreon.com/vrct_dev">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
|
||||||
|
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
|
||||||
|
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
| [English](/docs/readmes/README.en.md) | **日本語** | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
VRCTは翻訳や文字起こしでVRChatの会話をサポートするソフトウェアです。
|
||||||
|
</h3>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
# ダウンロード&インストール
|
||||||
|
好きな場所からダウンロードしてください。
|
||||||
|
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
|
||||||
|
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
|
||||||
|
ダウンロードしてexeを起動するだけです。
|
||||||
|
|
||||||
|
# VRCTってなに?
|
||||||
|
VRCTは話す言語の異なる人同士が会話を行うためにチャットもしくは音声の翻訳を行うことで会話をサポートするソフトウェアです。
|
||||||
|
これらの機能はVRChat内で使用するために設計されています。
|
||||||
|
※サポート対象外ですがその他の用途として映画鑑賞等でも使用されています。
|
||||||
|
|
||||||
|
VRCTはあなたの会話を以下でサポートをします。
|
||||||
|
- 💬 **VRChatへのチャット送信機能**
|
||||||
|
- 🌐 **翻訳機能**
|
||||||
|
- 🎙 **マイクの文字起こし機能**
|
||||||
|
- 🔈 **スピーカーの文字起こし機能**
|
||||||
|
|
||||||
|
# ドキュメント
|
||||||
|
初期設定や基本機能、その他の機能についても記載してあります。
|
||||||
|
- [Documents Link](https://misyaguziya.github.io/VRCT-Docs/)
|
||||||
|
|
||||||
|
# 使い方(Youtube)
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=rUTad037n8Q)
|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
## Author
|
||||||
|
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (メイン開発)
|
||||||
|
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI多言語対応)
|
||||||
|
- [レラ](https://github.com/soumt-r) (テクニカルサポート)
|
||||||
|
- [どね](https://twitter.com/done_vrc) (ロゴデザイン)
|
||||||
|
|
||||||
|
## テレメトリー(利用統計情報)
|
||||||
|
|
||||||
|
VRCTは[Aptabase](https://aptabase.com)を通じて、アプリの改善のために匿名のテレメトリーデータを収集しています。収集されるデータは、起動回数、起動時間、使用機能です。個人を特定できる情報は一切収集されません。
|
||||||
|
|
||||||
|
テレメトリーはアプリの設定からいつでも無効化できます。詳細は[Aptabaseプライバシーポリシー](https://aptabase.com/legal/privacy)をご確認ください。
|
||||||
|
|
||||||
|
## Thanks to our contributors
|
||||||
|
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
|
||||||
|
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
VRCT は VRChat によって承認されておらず、VRChat または VRChat の開発もしくは管理に公式に関与する者の見解や意見が反映されたものではありません。VRChat および関連するすべての財産は 米国VRChat, Incの商標または登録商標です。
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
|
||||||
|
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
|
||||||
|
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
[](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
[](https://github.com/sponsors/misyaguziya)
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
Become a VRCT Supporter on:
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<a href="https://vrct-dev.fanbox.cc">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
|
||||||
|
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<a href="https://patreon.com/vrct_dev">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
|
||||||
|
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
|
||||||
|
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | **한국어** | [繁體中文](/docs/readmes/README.zh-Hant.md) |
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
VRCT는 음성인식 및 번역 기능을 통해 VRChat의 대화를 지원하는 소프트웨어입니다.
|
||||||
|
</h3>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
# 다운로드 및 설치
|
||||||
|
원하는 곳에서 다운로드 할 수 있어요.
|
||||||
|
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
|
||||||
|
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
|
||||||
|
다운로드 후 exe를 실행하기만 하면 됩니다.
|
||||||
|
|
||||||
|
# VRCT가 뭔가요?
|
||||||
|
VRCT는 서로 다른 언어를 사용하는 사람들끼리 대화를 할 수 있도록 채팅이나 음성 번역을 통해 대화를 지원하는 소프트웨어에요.
|
||||||
|
이 기능들은 VRChat 내에서 사용하도록 설계되었어요.
|
||||||
|
※ 지원 대상에서 제외되지만, 영화 감상 등 다른 용도로도 사용되고 있습니다.
|
||||||
|
|
||||||
|
VRCT는 다음과 같이 당신의 대화를 도와드려요.
|
||||||
|
- 💬 **VRChat으로의 채팅 전송 기능**
|
||||||
|
- 🌐 **번역 기능**
|
||||||
|
- 🎙 **마이크 음성인식 기능**
|
||||||
|
- 🔈 **스피커 음성인식 기능**
|
||||||
|
|
||||||
|
# 문서 (일본어)
|
||||||
|
초기 설정과 기본 기능 및 기타 기능에 대해 설명되어 있어요.
|
||||||
|
- [Documents Link](https://misyaguziya.github.io/VRCT-Docs/)
|
||||||
|
|
||||||
|
# 사용법 (Youtube)
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=rUTad037n8Q)
|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
## Author
|
||||||
|
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (주요 개발)
|
||||||
|
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 다국어 지원)
|
||||||
|
- [レラ](https://github.com/soumt-r) (기술 지원)
|
||||||
|
- [どね](https://twitter.com/done_vrc) (로고 디자인)
|
||||||
|
|
||||||
|
## 텔레메트리 (사용 통계)
|
||||||
|
|
||||||
|
VRCT는 [Aptabase](https://aptabase.com)를 통해 앱 개선을 위한 익명 텔레메트리 데이터를 수집합니다. 수집되는 데이터는 앱 시작 횟수, 세션 시간, 기능 사용입니다. 개인 식별 정보는 수집되지 않습니다.
|
||||||
|
|
||||||
|
앱 설정에서 언제든지 텔레메트리를 비활성화할 수 있습니다. 자세한 내용은 [Aptabase 개인정보 처리방침](https://aptabase.com/legal/privacy)을 참조하세요.
|
||||||
|
|
||||||
|
## Thanks to our contributors
|
||||||
|
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
|
||||||
|
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
VRCT는 VRChat의 어떠한 승인도 받지 않았으며, VRChat 또는 VRChat의 개발 또는 관리에 공식적으로 관여하는 사람의 견해나 의견을 반영하지 않습니다. VRChat 및 모든 관련 재산은 미국 VRChat, Inc의 상표 또는 등록상표입니다.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
|
||||||
|
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
|
||||||
|
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/releases)
|
||||||
|
[](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
|
||||||
|
[](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
[](https://github.com/sponsors/misyaguziya)
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
Become a VRCT Supporter on:
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<a href="https://vrct-dev.fanbox.cc">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
|
||||||
|
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
|
||||||
|
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<a href="https://patreon.com/vrct_dev">
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
|
||||||
|
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
|
||||||
|
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
|
||||||
|
</picture>
|
||||||
|
</a> 
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | **繁體中文** |
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
VRCT 是一個支援 VRChat 對話翻譯和紀錄的軟體。
|
||||||
|
</h3>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
# 下載 & 安裝
|
||||||
|
你可以從這些地方下載 VRCT:
|
||||||
|
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
|
||||||
|
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
|
||||||
|
|
||||||
|
你只需要下載並啟動 exe 文件。
|
||||||
|
|
||||||
|
# 什麼是 VRCT?
|
||||||
|
VRCT 是一種透過提供聊天或語音翻譯來幫助語言不通的人對話的軟體。
|
||||||
|
這些功能專為在 VRChat 中使用而設計,但你也可以拿來看電影。
|
||||||
|
|
||||||
|
VRCT 可以:
|
||||||
|
- 💬 **傳送訊息至遊戲內 Chatbox**
|
||||||
|
- 🌐 **自動翻譯**
|
||||||
|
- 🎙 **麥克風轉文字**
|
||||||
|
- 🔈 **喇叭轉文字**
|
||||||
|
|
||||||
|
# 文件
|
||||||
|
解釋了初始設定、基本功能以及其他功能。
|
||||||
|
- [Documents Link](https://misyaguziya.github.io/VRCT-Docs/)
|
||||||
|
|
||||||
|
# YouTube 教學(日語、英文字幕)
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=rUTad037n8Q)
|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
|
||||||
|
## 作者
|
||||||
|
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (主要開發)
|
||||||
|
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 多語系支援)
|
||||||
|
- [レラ](https://github.com/soumt-r) (技術支援)
|
||||||
|
- [どね](https://twitter.com/done_vrc) (Logo 設計)
|
||||||
|
|
||||||
|
## 遙測(使用統計)
|
||||||
|
|
||||||
|
VRCT 透過 [Aptabase](https://aptabase.com) 收集匿名遙測資料以協助改善應用程式。收集的資料包括應用程式啟動次數、使用時長、功能使用。不會收集任何可識別個人身份的資訊。
|
||||||
|
|
||||||
|
您可以隨時在應用程式設定中停用遙測。詳情請參閱 [Aptabase 隱私權政策](https://aptabase.com/legal/privacy)。
|
||||||
|
|
||||||
|
## Thanks to our contributors
|
||||||
|
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
|
||||||
|
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
VRCT 未得到 VRChat 的認可,也不反映 VRChat 或正式參與製作或管理 VRChat 財產的任何人的觀點或意見。VRChat 和所有相關財產均為 VRChat Inc. 的商標或註冊商標。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="./assets/chato_icon_fill.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>VRCT</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src-ui/views/app/index.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import yaml from "js-yaml";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import en_yml from "./en.yml?raw";
|
||||||
|
import ja_yml from "./ja.yml?raw";
|
||||||
|
import ko_yml from "./ko.yml?raw";
|
||||||
|
import zh_hant_yml from "./zh-Hant.yml?raw";
|
||||||
|
import zh_hans_yml from "./zh-Hans.yml?raw";
|
||||||
|
|
||||||
|
const translation_en = yaml.load(en_yml);
|
||||||
|
const translation_ja = yaml.load(ja_yml);
|
||||||
|
const translation_ko = yaml.load(ko_yml);
|
||||||
|
const translation_zh_Hant = yaml.load(zh_hant_yml);
|
||||||
|
const translation_zh_Hans = yaml.load(zh_hans_yml);
|
||||||
|
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: { translation: translation_en },
|
||||||
|
ja: { translation: translation_ja },
|
||||||
|
ko: { translation: translation_ko },
|
||||||
|
"zh-Hant": { translation: translation_zh_Hant },
|
||||||
|
"zh-Hans": { translation: translation_zh_Hans },
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
lng: "en",
|
||||||
|
fallbackLng: "en",
|
||||||
|
// debug: true,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // react already safes from xss
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# =================================
|
||||||
|
# IMPORTANT:
|
||||||
|
# Please read 'readme_first.txt' before making any changes.
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
common:
|
||||||
|
go_back_button_label: "Go Back"
|
||||||
|
|
||||||
|
common_error:
|
||||||
|
no_device_mic: "No mic device detected."
|
||||||
|
no_device_speaker: "No speaker device detected."
|
||||||
|
threshold_invalid_value: "You can set it with a value between {{min}} to {{max}}."
|
||||||
|
failed_download_weight_ctranslate2: "CTranslate2 weight download error."
|
||||||
|
failed_download_weight_whisper: "Whisper weight download error."
|
||||||
|
translation_limit: "Translation engine limit reached or temporarily restricted."
|
||||||
|
deepl_auth_key_invalid_length: "DeepL auth key length is not correct."
|
||||||
|
deepl_auth_key_failed_authentication: "Auth key is incorrect or API usage limit reached."
|
||||||
|
|
||||||
|
invalid_value_mic_record_timeout: "It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more."
|
||||||
|
invalid_value_mic_phrase_timeout: "It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more."
|
||||||
|
invalid_value_mic_max_phrase: "You can set a number equal to or greater than 0."
|
||||||
|
|
||||||
|
invalid_value_speaker_record_timeout: "It cannot be greater than '{{speaker_phrase_timeout_label}}' with a value of 0 or more."
|
||||||
|
invalid_value_speaker_phrase_timeout: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more."
|
||||||
|
invalid_value_speaker_max_phrase: "You can set a number equal to or greater than 0."
|
||||||
|
|
||||||
|
common_warning:
|
||||||
|
unable_to_use_osc_query: "The following functions have been automatically disabled due to the OSC IP Address settings preventing OSC data from being received."
|
||||||
|
|
||||||
|
main_page:
|
||||||
|
translation: "Translation"
|
||||||
|
transcription_send: "Voice2Chatbox"
|
||||||
|
transcription_receive: "Speaker2Log"
|
||||||
|
foreground: "Set To Stay On Top"
|
||||||
|
|
||||||
|
language_settings: "Language Settings"
|
||||||
|
your_language: "Your Language"
|
||||||
|
translate_each_other_label: "Translate Both Languages"
|
||||||
|
swap_button_label: "Switch Languages"
|
||||||
|
target_language: "Target Language"
|
||||||
|
translator: "Translator"
|
||||||
|
translator_label_default: "Default"
|
||||||
|
|
||||||
|
translator_selector:
|
||||||
|
is_selected_same_language: "You are selecting the same language for '{{your_language}}' and '{{target_language}}' so only '{{ctranslate2}}' is available."
|
||||||
|
|
||||||
|
message_log:
|
||||||
|
all: "All"
|
||||||
|
sent: "Sent"
|
||||||
|
received: "Received"
|
||||||
|
system: "System"
|
||||||
|
|
||||||
|
resend_button_on_hover_desc: "Press And Hold To Send"
|
||||||
|
|
||||||
|
state_text_enabled: "Enabled"
|
||||||
|
state_text_disabled: "Disabled"
|
||||||
|
|
||||||
|
language_selector:
|
||||||
|
title_your_language: "Select Your Language"
|
||||||
|
title_target_language: "Select Target Language"
|
||||||
|
|
||||||
|
update_available: "New version is ready!"
|
||||||
|
updating: "Now updating..."
|
||||||
|
|
||||||
|
update_modal:
|
||||||
|
cpu_desc: "Use CPU only as the compute device."
|
||||||
|
cuda_desc: "Selectable between CPU and NVIDIA GPUs as compute devices."
|
||||||
|
cuda_compare_cpu_desc: "GPUs offer faster processing than CPUs."
|
||||||
|
cuda_disk_space_desc: "Requires approximately {{size}} of disk space."
|
||||||
|
close_modal: "Close"
|
||||||
|
download_latest_and_restart: "The latest version will be downloaded,\nand the app will automatically restart."
|
||||||
|
is_latest_version_already: "Already using the latest version"
|
||||||
|
is_current_compute_device: "The version currently in use"
|
||||||
|
|
||||||
|
config_page:
|
||||||
|
common:
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
label_type: "Processing Type"
|
||||||
|
type_template_auto: "Automatic"
|
||||||
|
type_template_low: "{{type_name}} (Lower accuracy, faster processing)"
|
||||||
|
type_template_high: "{{type_name}} (Higher accuracy, slower processing)"
|
||||||
|
warning_labels:
|
||||||
|
unable_to_use_osc_query: "Due to the OSC IP Address settings, OSC data cannot be received, so this feature is currently unavailable."
|
||||||
|
|
||||||
|
side_menu_labels:
|
||||||
|
device: "Device"
|
||||||
|
appearance: "Appearance"
|
||||||
|
translation: "Translation"
|
||||||
|
transcription: "Transcription"
|
||||||
|
others: "Other"
|
||||||
|
hotkeys: "Hotkeys"
|
||||||
|
plugins: "Plugins"
|
||||||
|
advanced_settings: "Advanced Settings"
|
||||||
|
|
||||||
|
device:
|
||||||
|
check_volume: "Check Volume"
|
||||||
|
label_auto_select: "Auto Select"
|
||||||
|
label_host: "Host/Driver"
|
||||||
|
label_device: "Device"
|
||||||
|
mic_host_device:
|
||||||
|
label: "Mic Device"
|
||||||
|
mic_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "Mic Sensitivity Settings (Current Setting: Automatic)"
|
||||||
|
desc_for_automatic: "Automatically control mic input sensitivity."
|
||||||
|
label_for_manual: "Mic Sensitivity Settings (Current Setting: Manual)"
|
||||||
|
desc_for_manual: "Input sensitivity can be manually adjusted using the slider. Click the mic icon to test your voice input and adjust the level while monitoring the volume."
|
||||||
|
speaker_device:
|
||||||
|
label: "Speaker Device"
|
||||||
|
speaker_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "Speaker Input Sensitivity Adjustment (Current Setting: Automatic)"
|
||||||
|
desc_for_automatic: "Automatically control speaker input sensitivity."
|
||||||
|
label_for_manual: "Speaker Input Sensitivity Adjustment (Current Setting: Manual)"
|
||||||
|
desc_for_manual: "Input sensitivity can be manually adjusted using the slider. Click the headphone icon to listen to the audio and adjust the level while checking the volume."
|
||||||
|
|
||||||
|
appearance:
|
||||||
|
transparency:
|
||||||
|
label: "Transparency"
|
||||||
|
desc: "Change the main window's transparency."
|
||||||
|
ui_size:
|
||||||
|
label: "UI Size"
|
||||||
|
textbox_ui_size:
|
||||||
|
label: "Message Logs Font Size"
|
||||||
|
desc: "You can adjust the log font size by changing the scaling factor relative to the UI size."
|
||||||
|
send_message_button_type:
|
||||||
|
label: "Send Message Button"
|
||||||
|
hide: "Hide (Use Enter key to send)"
|
||||||
|
show: "Show"
|
||||||
|
show_and_disable_enter_key: "Show and disable sending using the Enter key."
|
||||||
|
show_resend_button:
|
||||||
|
label: "Show Resend Button"
|
||||||
|
desc: "When hovering over a sent message log, the resend button appears. Click to edit, long press to resend."
|
||||||
|
font_family:
|
||||||
|
label: "Font Family"
|
||||||
|
ui_language:
|
||||||
|
label: "UI Language"
|
||||||
|
|
||||||
|
translation:
|
||||||
|
ctranslate2_weight_type:
|
||||||
|
label: "{{ctranslate2}} Model"
|
||||||
|
desc: "You can choose the translation model when using the {{ctranslate2}} translation engine."
|
||||||
|
small: "Basic Model ({{capacity}})"
|
||||||
|
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."
|
||||||
|
open_auth_key_webpage: "Open DeepL Account Webpage"
|
||||||
|
save: "Save"
|
||||||
|
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"
|
||||||
|
section_label_transcription_engines: "Transcription Engines"
|
||||||
|
mic_record_timeout:
|
||||||
|
label: "Mic Record Timeout"
|
||||||
|
desc: "Detects silence and, when the specified number of seconds passes, the system considers the voice input to have ended. (Second(s))"
|
||||||
|
mic_phrase_timeout:
|
||||||
|
label: "Mic Phrase Timeout"
|
||||||
|
desc: "Transcription processing is performed at intervals of the specified number of seconds."
|
||||||
|
mic_max_phrase:
|
||||||
|
label: "Mic Max Words"
|
||||||
|
desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs and send to VRChat."
|
||||||
|
mic_word_filter:
|
||||||
|
label: "Mic Word Filter"
|
||||||
|
desc: "If a registered word is detected, the message will not be sent. To add multiple words at once, separate them with ',' (comma).\n*Duplicate words will not be registered."
|
||||||
|
add_button_label: "Add"
|
||||||
|
count_desc: "Words Currently Registered: {{count}}"
|
||||||
|
speaker_record_timeout:
|
||||||
|
label: "Speaker Record Timeout"
|
||||||
|
desc: "Detects silence and, when the specified number of seconds has passed, considers the speaker input to have ended. (Second(s))"
|
||||||
|
speaker_phrase_timeout:
|
||||||
|
label: "Speaker Phrase Timeout"
|
||||||
|
desc: "Transcription processing is performed at intervals of the specified number of seconds."
|
||||||
|
speaker_max_phrase:
|
||||||
|
label: "Speaker Max Words"
|
||||||
|
desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs."
|
||||||
|
select_transcription_engine:
|
||||||
|
label: "Transcription Engine Used For Speech Recognition"
|
||||||
|
whisper_weight_type:
|
||||||
|
label: "Whisper Model"
|
||||||
|
desc: "Larger models have higher accuracy, but they also consume more CPU or GPU resources.\nEspecially for models larger than medium, it may be difficult or even impossible to use them depending on the performance of your CPU/GPU."
|
||||||
|
model_template: "{{model_name}} model ({{capacity}})"
|
||||||
|
recommended_model_template: "{{model_name}} model ({{capacity}}) (Recommended)"
|
||||||
|
transcription_compute_device:
|
||||||
|
label: "Processing Device Used For AI transcription"
|
||||||
|
|
||||||
|
vr:
|
||||||
|
single_line: "Single line"
|
||||||
|
multi_lines: "Multiple Lines"
|
||||||
|
overlay_enable: "Enable"
|
||||||
|
restore_default_settings: "Reset to Default Settings"
|
||||||
|
position: "Position"
|
||||||
|
rotation: "Rotation"
|
||||||
|
x_position: "X-axis (left-right)"
|
||||||
|
y_position: "Y-axis (up-down)"
|
||||||
|
z_position: "Z-axis (front-back)"
|
||||||
|
x_rotation: "X-axis rotation"
|
||||||
|
y_rotation: "Y-axis rotation"
|
||||||
|
z_rotation: "Z-axis rotation"
|
||||||
|
sample_text_button:
|
||||||
|
start: "Send Sample Texts\nTo Overlay"
|
||||||
|
stop: "Stop Sending"
|
||||||
|
sample_text: "Sample Text."
|
||||||
|
opacity: "Opacity"
|
||||||
|
ui_scaling: "UI Scaling"
|
||||||
|
display_duration: "Display Duration"
|
||||||
|
fadeout_duration: "Fadeout Duration"
|
||||||
|
common_settings: "Common Settings"
|
||||||
|
tracker: "Tracker"
|
||||||
|
hmd: "HMD"
|
||||||
|
left_hand: "Left Hand"
|
||||||
|
right_hand: "Right Hand"
|
||||||
|
overlay_show_only_translated_messages:
|
||||||
|
label: "Show Only Translated Messages"
|
||||||
|
voice_typing_mode:
|
||||||
|
label: "Voice Typing Mode"
|
||||||
|
desc: "Automatically pastes transcribed text into the active input field.\n*Utilizes standard Windows clipboard functionality."
|
||||||
|
|
||||||
|
others:
|
||||||
|
section_label_sounds: "Sounds"
|
||||||
|
section_label_message_formats: "Message Formats"
|
||||||
|
auto_clear_the_message_box:
|
||||||
|
label: "Auto Clear Message box"
|
||||||
|
send_only_translated_messages:
|
||||||
|
label: "Send Only Translated Messages"
|
||||||
|
auto_export_message_logs:
|
||||||
|
label: "Auto Save Message Logs"
|
||||||
|
desc: "Automatically saves the conversation messages as text files."
|
||||||
|
vrc_mic_mute_sync:
|
||||||
|
label: "VRC Mic Mute Sync"
|
||||||
|
desc: "Messages will not be sent to VRCT while VRChat's mic is muted.\n*There may be a slight delay. Push-To-Talk is not supported."
|
||||||
|
send_message_to_vrc:
|
||||||
|
label: "Send Message To VRChat"
|
||||||
|
desc: "This feature is not supported, but there is a way to use it without sending messages to VRChat. Make sure to enable this feature when you wish to send messages to VRChat."
|
||||||
|
notification_vrc_sfx:
|
||||||
|
label: "Enable Notification Sound When Sending Chat"
|
||||||
|
desc: "When this feature is disabled, messages will be sent silently without playing the chatbox notification sound that others can hear."
|
||||||
|
send_received_message_to_vrc:
|
||||||
|
label: "Send Received Message To VRChat"
|
||||||
|
desc: "Send the message you received from the speaker's voice to VRChat's chatbox."
|
||||||
|
message_format_common:
|
||||||
|
example_view:
|
||||||
|
title: "Preview"
|
||||||
|
original_translated: "Original + Translation"
|
||||||
|
original_translated_multi: "Original + Translation (Multi-language)"
|
||||||
|
translated_only_multi: "Translation Only (Multi-language)"
|
||||||
|
translated_only: "Translation Only"
|
||||||
|
original_only: "Original Only"
|
||||||
|
settings:
|
||||||
|
title: "Settings"
|
||||||
|
original: "Original"
|
||||||
|
translated: "Translation"
|
||||||
|
for_multi_translation: "For Multi-Translation"
|
||||||
|
send_message_format:
|
||||||
|
label: "Message Format (Send)"
|
||||||
|
desc: "This allows you to change the format of the message that others see in VRChat."
|
||||||
|
received_message_format:
|
||||||
|
label: "Message Format (Speaker2Chatbox)"
|
||||||
|
desc: "Currently, it is used in Speaker2Chatbox."
|
||||||
|
common_convert_message_hiragana_romaji:
|
||||||
|
desc_1: "Supported only when Japanese is selected as the translation language."
|
||||||
|
desc_2: "Due to the complexity of Japanese, it has limitations on accuracy."
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: "Show Romaji"
|
||||||
|
desc: "When enabled along with '{{convert_message_to_hiragana}}', romaji will be shown on mouse hover."
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: "Show Hiragana"
|
||||||
|
telemetry:
|
||||||
|
label: "Support Our Development"
|
||||||
|
desc: "Sends usage data so our developers can grin and say 'Thanks for using it!' while improving the app.\n* No personal info or conversation content is ever sent."
|
||||||
|
aptabase_privacy_policy_label: "Aptabase Privacy Policy"
|
||||||
|
|
||||||
|
hotkeys:
|
||||||
|
toggle_vrct_visibility:
|
||||||
|
label: "Toggle VRCT visibility"
|
||||||
|
toggle_translation:
|
||||||
|
label: "Toggle {{translation}}"
|
||||||
|
toggle_transcription_send:
|
||||||
|
label: "Toggle {{transcription_send}}"
|
||||||
|
toggle_transcription_receive:
|
||||||
|
label: "Toggle {{transcription_receive}}"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "Downloaded version: {{downloaded_version}}"
|
||||||
|
latest_version: "Latest version: {{latest_version}}"
|
||||||
|
available_after_updating: "Available after updating to the latest version"
|
||||||
|
unavailable_downloaded: "Currently Unavailable - Incompatible with the VRCT version in use"
|
||||||
|
no_latest_info: "Unable to retrieve the latest information"
|
||||||
|
using_latest_version: "Using the latest version"
|
||||||
|
available_latest_version: "Latest version available"
|
||||||
|
unavailable_latest_version: "Latest version currently unavailable"
|
||||||
|
available_in_latest_vrct_version: "Available in the latest VRCT version"
|
||||||
|
unavailable_not_downloaded: "Currently unavailable"
|
||||||
|
|
||||||
|
advanced_settings:
|
||||||
|
osc_ip_address:
|
||||||
|
label: "OSC IP Address"
|
||||||
|
osc_port:
|
||||||
|
label: "OSC Port"
|
||||||
|
open_config_filepath:
|
||||||
|
label: "Open Config File"
|
||||||
|
switch_compute_device:
|
||||||
|
label: "Switch VRCT To CPU/GPU Version"
|
||||||
|
enable_websocket:
|
||||||
|
label: "Enable WebSocket Server"
|
||||||
|
websocket_host:
|
||||||
|
label: "WebSocket Host"
|
||||||
|
websocket_port:
|
||||||
|
label: "WebSocket Port"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
save_success: "Settings have been saved."
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: "Downloading the plugin."
|
||||||
|
downloaded_success: "Downloaded successfully."
|
||||||
|
downloaded_error: "Download failed."
|
||||||
|
|
||||||
|
updating: "Updating the plugin."
|
||||||
|
updated_success: "Updated successfully."
|
||||||
|
updated_error: "Update failed."
|
||||||
|
|
||||||
|
disabled_out_of_support: "The plugin has been disabled. It's not supported on this VRCT version."
|
||||||
|
disabled_due_to_an_error: "An error was detected while running the plugin. Please report this to the plugin developer."
|
||||||
|
|
||||||
|
is_enabled: "The plugin enabled."
|
||||||
|
is_disabled: "The plugin disabled."
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# =================================
|
||||||
|
# IMPORTANT:
|
||||||
|
# Please read 'readme_first.txt' before making any changes.
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
common:
|
||||||
|
go_back_button_label: "戻る"
|
||||||
|
|
||||||
|
common_error:
|
||||||
|
no_device_mic: "マイクデバイスが検出されませんでした。"
|
||||||
|
no_device_speaker: "スピーカーデバイスが検出されませんでした。"
|
||||||
|
threshold_invalid_value: "{{min}} から {{max}} までの数値で設定できます。"
|
||||||
|
failed_download_weight_ctranslate2: "CTranslate2 モデルのダウンロードに失敗しました。"
|
||||||
|
failed_download_weight_whisper: "Whisper モデルのダウンロードに失敗しました。"
|
||||||
|
translation_limit: "翻訳エンジンの使用制限に達したか、一時的に利用制限がかけられています。"
|
||||||
|
deepl_auth_key_invalid_length: "認証キーの文字数が間違っています。"
|
||||||
|
deepl_auth_key_failed_authentication: "認証キーが間違っているか、API使用制限が上限に達しています。"
|
||||||
|
|
||||||
|
invalid_value_mic_record_timeout: "0 以上で 「{{mic_phrase_timeout_label}}」 より大きくすることはできません。"
|
||||||
|
invalid_value_mic_phrase_timeout: "0 以上で 「{{mic_record_timeout_label}}」 より小さくすることはできません。"
|
||||||
|
invalid_value_mic_max_phrase: "0 以上の数値を設定できます。"
|
||||||
|
|
||||||
|
invalid_value_speaker_record_timeout: "0 以上で 「{{speaker_phrase_timeout_label}}」 より大きくすることはできません。"
|
||||||
|
invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。"
|
||||||
|
invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。"
|
||||||
|
|
||||||
|
common_warning:
|
||||||
|
unable_to_use_osc_query: "OSC IP Address の設定によりOSCデータの受信ができないため、以下の機能が自動的に無効になっています。"
|
||||||
|
|
||||||
|
main_page:
|
||||||
|
translation: "翻訳"
|
||||||
|
transcription_send: "マイク入力"
|
||||||
|
transcription_receive: "聞き取り"
|
||||||
|
foreground: "最前面固定"
|
||||||
|
|
||||||
|
language_settings: "言語設定"
|
||||||
|
your_language: "あなたの言語"
|
||||||
|
translate_each_other_label: "双方向に翻訳"
|
||||||
|
swap_button_label: "言語を入れ替え"
|
||||||
|
target_language: "相手の言語"
|
||||||
|
translator: "翻訳エンジン"
|
||||||
|
translator_label_default: "デフォルト"
|
||||||
|
|
||||||
|
translator_selector:
|
||||||
|
is_selected_same_language: "「{{your_language}}」と「{{target_language}}」に同じ言語が選択がされているため、「{{ctranslate2}}」のみが使用できます。"
|
||||||
|
|
||||||
|
message_log:
|
||||||
|
all: "全て"
|
||||||
|
sent: "送信"
|
||||||
|
received: "受信"
|
||||||
|
system: "システム"
|
||||||
|
|
||||||
|
resend_button_on_hover_desc: "長押しで送信"
|
||||||
|
|
||||||
|
state_text_enabled: "有効"
|
||||||
|
state_text_disabled: "無効"
|
||||||
|
|
||||||
|
language_selector:
|
||||||
|
title_your_language: "あなたの言語"
|
||||||
|
title_target_language: "相手の言語"
|
||||||
|
|
||||||
|
update_available: "新しいバージョンが出ました!"
|
||||||
|
updating: "アップデート中..."
|
||||||
|
|
||||||
|
update_modal:
|
||||||
|
cpu_desc: "処理デバイスとしてCPUのみを使用"
|
||||||
|
cuda_desc: "処理デバイスとしてCPUとNVIDIA製のGPUを選択可能"
|
||||||
|
cuda_compare_cpu_desc: "GPU選択時、CPUと比べて処理が高速"
|
||||||
|
cuda_disk_space_desc: "約{{size}}のディスク容量が必要"
|
||||||
|
close_modal: "閉じる"
|
||||||
|
download_latest_and_restart: "最新版がダウンロードされ、\nアプリは自動的に再起動します。"
|
||||||
|
is_latest_version_already: "すでに最新版を使用中"
|
||||||
|
is_current_compute_device: "現在使用中のバージョン"
|
||||||
|
|
||||||
|
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: "処理デバイス"
|
||||||
|
label_type: "処理タイプ"
|
||||||
|
type_template_auto: "自動"
|
||||||
|
type_template_low: "{{type_name}} (精度が悪く、処理は早い)"
|
||||||
|
type_template_high: "{{type_name}} (精度が良く、処理は遅い)"
|
||||||
|
warning_labels:
|
||||||
|
unable_to_use_osc_query: "OSC IP Address の設定によりOSCデータの受信ができないため、現在この機能は使用できません。"
|
||||||
|
|
||||||
|
side_menu_labels:
|
||||||
|
device: "デバイス"
|
||||||
|
appearance: "デザイン"
|
||||||
|
translation: "翻訳"
|
||||||
|
transcription: "音声認識"
|
||||||
|
others: "その他"
|
||||||
|
hotkeys: "ホットキー"
|
||||||
|
plugins: "プラグイン"
|
||||||
|
advanced_settings: "高度な設定"
|
||||||
|
|
||||||
|
device:
|
||||||
|
check_volume: "音量チェック"
|
||||||
|
label_auto_select: "自動選択"
|
||||||
|
label_host: "ホスト/ドライバー"
|
||||||
|
label_device: "デバイス"
|
||||||
|
mic_host_device:
|
||||||
|
label: "マイク (デバイス)"
|
||||||
|
mic_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "マイク入力感度の調整 (現在の設定: 自動)"
|
||||||
|
desc_for_automatic: "マイクの入力感度を自動的に調節する。"
|
||||||
|
label_for_manual: "マイク入力感度の調整 (現在の設定: 手動)"
|
||||||
|
desc_for_manual: "スライダーを調整して入力感度を手動で決められます。マイクのアイコンを押すと、実際に声を入力し、音量を確認しながら調節できます。"
|
||||||
|
speaker_device:
|
||||||
|
label: "スピーカー (デバイス)"
|
||||||
|
speaker_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "スピーカー入力感度の調整 (現在の設定: 自動)"
|
||||||
|
desc_for_automatic: "スピーカーの入力感度を自動的に調節する。"
|
||||||
|
label_for_manual: "スピーカー入力感度の調整 (現在の設定: 手動)"
|
||||||
|
desc_for_manual: "スライダーを調整して入力感度を手動で決められます。ヘッドフォンのアイコンを押すと、実際に音声を聞き取り、音量を確認しながら調節できます。"
|
||||||
|
|
||||||
|
appearance:
|
||||||
|
transparency:
|
||||||
|
label: "透明度"
|
||||||
|
desc: "メイン画面の透明度を変更できます。"
|
||||||
|
ui_size:
|
||||||
|
label: "UIサイズ"
|
||||||
|
textbox_ui_size:
|
||||||
|
label: "ログのフォントサイズ"
|
||||||
|
desc: "ログに表示されるフォントのサイズを、UIサイズを基準にして倍率を変えられます。"
|
||||||
|
send_message_button_type:
|
||||||
|
label: "メッセージ送信ボタン"
|
||||||
|
hide: "非表示 (エンターキーを使って送信)"
|
||||||
|
show: "表示"
|
||||||
|
show_and_disable_enter_key: "表示し、エンターキーでの送信を無効"
|
||||||
|
show_resend_button:
|
||||||
|
label: "再送信ボタンを表示する"
|
||||||
|
desc: "送信済メッセージログにマウスホバーすると、再送信ボタンが表示されます。クリックで編集モード、長押しで再送信します。"
|
||||||
|
font_family:
|
||||||
|
label: "使用フォント"
|
||||||
|
ui_language:
|
||||||
|
label: "UIの言語"
|
||||||
|
|
||||||
|
translation:
|
||||||
|
ctranslate2_weight_type:
|
||||||
|
label: "AI翻訳 {{ctranslate2}} のモデルタイプ"
|
||||||
|
desc: "翻訳エンジン「{{ctranslate2}}」で翻訳する際に、使用する翻訳モデルを選択できます。"
|
||||||
|
small: "通常モデル ({{capacity}})"
|
||||||
|
large: "高精度モデル ({{capacity}})"
|
||||||
|
translation_compute_device:
|
||||||
|
label: "AI翻訳の処理デバイス"
|
||||||
|
|
||||||
|
deepl_auth_key:
|
||||||
|
label: "DeepL APIキーの登録"
|
||||||
|
desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。"
|
||||||
|
open_auth_key_webpage: "DeepLアカウントページを開く"
|
||||||
|
save: "保存"
|
||||||
|
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: "スピーカー"
|
||||||
|
section_label_transcription_engines: "音声認識エンジン"
|
||||||
|
mic_record_timeout:
|
||||||
|
label: "入力が終了したとみなす無音時間"
|
||||||
|
desc: "無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。"
|
||||||
|
mic_phrase_timeout:
|
||||||
|
label: "一度に文字起こしする時間の長さ"
|
||||||
|
desc: "設定された秒数ごとに文字起こし処理が行われます。"
|
||||||
|
mic_max_phrase:
|
||||||
|
label: "送信するまでに保持する単語数"
|
||||||
|
desc: "文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をVRChatへ送信し、ログに表示します。"
|
||||||
|
mic_word_filter:
|
||||||
|
label: "ワードフィルター"
|
||||||
|
desc: "登録された単語を検出すると、その文章は送信されません。\n「,」カンマで区切ると、まとめて複数の単語を追加できます。\n※重複した単語は登録されません。"
|
||||||
|
add_button_label: "追加"
|
||||||
|
count_desc: "現在登録されている単語数: {{count}}"
|
||||||
|
speaker_record_timeout:
|
||||||
|
label: "入力が終了したとみなす無音時間"
|
||||||
|
desc: "無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。"
|
||||||
|
speaker_phrase_timeout:
|
||||||
|
label: "一度に文字起こしする時間の長さ"
|
||||||
|
desc: "設定された秒数ごとに文字起こし処理が行われます。"
|
||||||
|
speaker_max_phrase:
|
||||||
|
label: "ログとして表示するまでに保持する単語数"
|
||||||
|
desc: "文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をログに表示します。"
|
||||||
|
select_transcription_engine:
|
||||||
|
label: "音声認識で使用するエンジン"
|
||||||
|
whisper_weight_type:
|
||||||
|
label: "Whisperモデルのタイプ"
|
||||||
|
desc: "容量が大きいモデルほど精度は高いですが、その分CPUやGPUを占有します。\n※特にmediumより容量の大きいモデルは、CPU/GPUの性能によっては使用すらも困難です。"
|
||||||
|
model_template: "{{model_name}} モデル ({{capacity}})"
|
||||||
|
recommended_model_template: "{{model_name}} モデル ({{capacity}}) [推奨]"
|
||||||
|
transcription_compute_device:
|
||||||
|
label: "AI音声認識で使用する処理デバイス"
|
||||||
|
|
||||||
|
vr:
|
||||||
|
single_line: "一行"
|
||||||
|
multi_lines: "複数行"
|
||||||
|
overlay_enable: "有効にする"
|
||||||
|
restore_default_settings: "初期値に戻す"
|
||||||
|
position: "位置"
|
||||||
|
rotation: "回転"
|
||||||
|
x_position: "X軸(左右)"
|
||||||
|
y_position: "Y軸(上下)"
|
||||||
|
z_position: "Z軸(前後)"
|
||||||
|
x_rotation: "X軸の回転"
|
||||||
|
y_rotation: "Y軸の回転"
|
||||||
|
z_rotation: "Z軸の回転"
|
||||||
|
sample_text_button:
|
||||||
|
start: "サンプルテキストを\nOverlayに送信する"
|
||||||
|
stop: "送信を停止"
|
||||||
|
sample_text: "サンプルテキスト"
|
||||||
|
opacity: "透明度"
|
||||||
|
ui_scaling: "サイズ"
|
||||||
|
display_duration: "表示時間"
|
||||||
|
fadeout_duration: "フェードアウト時間"
|
||||||
|
common_settings: "共通設定"
|
||||||
|
tracker: "表示するトラッカーの位置"
|
||||||
|
hmd: "HMD"
|
||||||
|
left_hand: "左手"
|
||||||
|
right_hand: "右手"
|
||||||
|
overlay_show_only_translated_messages:
|
||||||
|
label: "翻訳後のメッセージのみ表示する"
|
||||||
|
voice_typing_mode:
|
||||||
|
label: "音声タイピングモード"
|
||||||
|
desc: "文字起こしされたテキストを、アクティブな入力欄へと自動的に貼り付けます。\n※Windows標準のクリップボード機能を利用しています。"
|
||||||
|
|
||||||
|
others:
|
||||||
|
section_label_sounds: "サウンド"
|
||||||
|
section_label_message_formats: "メッセージフォーマット"
|
||||||
|
auto_clear_the_message_box:
|
||||||
|
label: "送信後はメッセージ入力欄を空にする"
|
||||||
|
send_only_translated_messages:
|
||||||
|
label: "翻訳後のメッセージのみ送信する"
|
||||||
|
auto_export_message_logs:
|
||||||
|
label: "会話ログを自動的に保存する"
|
||||||
|
desc: "テキストファイルとしてログがlogsフォルダ内に保存されます。"
|
||||||
|
vrc_mic_mute_sync:
|
||||||
|
label: "VRCマイクミュート同期"
|
||||||
|
desc: "VRChatのマイクがミュートされている間は、メッセージをVRChatに送信しません。\n※若干の遅延はあります。また、Push-To-Talkは非対応です。"
|
||||||
|
send_message_to_vrc:
|
||||||
|
label: "VRChatにメッセージを送信する"
|
||||||
|
desc: "サポート対象外ですが、VRChatにメッセージを送信せずに使う方法があります。送信したい場合、この機能を有効にする事を忘れないでください。"
|
||||||
|
notification_vrc_sfx:
|
||||||
|
label: "チャット送信時にチャットボックスの通知音を鳴らす"
|
||||||
|
desc: "この機能を無効にすると、他の人が聞こえるチャットボックスの通知音を鳴らさず、静かに送信します。"
|
||||||
|
send_received_message_to_vrc:
|
||||||
|
label: "受信したメッセージをVRChatに送信する"
|
||||||
|
desc: "スピーカーから聞き取り、文字起こしされたメッセージをVRChatに送信します。"
|
||||||
|
message_format_common:
|
||||||
|
example_view:
|
||||||
|
title: "プレビュー"
|
||||||
|
original_translated: "原文 + 翻訳"
|
||||||
|
original_translated_multi: "原文 + 翻訳(多言語)"
|
||||||
|
translated_only_multi: "翻訳のみ(多言語)"
|
||||||
|
translated_only: "翻訳のみ"
|
||||||
|
original_only: "原文のみ"
|
||||||
|
settings:
|
||||||
|
title: "設定"
|
||||||
|
original: "原文"
|
||||||
|
translated: "翻訳"
|
||||||
|
for_multi_translation: "多言語翻訳用"
|
||||||
|
send_message_format:
|
||||||
|
label: "メッセージフォーマット(送信)"
|
||||||
|
desc: "VRChatで相手に実際に見えるフォーマットを変更できます。"
|
||||||
|
received_message_format:
|
||||||
|
label: メッセージフォーマット(Speaker2Chatbox)
|
||||||
|
desc: 今のところ、Speaker2Chatboxで送信した時の表示に使われます。
|
||||||
|
common_convert_message_hiragana_romaji:
|
||||||
|
desc_1: "翻訳言語として日本語を選択した時のみサポート。"
|
||||||
|
desc_2: "日本語は特殊なため、精度には限界があります。"
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: "ローマ字を表示"
|
||||||
|
desc: "「{{convert_message_to_hiragana}}」と同時に有効にした場合は、マウスホバーで表示されます。"
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: "ひらがなを表示"
|
||||||
|
telemetry:
|
||||||
|
label: "製品の改善に協力する"
|
||||||
|
desc: "開発者が「みんな使ってくれてるんだな~」ってにやにやしながら機能改善するための使用状況データを送信します。\n※会話内容など個人情報は一切送信しません。"
|
||||||
|
aptabase_privacy_policy_label: "Aptabase プライバシーポリシー"
|
||||||
|
|
||||||
|
hotkeys:
|
||||||
|
toggle_vrct_visibility:
|
||||||
|
label: "VRCTの最小化/アクティブ化の切り替え"
|
||||||
|
toggle_translation:
|
||||||
|
label: "「{{translation}}」 オン/オフの切り替え"
|
||||||
|
toggle_transcription_send:
|
||||||
|
label: "「{{transcription_send}}」 オン/オフの切り替え"
|
||||||
|
toggle_transcription_receive:
|
||||||
|
label: "「{{transcription_receive}}」 オン/オフの切り替え"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "ダウンロード済バージョン: {{downloaded_version}}"
|
||||||
|
latest_version: "最新バージョン: {{latest_version}}"
|
||||||
|
available_after_updating: "最新版にアップデート後 利用可能"
|
||||||
|
unavailable_downloaded: "現在利用不可 使用中VRCTバージョンとの互換性なし"
|
||||||
|
no_latest_info: "最新情報が取得できません"
|
||||||
|
using_latest_version: "最新版を使用中"
|
||||||
|
available_latest_version: "最新版を利用可能"
|
||||||
|
unavailable_latest_version: "最新版は現在利用不可"
|
||||||
|
available_in_latest_vrct_version: "VRCT最新版で利用可能"
|
||||||
|
unavailable_not_downloaded: "現在利用不可"
|
||||||
|
|
||||||
|
advanced_settings:
|
||||||
|
osc_ip_address:
|
||||||
|
label: "OSC IP Address"
|
||||||
|
osc_port:
|
||||||
|
label: "OSC Port"
|
||||||
|
open_config_filepath:
|
||||||
|
label: "設定ファイルを開く"
|
||||||
|
switch_compute_device:
|
||||||
|
label: "VRCT CPU/GPUバージョンの切り替え"
|
||||||
|
enable_websocket:
|
||||||
|
label: "WebSocketサーバーを有効にする"
|
||||||
|
websocket_host:
|
||||||
|
label: "WebSocket Host"
|
||||||
|
websocket_port:
|
||||||
|
label: "WebSocket Port"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
save_success: "設定を保存しました。"
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: "プラグインをダウンロード中。"
|
||||||
|
downloaded_success: "プラグインのダウンロードが完了しました。"
|
||||||
|
downloaded_error: "プラグインのダウンロードに失敗しました。"
|
||||||
|
|
||||||
|
updating: "プラグインをアップデート中。"
|
||||||
|
updated_success: "プラグインのアップデートが完了しました。"
|
||||||
|
updated_error: "プラグインのアップデートに失敗しました。"
|
||||||
|
|
||||||
|
disabled_out_of_support: "現在のバージョンとの互換性がありません。プラグインを無効にしました。"
|
||||||
|
disabled_due_to_an_error: "プラグイン実行中にエラーを検知しました。プラグイン開発者に報告してください。"
|
||||||
|
|
||||||
|
is_enabled: "プラグインを有効にしました。"
|
||||||
|
is_disabled: "プラグインを無効にしました。"
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# =================================
|
||||||
|
# IMPORTANT:
|
||||||
|
# Please read 'readme_first.txt' before making any changes.
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
common:
|
||||||
|
go_back_button_label: "돌아가기"
|
||||||
|
|
||||||
|
common_error:
|
||||||
|
no_device_mic: "마이크 디바이스를 찾지 못했습니다."
|
||||||
|
no_device_speaker: "스피커 디바이스를 찾지 못했습니다."
|
||||||
|
threshold_invalid_value: "{{min}}부터 {{max}}까지의 숫자로 설정할 수 있습니다."
|
||||||
|
failed_download_weight_ctranslate2: "CTranslate2 모델 다운로드에 실패했습니다."
|
||||||
|
failed_download_weight_whisper: "Whisper 모델 다운로드에 실패했습니다."
|
||||||
|
translation_limit: "번역 엔진 사용 제한에 도달했거나 일시적으로 이용 제한이 걸렸습니다."
|
||||||
|
deepl_auth_key_invalid_length: "DeepL 인증 키의 문자 수가 잘못되었습니다."
|
||||||
|
deepl_auth_key_failed_authentication: "인증키가 잘못되었거나 API 사용 제한이 상한에 도달했습니다."
|
||||||
|
|
||||||
|
invalid_value_mic_record_timeout: "0 이상에서 '{{mic_phrase_timeout_label}}'보다 클 수 없습니다."
|
||||||
|
invalid_value_mic_phrase_timeout: "0 이상에서 '{{mic_record_timeout_label}}'보다 작을 수 없습니다."
|
||||||
|
invalid_value_mic_max_phrase: "0 이상의 숫자만 설정할 수 있습니다."
|
||||||
|
|
||||||
|
invalid_value_speaker_record_timeout: "0 이상에서 '{{speaker_phrase_timeout_label}}'보다 클 수 없습니다."
|
||||||
|
invalid_value_speaker_phrase_timeout: "0 이상에서 '{{speaker_record_timeout_label}}'보다 작을 수 없습니다."
|
||||||
|
invalid_value_speaker_max_phrase: "0 이상의 숫자만 설정할 수 있습니다."
|
||||||
|
|
||||||
|
common_warning:
|
||||||
|
unable_to_use_osc_query: "OSC IP 주소 설정으로 인해 OSC 데이터 수신이 불가능하므로, 아래 기능이 자동으로 비활성화되었습니다."
|
||||||
|
|
||||||
|
main_page:
|
||||||
|
translation: "번역"
|
||||||
|
transcription_send: "음성인식 (마이크)"
|
||||||
|
transcription_receive: "음성인식 (스피커)"
|
||||||
|
foreground: "항상 위로"
|
||||||
|
|
||||||
|
language_settings: "언어 설정"
|
||||||
|
your_language: "당신의 언어"
|
||||||
|
translate_each_other_label: "양방향으로 번역"
|
||||||
|
swap_button_label: "언어 교체"
|
||||||
|
target_language: "상대방의 언어"
|
||||||
|
translator: "번역 엔진"
|
||||||
|
translator_label_default: "기본값"
|
||||||
|
|
||||||
|
translator_selector:
|
||||||
|
is_selected_same_language: "'{{your_language}}'와 '{{target_language}}'에 동일한 언어가 선택되어 있으므로, 「{{ctranslate2}}」만 사용할 수 있습니다."
|
||||||
|
|
||||||
|
message_log:
|
||||||
|
all: "전체"
|
||||||
|
sent: "전송"
|
||||||
|
received: "수신"
|
||||||
|
system: "시스템"
|
||||||
|
|
||||||
|
resend_button_on_hover_desc: "길게 눌러서 보내기"
|
||||||
|
|
||||||
|
state_text_enabled: "Enabled"
|
||||||
|
state_text_disabled: "Disabled"
|
||||||
|
|
||||||
|
language_selector:
|
||||||
|
title_your_language: "당신의 언어"
|
||||||
|
title_target_language: "상대방의 언어"
|
||||||
|
|
||||||
|
update_available: "새로운 버전이 나왔습니다!"
|
||||||
|
updating: "업데이트 중..."
|
||||||
|
|
||||||
|
update_modal:
|
||||||
|
cpu_desc: "처리 장치로 CPU만을 사용합니다."
|
||||||
|
cuda_desc: "처리 장치로 CPU와 NVIDIA GPU를 선택할 수 있습니다."
|
||||||
|
cuda_compare_cpu_desc: "GPU 선택 시, CPU에 비해 처리가 빠릅니다."
|
||||||
|
cuda_disk_space_desc: "약 {{size}}의 디스크 용량이 필요합니다."
|
||||||
|
close_modal: "닫기"
|
||||||
|
download_latest_and_restart: "최신 버전이 다운로드되면 앱이 자동으로 재시작됩니다."
|
||||||
|
is_latest_version_already: "이미 최신 버전을 사용 중입니다."
|
||||||
|
is_current_compute_device: "현재 사용 중인 버전"
|
||||||
|
|
||||||
|
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:
|
||||||
|
label_type:
|
||||||
|
type_template_auto:
|
||||||
|
type_template_low:
|
||||||
|
type_template_high:
|
||||||
|
warning_labels:
|
||||||
|
unable_to_use_osc_query:
|
||||||
|
|
||||||
|
side_menu_labels:
|
||||||
|
device: "장치"
|
||||||
|
appearance: "모양"
|
||||||
|
translation: "번역"
|
||||||
|
transcription: "음성인식"
|
||||||
|
others: "기타"
|
||||||
|
hotkeys: "단축키"
|
||||||
|
plugins: "플러그인"
|
||||||
|
advanced_settings: "고급 설정"
|
||||||
|
|
||||||
|
device:
|
||||||
|
check_volume: "음량 확인"
|
||||||
|
label_auto_select: "자동 선택"
|
||||||
|
label_host: "호스트/드라이버"
|
||||||
|
label_device: "장치"
|
||||||
|
mic_host_device:
|
||||||
|
label: "마이크 장치"
|
||||||
|
mic_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "음성 입력 최소 볼륨 (현재 설정: 자동)"
|
||||||
|
desc_for_automatic: "마이크의 입력 감도를 자동으로 조절합니다."
|
||||||
|
label_for_manual: "음성 입력 최소 볼륨 (현재 설정: 수동)"
|
||||||
|
desc_for_manual: "슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 마이크 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다."
|
||||||
|
speaker_device:
|
||||||
|
label: "스피커 장치"
|
||||||
|
speaker_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "음성 입력 최소 볼륨 (현재 설정: 자동)"
|
||||||
|
desc_for_automatic: "스피커의 입력 감도를 자동으로 조절합니다."
|
||||||
|
label_for_manual: "음성 입력 최소 볼륨 (현재 설정: 수동)"
|
||||||
|
desc_for_manual: "슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 헤드폰 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다."
|
||||||
|
|
||||||
|
appearance:
|
||||||
|
transparency:
|
||||||
|
label: "투명도"
|
||||||
|
desc: "메인 화면의 투명도를 변경합니다."
|
||||||
|
ui_size:
|
||||||
|
label: "UI 크기"
|
||||||
|
textbox_ui_size:
|
||||||
|
label: "텍스트 박스 글자 크기"
|
||||||
|
desc: "로그에 표시되는 글자 크기의 배율을 UI 크기에 따라 변경합니다."
|
||||||
|
send_message_button_type:
|
||||||
|
label: "메시지 전송 버튼"
|
||||||
|
hide: "숨김 (Enter 키를 사용하여 전송)"
|
||||||
|
show: "표시"
|
||||||
|
show_and_disable_enter_key: "표시 (Enter 키 전송 비활성화)"
|
||||||
|
show_resend_button:
|
||||||
|
label: "재전송 버튼 표시"
|
||||||
|
desc: "보낸 메시지 로그에 마우스를 올리면 재전송 버튼이 표시됩니다. 클릭하면 편집 모드로 전환되며, 길게 누르면 재전송됩니다."
|
||||||
|
font_family:
|
||||||
|
label: "폰트"
|
||||||
|
ui_language:
|
||||||
|
label: "UI 언어"
|
||||||
|
|
||||||
|
translation:
|
||||||
|
ctranslate2_weight_type:
|
||||||
|
label: "번역 모델"
|
||||||
|
desc: "오프라인 번역 시의 번역 모델을 변경합니다."
|
||||||
|
small: "일반 모델 ({{capacity}})"
|
||||||
|
large: "정밀 모델 ({{capacity}})"
|
||||||
|
translation_compute_device:
|
||||||
|
label: "AI 번역 처리 장치"
|
||||||
|
|
||||||
|
deepl_auth_key:
|
||||||
|
label: "DeepL 인증키"
|
||||||
|
desc: "사용시 메인화면에 있는 {{translator}}를 DeepL_API로 변경해 주세요.\n지원하지 않는 언어도 있습니다."
|
||||||
|
open_auth_key_webpage: "DeepL 계정 페이지 열기"
|
||||||
|
save: "저장"
|
||||||
|
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: "스피커"
|
||||||
|
section_label_transcription_engines: "음성 인식 엔진"
|
||||||
|
mic_record_timeout:
|
||||||
|
label: "최대 무음 시간"
|
||||||
|
desc: "무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다."
|
||||||
|
mic_phrase_timeout:
|
||||||
|
label: "최대 인식 시간"
|
||||||
|
desc: "설정된 초 단위로 음성인식 처리가 이루어집니다."
|
||||||
|
mic_max_phrase:
|
||||||
|
label: "최대 입력 절(phrases) 수"
|
||||||
|
desc: "인식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 VRChat으로 전송하고 로그에 표시합니다."
|
||||||
|
mic_word_filter:
|
||||||
|
label: "단어 필터"
|
||||||
|
desc: "등록된 단어가 감지되면 해당 문장은 전송되지 않습니다.\n',' 쉼표로 구분하면 여러 단어를 추가할 수 있습니다.\n* 중복된 단어는 등록되지 않습니다."
|
||||||
|
add_button_label: "추가"
|
||||||
|
count_desc: "현재 등록되어 있는 단어 수: {{count}}"
|
||||||
|
speaker_record_timeout:
|
||||||
|
label: "최대 무음 시간"
|
||||||
|
desc: "무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다."
|
||||||
|
speaker_phrase_timeout:
|
||||||
|
label: "최대 인식 시간"
|
||||||
|
desc: "설정된 초 단위로 음성인식 처리가 이루어집니다."
|
||||||
|
speaker_max_phrase:
|
||||||
|
label: "최대 입력 절(phrases) 수"
|
||||||
|
desc: "식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 로그에 표시합니다."
|
||||||
|
select_transcription_engine:
|
||||||
|
label: "음성 인식에 사용할 엔진"
|
||||||
|
whisper_weight_type:
|
||||||
|
label: "Whisper 모델 타입"
|
||||||
|
desc: "용량이 큰 모델일수록 정확도는 높지만, 그만큼 CPU나 GPU를 많이 차지합니다. * 특히 medium보다 용량이 큰 모델은 CPU/GPU 성능에 따라 사용 자체가 어려울 수 있습니다."
|
||||||
|
model_template: "{{model_name}} 모델 ({{capacity}})"
|
||||||
|
recommended_model_template: "{{model_name}} 모델 ({{capacity}}) (권장)"
|
||||||
|
transcription_compute_device:
|
||||||
|
label:
|
||||||
|
|
||||||
|
vr:
|
||||||
|
single_line: "한 줄"
|
||||||
|
multi_lines: "여러 줄"
|
||||||
|
overlay_enable: "활성화"
|
||||||
|
restore_default_settings: "초기값으로 되돌리기"
|
||||||
|
position: "위치"
|
||||||
|
rotation: "회전"
|
||||||
|
x_position: "X축 (좌우)"
|
||||||
|
y_position: "Y축 (상하)"
|
||||||
|
z_position: "Z축 (앞뒤)"
|
||||||
|
x_rotation: "X축 회전"
|
||||||
|
y_rotation: "Y축 회전"
|
||||||
|
z_rotation: "Z축 회전"
|
||||||
|
sample_text_button:
|
||||||
|
start: "샘플 텍스트를\n오버레이에 전송"
|
||||||
|
stop: "전송 중지"
|
||||||
|
sample_text: "샘플 텍스트"
|
||||||
|
opacity: "투명도"
|
||||||
|
ui_scaling: "UI 크기 조정"
|
||||||
|
display_duration: "표시 시간"
|
||||||
|
fadeout_duration: "페이드 아웃 시간"
|
||||||
|
common_settings: "공통 설정"
|
||||||
|
tracker: "표시할 트래커의 위치"
|
||||||
|
hmd: "HMD"
|
||||||
|
left_hand: "왼손"
|
||||||
|
right_hand: "오른손"
|
||||||
|
overlay_show_only_translated_messages:
|
||||||
|
label: "번역된 메시지만 표시"
|
||||||
|
voice_typing_mode:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
|
||||||
|
others:
|
||||||
|
section_label_sounds: "사운드"
|
||||||
|
section_label_message_formats: "메시지 형식"
|
||||||
|
auto_clear_the_message_box:
|
||||||
|
label: "챗박스 자동 삭제"
|
||||||
|
send_only_translated_messages:
|
||||||
|
label: "번역된 메시지만 전송"
|
||||||
|
auto_export_message_logs:
|
||||||
|
label: "대화 로그 자동 저장"
|
||||||
|
desc: "logs 폴더에 텍스트 파일로 로그가 저장됩니다."
|
||||||
|
vrc_mic_mute_sync:
|
||||||
|
label: "VRC 마이크 음소거 동기화"
|
||||||
|
desc: "VRChat의 마이크가 음소거 상태인 동안에는 메시지를 VRChat에 전송하지 않습니다. * 약간의 지연이 발생할 수 있습니다. 또한 눌러서 말하기(Push-To-Talk) 기능은 지원되지 않습니다."
|
||||||
|
send_message_to_vrc:
|
||||||
|
label: "VRChat에 메시지 전송"
|
||||||
|
desc: "VRChat에 메시지를 보내지 않고 사용할 수 있는 방법이 있지만 지원되지 않습니다. VRChat에 메시지를 보내려면 이 기능을 활성화하세요."
|
||||||
|
notification_vrc_sfx:
|
||||||
|
label: "채팅 전송 시 채팅창 알림음 재생"
|
||||||
|
desc: "이 기능을 비활성화하면 다른 사람이 들을 수 있는 채팅창 알림음을 울리지 않고 조용히 전송합니다."
|
||||||
|
send_received_message_to_vrc:
|
||||||
|
label: "수신한 메시지를 VRChat에 전송"
|
||||||
|
desc: "스피커에서 인식된 내용을 텍스트로 변환한 메시지가 VRChat에 전송됩니다."
|
||||||
|
message_format_common:
|
||||||
|
example_view:
|
||||||
|
title: "미리 보기"
|
||||||
|
original_translated: "원문 + 번역문"
|
||||||
|
original_translated_multi: "원문 + 번역문 (다국어)"
|
||||||
|
translated_only_multi: "번역문만 (다국어)"
|
||||||
|
translated_only: "번역문만"
|
||||||
|
original_only: "원문만"
|
||||||
|
settings:
|
||||||
|
title: "설정"
|
||||||
|
original: "원문"
|
||||||
|
translated: "번역문"
|
||||||
|
for_multi_translation: "다국어 번역용"
|
||||||
|
send_message_format:
|
||||||
|
label: "메시지 형식 (송신)"
|
||||||
|
desc: "VRChat에서 상대방에게 실제로 보이는 형식을 변경할 수 있습니다."
|
||||||
|
received_message_format:
|
||||||
|
label: "메시지 형식 (Speaker2Chatbox)"
|
||||||
|
desc: "현재로서는 Speaker2Chatbox로 전송할 때의 표시용으로 사용됩니다."
|
||||||
|
common_convert_message_hiragana_romaji:
|
||||||
|
desc_1:
|
||||||
|
desc_2:
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label:
|
||||||
|
telemetry:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
aptabase_privacy_policy_label:
|
||||||
|
|
||||||
|
hotkeys:
|
||||||
|
toggle_vrct_visibility:
|
||||||
|
label: "VRCT 최소화/활성화 전환"
|
||||||
|
toggle_translation:
|
||||||
|
label: "'{{translation}}' 켜기/끄기 전환"
|
||||||
|
toggle_transcription_send:
|
||||||
|
label: "'{{transcription_send}}' 켜기/끄기 전환"
|
||||||
|
toggle_transcription_receive:
|
||||||
|
label: "'{{transcription_receive}}' 켜기/끄기 전환"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "다운로드된 버전: {{downloaded_version}}"
|
||||||
|
latest_version: "최신 버전: {{latest_version}}"
|
||||||
|
available_after_updating: "최신 버전으로 업데이트 후 이용 가능"
|
||||||
|
unavailable_downloaded: "현재 사용 중인 VRCT 버전과의 호환성 문제로 인해 이용 불가"
|
||||||
|
no_latest_info: "최신 정보를 가져올 수 없습니다"
|
||||||
|
using_latest_version: "최신 버전을 사용 중"
|
||||||
|
available_latest_version: "최신 버전을 이용 가능"
|
||||||
|
unavailable_latest_version: "최신 버전은 현재 이용 불가"
|
||||||
|
available_in_latest_vrct_version: "VRCT 최신 버전에서 이용 가능"
|
||||||
|
unavailable_not_downloaded: "현재 이용 불가"
|
||||||
|
|
||||||
|
advanced_settings:
|
||||||
|
osc_ip_address:
|
||||||
|
label: "OSC IP 주소"
|
||||||
|
osc_port:
|
||||||
|
label: "OSC 포트"
|
||||||
|
open_config_filepath:
|
||||||
|
label: "설정 파일 열기"
|
||||||
|
switch_compute_device:
|
||||||
|
label: "VRCT CPU/GPU 버전 전환"
|
||||||
|
enable_websocket:
|
||||||
|
label: "WebSocket 서버 활성화"
|
||||||
|
websocket_host:
|
||||||
|
label: "WebSocket 호스트"
|
||||||
|
websocket_port:
|
||||||
|
label: "WebSocket 포트"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
save_success: "설정을 저장했습니다."
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: "플러그인을 다운로드 중입니다."
|
||||||
|
downloaded_success: "플러그인 다운로드가 완료되었습니다."
|
||||||
|
downloaded_error: "플러그인 다운로드에 실패했습니다."
|
||||||
|
|
||||||
|
updating: "플러그인을 업데이트 중입니다."
|
||||||
|
updated_success: "플러그인 업데이트가 완료되었습니다."
|
||||||
|
updated_error: "플러그인 업데이트에 실패했습니다."
|
||||||
|
|
||||||
|
disabled_out_of_support: "현재 버전과 호환되지 않습니다. 플러그인을 비활성화했습니다."
|
||||||
|
disabled_due_to_an_error: "플러그인 실행 중 오류를 감지했습니다. 플러그인 개발자에게 보고해 주세요."
|
||||||
|
|
||||||
|
is_enabled: "플러그인을 활성화했습니다."
|
||||||
|
is_disabled: "플러그인을 비활성화했습니다."
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Thank you for considering contributing to VRCT's UI translations.
|
||||||
|
If you want to edit a language:
|
||||||
|
- If it already exists: edit the corresponding [xx].yml file.
|
||||||
|
- If it doesn’t exist: please open a GitHub issue or contact us through another channel. (If you know how to add a new language yourself, feel free to do so! Sorry for the complicated structure...)
|
||||||
|
|
||||||
|
Languages currently supported:
|
||||||
|
- en: English
|
||||||
|
- ja: 日本語
|
||||||
|
- ko: 한국어
|
||||||
|
- zh-Hant: 繁體中文
|
||||||
|
- zh-Hans: 简体中文
|
||||||
|
|
||||||
|
Last updated: 2025/12
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// To avoid a name conflict with our own `useTranslation` function,
|
||||||
|
// rename the one from `react-i18next` to `useI18n`.
|
||||||
|
// This is aliased via `vite.config.js`, so it can be imported using `@useI18n`.
|
||||||
|
// Example:
|
||||||
|
// import { useI18n } from "@useI18n";
|
||||||
|
//
|
||||||
|
// export const useTranslation = () => {
|
||||||
|
// const { t } = useI18n();
|
||||||
|
// ...
|
||||||
|
// };
|
||||||
|
export { useTranslation as useI18n } from "react-i18next";
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# =================================
|
||||||
|
# IMPORTANT:
|
||||||
|
# Please read 'readme_first.txt' before making any changes.
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
common:
|
||||||
|
go_back_button_label: "返回"
|
||||||
|
|
||||||
|
common_error:
|
||||||
|
no_device_mic: "未检测到麦克风设备。"
|
||||||
|
no_device_speaker: "未检测到扬声器设备。"
|
||||||
|
threshold_invalid_value: "可设置的数值范围为 {{min}} 到 {{max}}。"
|
||||||
|
failed_download_weight_ctranslate2: "CTranslate2 模型下载失败。"
|
||||||
|
failed_download_weight_whisper: "Whisper 模型下载失败。"
|
||||||
|
translation_limit: "已达到翻译引擎使用限制或临时受到使用限制。"
|
||||||
|
deepl_auth_key_invalid_length: "授权密钥字符数错误。"
|
||||||
|
deepl_auth_key_failed_authentication: "授权密钥错误或已达API使用上限。"
|
||||||
|
|
||||||
|
invalid_value_mic_record_timeout: "数值应为 0 至 「{{mic_phrase_timeout_label}}」"
|
||||||
|
invalid_value_mic_phrase_timeout: "转录间隔时间大于0秒且不能小于「{{mic_record_timeout_label}}」"
|
||||||
|
invalid_value_mic_max_phrase: "数值应为 0 以上"
|
||||||
|
|
||||||
|
invalid_value_speaker_record_timeout: "数值应为 0 至 「{{speaker_phrase_timeout_label}}」"
|
||||||
|
invalid_value_speaker_phrase_timeout: "转录间隔时间大于0秒且不能小于「{{speaker_record_timeout_label}}」"
|
||||||
|
invalid_value_speaker_max_phrase: "数值应为 0 以上"
|
||||||
|
|
||||||
|
common_warning:
|
||||||
|
unable_to_use_osc_query: "由于 OSC IP Address 的设置无法接收 OSC 数据,以下功能已自动禁用。"
|
||||||
|
|
||||||
|
main_page:
|
||||||
|
translation: "翻译"
|
||||||
|
transcription_send: "你的语音转文字"
|
||||||
|
transcription_receive: "他人语音转文字"
|
||||||
|
foreground: "顶层显示"
|
||||||
|
|
||||||
|
language_settings: "语言设定"
|
||||||
|
your_language: "你的语言"
|
||||||
|
translate_each_other_label: "双向翻译"
|
||||||
|
swap_button_label: "互换"
|
||||||
|
target_language: "目标语言"
|
||||||
|
translator: "翻译器"
|
||||||
|
translator_label_default: "默认"
|
||||||
|
|
||||||
|
translator_selector:
|
||||||
|
is_selected_same_language: "「{{your_language}}」和「{{target_language}}」选择了相同的语言,因此只能使用「{{ctranslate2}}」。"
|
||||||
|
|
||||||
|
message_log:
|
||||||
|
all: "全部"
|
||||||
|
sent: "发送"
|
||||||
|
received: "接受"
|
||||||
|
system: "系统"
|
||||||
|
|
||||||
|
resend_button_on_hover_desc: "长按发送"
|
||||||
|
|
||||||
|
state_text_enabled: "启用"
|
||||||
|
state_text_disabled: "停用"
|
||||||
|
|
||||||
|
language_selector:
|
||||||
|
title_your_language: "你的语言"
|
||||||
|
title_target_language: "目标语言"
|
||||||
|
|
||||||
|
update_available: "有新版本可供使用!"
|
||||||
|
updating: "更新中..."
|
||||||
|
|
||||||
|
update_modal:
|
||||||
|
cpu_desc: "仅使用 CPU 作为处理设备"
|
||||||
|
cuda_desc: "可选择 CPU 和 NVIDIA GPU 作为处理设备"
|
||||||
|
cuda_compare_cpu_desc: "选择 GPU 时,与 CPU 相比处理更快"
|
||||||
|
cuda_disk_space_desc: "需要约 {{size}} 的磁盘空间"
|
||||||
|
close_modal: "关闭"
|
||||||
|
download_latest_and_restart: "将下载最新版本,\n应用程序将自动重启。"
|
||||||
|
is_latest_version_already: "已在使用最新版本"
|
||||||
|
is_current_compute_device: "当前使用的版本"
|
||||||
|
|
||||||
|
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: "处理设备"
|
||||||
|
label_type: "处理类型"
|
||||||
|
type_template_auto: "自动"
|
||||||
|
type_template_low: "{{type_name}} (精度较低,处理较快)"
|
||||||
|
type_template_high: "{{type_name}} (精度较高,处理较慢)"
|
||||||
|
warning_labels:
|
||||||
|
unable_to_use_osc_query: "由于 OSC IP Address 的设置无法接收 OSC 数据,目前无法使用此功能。"
|
||||||
|
|
||||||
|
side_menu_labels:
|
||||||
|
device: "设备"
|
||||||
|
appearance: "外观"
|
||||||
|
translation: "翻译"
|
||||||
|
transcription: "语音识别"
|
||||||
|
others: "其他"
|
||||||
|
hotkeys: "快捷键"
|
||||||
|
plugins: "插件"
|
||||||
|
advanced_settings: "高级设置"
|
||||||
|
|
||||||
|
device:
|
||||||
|
check_volume: "音量检查"
|
||||||
|
label_auto_select: "自动选择"
|
||||||
|
label_host: "主机/驱动程序"
|
||||||
|
label_device: "设备"
|
||||||
|
mic_host_device:
|
||||||
|
label: "麦克风 (设备)"
|
||||||
|
mic_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "麦克风输入阈值(当前设置:自动)"
|
||||||
|
desc_for_automatic: "自动调整麦克风输入阈值"
|
||||||
|
label_for_manual: "麦克风输入阈值(当前设置:手动)"
|
||||||
|
desc_for_manual: "使用滑杆手动确定麦克风输入灵敏度。按下麦克风图标输入语音,并在监控音量的同时调节灵敏度。"
|
||||||
|
speaker_device:
|
||||||
|
label: "他人语音 (设备)"
|
||||||
|
speaker_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "他人语音接收阈值(当前设置:自动)"
|
||||||
|
desc_for_automatic: "自动调节他人语音接收阈值"
|
||||||
|
label_for_manual: "他人语音接收阈值(当前设置:手动)"
|
||||||
|
desc_for_manual: "使用滑杆手动调整他人语音接收阈值.在按下耳机按钮时,请根据实际听到的声音调整该大小"
|
||||||
|
|
||||||
|
appearance:
|
||||||
|
transparency:
|
||||||
|
label: "透明度"
|
||||||
|
desc: "更改主视窗透明度"
|
||||||
|
ui_size:
|
||||||
|
label: "界面大小"
|
||||||
|
textbox_ui_size:
|
||||||
|
label: "文本框字体大小"
|
||||||
|
desc: "你可以根据用户界面大小调整文本框中使用的字体大小。"
|
||||||
|
send_message_button_type:
|
||||||
|
label: "发送信息按钮"
|
||||||
|
hide: "隐藏 (可使用回车发送信息)"
|
||||||
|
show: "显示"
|
||||||
|
show_and_disable_enter_key: "显示,并且停用‘回车发送信息’"
|
||||||
|
show_resend_button:
|
||||||
|
label: "显示重新发送按钮"
|
||||||
|
desc: "鼠标悬停在已发送消息日志上时,会显示重新发送按钮。点击进入编辑模式,长按重新发送。"
|
||||||
|
font_family:
|
||||||
|
label: "字体"
|
||||||
|
ui_language:
|
||||||
|
label: "界面语言"
|
||||||
|
|
||||||
|
translation:
|
||||||
|
ctranslate2_weight_type:
|
||||||
|
label: "选择离线翻译模型"
|
||||||
|
desc: "可以选择用于离线翻译的翻译模型"
|
||||||
|
small: "普通模型 ({{capacity}})"
|
||||||
|
large: "高精度模型 ({{capacity}})"
|
||||||
|
translation_compute_device:
|
||||||
|
label: "AI 翻译的处理设备"
|
||||||
|
|
||||||
|
deepl_auth_key:
|
||||||
|
label: "DeepL 授权密匙"
|
||||||
|
desc: "在使用的时候,使用时请在主屏幕上通过 DeepL_API 选择 {{translator}}\n※某些语言可能不支持"
|
||||||
|
open_auth_key_webpage: "打开DeepL账号页面"
|
||||||
|
save: "保存"
|
||||||
|
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: "扬声器"
|
||||||
|
section_label_transcription_engines: "语音识别引擎"
|
||||||
|
mic_record_timeout:
|
||||||
|
label: "语音输入结束后的静音时间"
|
||||||
|
desc: "当检测到静音并经过设定的秒数后,语音输入即被视为完成。"
|
||||||
|
mic_phrase_timeout:
|
||||||
|
label: "转录间隔"
|
||||||
|
desc: "在经过设定的时间后执行转录"
|
||||||
|
mic_max_phrase:
|
||||||
|
label: "麦克风发送时的最小单词数"
|
||||||
|
desc: "转录字数的下限,只有超过这个数字,才会记录翻译结果并发送到VRC"
|
||||||
|
mic_word_filter:
|
||||||
|
label: "单词过滤器"
|
||||||
|
desc: "检测出被记录的单词时,不会发送这段话\n如要添加多个单词,可以用逗号来分割\n※不会记录重复的单词"
|
||||||
|
add_button_label: "添加"
|
||||||
|
count_desc: "现在被记录的单词数: {{count}}"
|
||||||
|
speaker_record_timeout:
|
||||||
|
label: "语音接收结束后的静音时间"
|
||||||
|
desc: "当检测到静音并经过设定的秒数后,语音接收即被视为完成。"
|
||||||
|
speaker_phrase_timeout:
|
||||||
|
label: "转录间隔"
|
||||||
|
desc: "在经过设定的时间后执行转录"
|
||||||
|
speaker_max_phrase:
|
||||||
|
label: "语音接收时的最小单词数"
|
||||||
|
desc: "转录字数的下限,只有超过这个数字,才会记录转录结果"
|
||||||
|
select_transcription_engine:
|
||||||
|
label: "语音识别使用的引擎"
|
||||||
|
whisper_weight_type:
|
||||||
|
label: "Whisper 模型类型"
|
||||||
|
desc: "容量越大的模型精度越高,但会占用更多 CPU/GPU 资源。\n※特别是比 medium 更大的模型,根据 CPU/GPU 性能可能难以使用。"
|
||||||
|
model_template: "{{model_name}} 模型 ({{capacity}})"
|
||||||
|
recommended_model_template: "{{model_name}} 模型 ({{capacity}}) (推荐)"
|
||||||
|
transcription_compute_device:
|
||||||
|
label: "AI 语音识别使用的处理设备"
|
||||||
|
|
||||||
|
vr:
|
||||||
|
single_line: "单行"
|
||||||
|
multi_lines: "多行"
|
||||||
|
overlay_enable: "启用"
|
||||||
|
restore_default_settings: "恢复默认设置"
|
||||||
|
position: "位置"
|
||||||
|
rotation: "旋转"
|
||||||
|
x_position: "X轴(左右)"
|
||||||
|
y_position: "Y轴(上下)"
|
||||||
|
z_position: "Z轴(前后)"
|
||||||
|
x_rotation: "X轴旋转"
|
||||||
|
y_rotation: "Y轴旋转"
|
||||||
|
z_rotation: "Z轴旋转"
|
||||||
|
sample_text_button:
|
||||||
|
start: "向 Overlay\n发送示例文本"
|
||||||
|
stop: "停止发送"
|
||||||
|
sample_text: "示例文本"
|
||||||
|
opacity: "透明度"
|
||||||
|
ui_scaling: "大小"
|
||||||
|
display_duration: "显示持续时间"
|
||||||
|
fadeout_duration: "渐隐持续时间"
|
||||||
|
common_settings: "通用设置"
|
||||||
|
tracker: "显示追踪器的位置"
|
||||||
|
hmd: "HMD"
|
||||||
|
left_hand: "左手"
|
||||||
|
right_hand: "右手"
|
||||||
|
overlay_show_only_translated_messages:
|
||||||
|
label: "仅显示翻译后的消息"
|
||||||
|
voice_typing_mode:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
|
||||||
|
others:
|
||||||
|
section_label_sounds: "声音"
|
||||||
|
section_label_message_formats: "消息格式"
|
||||||
|
auto_clear_the_message_box:
|
||||||
|
label: "发言后自动清空chatbox"
|
||||||
|
send_only_translated_messages:
|
||||||
|
label: "只发送翻译后的信息"
|
||||||
|
auto_export_message_logs:
|
||||||
|
label: "自动导出聊天记录"
|
||||||
|
desc: "以文本文件的形式在logs文件夹中保存。"
|
||||||
|
vrc_mic_mute_sync:
|
||||||
|
label: "与VRC中的麦克风静音同步"
|
||||||
|
desc: "当VRChat的麦克风处于静音时,不在VRChat中发送信息\n※存在少许延迟且不支持按键发言."
|
||||||
|
send_message_to_vrc:
|
||||||
|
label: "发送信息至VRChat"
|
||||||
|
desc: "不发送信息至VRChat的情况下也能使用它,但该功能现在并未完成.在想要发送信息时,请不要忘记打开这个功能."
|
||||||
|
notification_vrc_sfx:
|
||||||
|
label: "发送聊天时播放聊天框的通知音"
|
||||||
|
desc: "禁用此功能后,将不会播放其他人能听到的聊天框通知音,静默发送。"
|
||||||
|
send_received_message_to_vrc:
|
||||||
|
label: "将接收到的消息发送到 VRChat"
|
||||||
|
desc: "将从扬声器听到并转录的消息发送到 VRChat。"
|
||||||
|
message_format_common:
|
||||||
|
example_view:
|
||||||
|
title: "预览"
|
||||||
|
original_translated: "原文 + 翻译"
|
||||||
|
original_translated_multi: "原文 + 翻译(多语言)"
|
||||||
|
translated_only_multi: "仅翻译(多语言)"
|
||||||
|
translated_only: "仅翻译"
|
||||||
|
original_only: "仅原文"
|
||||||
|
settings:
|
||||||
|
title: "设置"
|
||||||
|
original: "原文"
|
||||||
|
translated: "翻译"
|
||||||
|
for_multi_translation: "多语言翻译用"
|
||||||
|
send_message_format:
|
||||||
|
label: "消息格式(发送)"
|
||||||
|
desc: "可以更改在 VRChat 中对方实际看到的格式。"
|
||||||
|
received_message_format:
|
||||||
|
label: "消息格式(Speaker2Chatbox)"
|
||||||
|
desc: "目前用于通过 Speaker2Chatbox 发送时的显示。"
|
||||||
|
common_convert_message_hiragana_romaji:
|
||||||
|
desc_1: "仅在选择日语作为翻译语言时支持。"
|
||||||
|
desc_2: "由于日语的特殊性,精度有限。"
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: "显示罗马音"
|
||||||
|
desc: "与「{{convert_message_to_hiragana}}」同时启用时,将通过鼠标悬停显示。"
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: "显示平假名"
|
||||||
|
telemetry:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
aptabase_privacy_policy_label:
|
||||||
|
|
||||||
|
hotkeys:
|
||||||
|
toggle_vrct_visibility:
|
||||||
|
label: "切换 VRCT 最小化/激活"
|
||||||
|
toggle_translation:
|
||||||
|
label: "切换「{{translation}}」开/关"
|
||||||
|
toggle_transcription_send:
|
||||||
|
label: "切换「{{transcription_send}}」开/关"
|
||||||
|
toggle_transcription_receive:
|
||||||
|
label: "切换「{{transcription_receive}}」开/关"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "已下载版本:{{downloaded_version}}"
|
||||||
|
latest_version: "最新版本:{{latest_version}}"
|
||||||
|
available_after_updating: "更新到最新版后可用"
|
||||||
|
unavailable_downloaded: "当前不可用 与使用中的 VRCT 版本不兼容"
|
||||||
|
no_latest_info: "无法获取最新信息"
|
||||||
|
using_latest_version: "正在使用最新版本"
|
||||||
|
available_latest_version: "最新版本可用"
|
||||||
|
unavailable_latest_version: "最新版本当前不可用"
|
||||||
|
available_in_latest_vrct_version: "可在 VRCT 最新版中使用"
|
||||||
|
unavailable_not_downloaded: "当前不可用"
|
||||||
|
|
||||||
|
advanced_settings:
|
||||||
|
osc_ip_address:
|
||||||
|
label: "OSC IP 地址"
|
||||||
|
osc_port:
|
||||||
|
label: "OSC 端口"
|
||||||
|
open_config_filepath:
|
||||||
|
label: "打开设置文件"
|
||||||
|
switch_compute_device:
|
||||||
|
label: "切换 VRCT CPU/GPU 版本"
|
||||||
|
enable_websocket:
|
||||||
|
label: "启用 WebSocket 服务器"
|
||||||
|
websocket_host:
|
||||||
|
label: "WebSocket 主机"
|
||||||
|
websocket_port:
|
||||||
|
label: "WebSocket 端口"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
save_success: "设置已保存。"
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: "正在下载插件。"
|
||||||
|
downloaded_success: "插件下载完成。"
|
||||||
|
downloaded_error: "插件下载失败。"
|
||||||
|
|
||||||
|
updating: "正在更新插件。"
|
||||||
|
updated_success: "插件更新完成。"
|
||||||
|
updated_error: "插件更新失败。"
|
||||||
|
|
||||||
|
disabled_out_of_support: "与当前版本不兼容。已禁用插件。"
|
||||||
|
disabled_due_to_an_error: "插件运行时检测到错误。请向插件开发者报告。"
|
||||||
|
|
||||||
|
is_enabled: "插件已启用。"
|
||||||
|
is_disabled: "插件已禁用。"
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# =================================
|
||||||
|
# IMPORTANT:
|
||||||
|
# Please read 'readme_first.txt' before making any changes.
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
common:
|
||||||
|
go_back_button_label: "返回"
|
||||||
|
|
||||||
|
common_error:
|
||||||
|
no_device_mic: "未偵測到麥克風裝置。"
|
||||||
|
no_device_speaker: "未偵測到喇叭裝置。"
|
||||||
|
threshold_invalid_value: "可以設置 {{min}} 到 {{max}} 之間的值。"
|
||||||
|
failed_download_weight_ctranslate2: "CTranslate2 模型下載失敗。"
|
||||||
|
failed_download_weight_whisper: "Whisper 模型下載失敗。"
|
||||||
|
translation_limit: "已達到翻譯引擎使用限制或臨時受到使用限制。"
|
||||||
|
deepl_auth_key_invalid_length: "授權金鑰字元數錯誤。"
|
||||||
|
deepl_auth_key_failed_authentication: "授權金鑰錯誤或已達 API 使用上限。"
|
||||||
|
|
||||||
|
invalid_value_mic_record_timeout: "不能大於「{{mic_phrase_timeout_label}}」,應為 0 或更高。"
|
||||||
|
invalid_value_mic_phrase_timeout: "不能小於「{{mic_record_timeout_label}}」,應為 0 或更高。"
|
||||||
|
invalid_value_mic_max_phrase: "可以設置 0 或更高的數值。"
|
||||||
|
|
||||||
|
invalid_value_speaker_record_timeout: "不能大於「{{speaker_phrase_timeout_label}}」,應為 0 或更高。"
|
||||||
|
invalid_value_speaker_phrase_timeout: "不能小於「{{speaker_record_timeout_label}}」,應為 0 或更高。"
|
||||||
|
invalid_value_speaker_max_phrase: "可以設置 0 或更高的數值。"
|
||||||
|
|
||||||
|
common_warning:
|
||||||
|
unable_to_use_osc_query: "由於 OSC IP Address 的設定無法接收 OSC 資料,以下功能已自動停用。"
|
||||||
|
|
||||||
|
main_page:
|
||||||
|
translation: "翻譯"
|
||||||
|
transcription_send: "麥克風轉文字"
|
||||||
|
transcription_receive: "喇叭轉文字"
|
||||||
|
foreground: "最上層顯示"
|
||||||
|
|
||||||
|
language_settings: "語言設定"
|
||||||
|
your_language: "你的語言"
|
||||||
|
translate_each_other_label: "互相翻譯"
|
||||||
|
swap_button_label: "交換語言"
|
||||||
|
target_language: "目標語言"
|
||||||
|
translator: "翻譯器"
|
||||||
|
translator_label_default: "預設"
|
||||||
|
|
||||||
|
translator_selector:
|
||||||
|
is_selected_same_language: "「{{your_language}}」和「{{target_language}}」選擇了相同的語言,因此只能使用「{{ctranslate2}}」。"
|
||||||
|
|
||||||
|
message_log:
|
||||||
|
all: "全部"
|
||||||
|
sent: "已發送"
|
||||||
|
received: "已接收"
|
||||||
|
system: "系統"
|
||||||
|
|
||||||
|
resend_button_on_hover_desc: "長按發送"
|
||||||
|
|
||||||
|
state_text_enabled: "啟用"
|
||||||
|
state_text_disabled: "停用"
|
||||||
|
|
||||||
|
language_selector:
|
||||||
|
title_your_language: "選擇你的語言"
|
||||||
|
title_target_language: "選擇目標語言"
|
||||||
|
|
||||||
|
update_available: "有新版本可供使用!"
|
||||||
|
updating: "正在更新..."
|
||||||
|
|
||||||
|
update_modal:
|
||||||
|
cpu_desc: "僅使用 CPU 作為處理裝置"
|
||||||
|
cuda_desc: "可選擇 CPU 和 NVIDIA GPU 作為處理裝置"
|
||||||
|
cuda_compare_cpu_desc: "選擇 GPU 時,與 CPU 相比處理更快"
|
||||||
|
cuda_disk_space_desc: "需要約 {{size}} 的磁碟空間"
|
||||||
|
close_modal: "關閉"
|
||||||
|
download_latest_and_restart: "將下載最新版本,\n應用程式將自動重新啟動。"
|
||||||
|
is_latest_version_already: "已在使用最新版本"
|
||||||
|
is_current_compute_device: "目前使用的版本"
|
||||||
|
|
||||||
|
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: "處理裝置"
|
||||||
|
label_type: "處理類型"
|
||||||
|
type_template_auto: "自動"
|
||||||
|
type_template_low: "{{type_name}}(精度較低,處理較快)"
|
||||||
|
type_template_high: "{{type_name}}(精度較高,處理較慢)"
|
||||||
|
warning_labels:
|
||||||
|
unable_to_use_osc_query: "由於 OSC IP Address 的設定無法接收 OSC 資料,目前無法使用此功能。"
|
||||||
|
|
||||||
|
side_menu_labels:
|
||||||
|
device: "裝置"
|
||||||
|
appearance: "外觀"
|
||||||
|
translation: "翻譯"
|
||||||
|
transcription: "語音辨識"
|
||||||
|
others: "其他"
|
||||||
|
hotkeys: "快捷鍵"
|
||||||
|
plugins: "外掛程式"
|
||||||
|
advanced_settings: "進階設定"
|
||||||
|
|
||||||
|
device:
|
||||||
|
check_volume: "音量檢查"
|
||||||
|
label_auto_select: "自動選擇"
|
||||||
|
label_host: "主機/驅動程式"
|
||||||
|
label_device: "裝置"
|
||||||
|
mic_host_device:
|
||||||
|
label: "麥克風裝置"
|
||||||
|
mic_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "麥克風能量閾值(當前設置:自動)"
|
||||||
|
desc_for_automatic: "自動判定麥克風輸入靈敏度。"
|
||||||
|
label_for_manual: "麥克風能量閾值(當前設置:手動)"
|
||||||
|
desc_for_manual: "使用滑桿調整麥克風輸入靈敏度,你可以按下麥克風圖示來測試。"
|
||||||
|
speaker_device:
|
||||||
|
label: "喇叭裝置"
|
||||||
|
speaker_dynamic_energy_threshold:
|
||||||
|
label_for_automatic: "喇叭能量閾值(當前設置:自動)"
|
||||||
|
desc_for_automatic: "自動確定喇叭輸入靈敏度。"
|
||||||
|
label_for_manual: "喇叭能量閾值(當前設置:手動)"
|
||||||
|
desc_for_manual: "使用滑桿調整喇叭輸入靈敏度,你可以按下喇叭圖示來測試。"
|
||||||
|
|
||||||
|
appearance:
|
||||||
|
transparency:
|
||||||
|
label: "透明度"
|
||||||
|
desc: "變更主視窗的透明度。"
|
||||||
|
ui_size:
|
||||||
|
label: "介面大小"
|
||||||
|
textbox_ui_size:
|
||||||
|
label: "訊息框字體大小"
|
||||||
|
desc: "你可以根據介面大小調整記錄中使用的字體大小。"
|
||||||
|
send_message_button_type:
|
||||||
|
label: "發送訊息按鈕"
|
||||||
|
hide: "隱藏(使用 Enter 鍵發送)"
|
||||||
|
show: "顯示"
|
||||||
|
show_and_disable_enter_key: "顯示並停用 Enter 鍵發送"
|
||||||
|
show_resend_button:
|
||||||
|
label: "顯示重新發送按鈕"
|
||||||
|
desc: "滑鼠懸停在已發送訊息記錄上時,會顯示重新發送按鈕。點擊進入編輯模式,長按重新發送。"
|
||||||
|
font_family:
|
||||||
|
label: "字型"
|
||||||
|
ui_language:
|
||||||
|
label: "介面語言"
|
||||||
|
|
||||||
|
translation:
|
||||||
|
ctranslate2_weight_type:
|
||||||
|
label: "選擇離線翻譯模型"
|
||||||
|
desc: "你可以選擇用於離線翻譯引擎的翻譯模型。"
|
||||||
|
small: "基本模型({{capacity}})"
|
||||||
|
large: "高準確率模型({{capacity}})"
|
||||||
|
translation_compute_device:
|
||||||
|
label: "AI 翻譯的處理裝置"
|
||||||
|
|
||||||
|
deepl_auth_key:
|
||||||
|
label: "DeepL 授權金鑰"
|
||||||
|
desc: "使用 DeepL API 時請在主螢幕選擇 {{translator}}。※可能不支援某些語言。"
|
||||||
|
open_auth_key_webpage: "打開 DeepL 帳號頁面"
|
||||||
|
save: "儲存"
|
||||||
|
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: "喇叭"
|
||||||
|
section_label_transcription_engines: "語音辨識引擎"
|
||||||
|
mic_record_timeout:
|
||||||
|
label: "麥克風音訊 - 判定結束時間"
|
||||||
|
desc: "麥克風未收到音訊後,結束一段話的判定時間(秒)。"
|
||||||
|
mic_phrase_timeout:
|
||||||
|
label: "麥克風音訊 - 紀錄間隔時間"
|
||||||
|
desc: "每隔多久要紀錄一次音訊。"
|
||||||
|
mic_max_phrase:
|
||||||
|
label: "麥克風音訊 - 最大單詞數量"
|
||||||
|
desc: "只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。"
|
||||||
|
mic_word_filter:
|
||||||
|
label: "麥克風單詞過濾器"
|
||||||
|
desc: "如果偵測到清單內的單詞,則不會發送訊息。要一次新增多個詞語,請用「,」(半形逗號)分隔。\n*重複詞語會被忽略。"
|
||||||
|
add_button_label: "新增"
|
||||||
|
count_desc: "當前註冊詞語數量:{{count}}"
|
||||||
|
speaker_record_timeout:
|
||||||
|
label: "喇叭音訊 - 判定結束時間"
|
||||||
|
desc: "偵測到靜音並在指定秒數後認為喇叭輸入已結束。(秒)"
|
||||||
|
speaker_phrase_timeout:
|
||||||
|
label: "喇叭音訊 - 紀錄間隔時間"
|
||||||
|
desc: "以指定秒數間隔進行轉錄處理。"
|
||||||
|
speaker_max_phrase:
|
||||||
|
label: "喇叭音訊 - 最大單詞數量"
|
||||||
|
desc: "只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。"
|
||||||
|
select_transcription_engine:
|
||||||
|
label: "語音辨識使用的引擎"
|
||||||
|
whisper_weight_type:
|
||||||
|
label: "Whisper 模型類型"
|
||||||
|
desc: "容量越大的模型精度越高,但會占用更多 CPU/GPU 資源。\n※特別是比 medium 更大的模型,根據 CPU/GPU 效能可能難以使用。"
|
||||||
|
model_template: "{{model_name}}模型({{capacity}})"
|
||||||
|
recommended_model_template: "{{model_name}}模型({{capacity}})(推薦)"
|
||||||
|
transcription_compute_device:
|
||||||
|
label: "AI 語音辨識使用的處理裝置"
|
||||||
|
|
||||||
|
vr:
|
||||||
|
single_line: "單行"
|
||||||
|
multi_lines: "多行"
|
||||||
|
overlay_enable: "啟用"
|
||||||
|
restore_default_settings: "恢復預設設定"
|
||||||
|
position: "位置"
|
||||||
|
rotation: "旋轉"
|
||||||
|
x_position: "X軸(左右)"
|
||||||
|
y_position: "Y軸(上下)"
|
||||||
|
z_position: "Z軸(前後)"
|
||||||
|
x_rotation: "X軸旋轉"
|
||||||
|
y_rotation: "Y軸旋轉"
|
||||||
|
z_rotation: "Z軸旋轉"
|
||||||
|
sample_text_button:
|
||||||
|
start: "向 Overlay\n發送範例文字"
|
||||||
|
stop: "停止發送"
|
||||||
|
sample_text: "範例文字"
|
||||||
|
opacity: "透明度"
|
||||||
|
ui_scaling: "介面縮放"
|
||||||
|
display_duration: "顯示持續時間"
|
||||||
|
fadeout_duration: "淡出持續時間"
|
||||||
|
common_settings: "通用設定"
|
||||||
|
tracker: "顯示追蹤器的位置"
|
||||||
|
hmd: "HMD"
|
||||||
|
left_hand: "左手"
|
||||||
|
right_hand: "右手"
|
||||||
|
overlay_show_only_translated_messages:
|
||||||
|
label: "僅顯示翻譯後的訊息"
|
||||||
|
voice_typing_mode:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
|
||||||
|
others:
|
||||||
|
section_label_sounds: "音效"
|
||||||
|
section_label_message_formats: "訊息格式"
|
||||||
|
auto_clear_the_message_box:
|
||||||
|
label: "自動清除 Chatbox"
|
||||||
|
send_only_translated_messages:
|
||||||
|
label: "僅發送翻譯訊息"
|
||||||
|
auto_export_message_logs:
|
||||||
|
label: "自動匯出訊息記錄"
|
||||||
|
desc: "自動將對話訊息匯出為文字文件。"
|
||||||
|
vrc_mic_mute_sync:
|
||||||
|
label: "VRC 麥克風靜音同步"
|
||||||
|
desc: "當 VRChat 的麥克風靜音時,VRCT 將不會向 VRChat 發送訊息。\n*存在一些延遲且不支援按鍵發話 (PTT)。"
|
||||||
|
send_message_to_vrc:
|
||||||
|
label: "發送訊息到 VRChat"
|
||||||
|
desc: "當你打算向 VRChat 發送訊息時啟用此功能。"
|
||||||
|
notification_vrc_sfx:
|
||||||
|
label: "發送聊天時播放聊天框的通知音"
|
||||||
|
desc: "停用此功能後,將不會播放其他人能聽到的聊天框通知音,靜默發送。"
|
||||||
|
send_received_message_to_vrc:
|
||||||
|
label: "將接收到的訊息發送到 VRChat"
|
||||||
|
desc: "將從喇叭聽到並轉錄的訊息發送到 VRChat。"
|
||||||
|
message_format_common:
|
||||||
|
example_view:
|
||||||
|
title: "預覽"
|
||||||
|
original_translated: "原文 + 翻譯"
|
||||||
|
original_translated_multi: "原文 + 翻譯(多語言)"
|
||||||
|
translated_only_multi: "僅翻譯(多語言)"
|
||||||
|
translated_only: "僅翻譯"
|
||||||
|
original_only: "僅原文"
|
||||||
|
settings:
|
||||||
|
title: "設定"
|
||||||
|
original: "原文"
|
||||||
|
translated: "翻譯"
|
||||||
|
for_multi_translation: "多語言翻譯用"
|
||||||
|
send_message_format:
|
||||||
|
label: "訊息格式(發送)"
|
||||||
|
desc: "可以變更在 VRChat 中對方實際看到的格式。"
|
||||||
|
received_message_format:
|
||||||
|
label: "訊息格式(Speaker2Chatbox)"
|
||||||
|
desc: "目前用於透過 Speaker2Chatbox 發送時的顯示。"
|
||||||
|
common_convert_message_hiragana_romaji:
|
||||||
|
desc_1: "僅在選擇日語作為翻譯語言時支援。"
|
||||||
|
desc_2: "由於日語的特殊性,精度有限。"
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: "顯示羅馬字"
|
||||||
|
desc: "與「{{convert_message_to_hiragana}}」同時啟用時,將透過滑鼠懸停顯示。"
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: "顯示平假名"
|
||||||
|
telemetry:
|
||||||
|
label:
|
||||||
|
desc:
|
||||||
|
aptabase_privacy_policy_label:
|
||||||
|
|
||||||
|
hotkeys:
|
||||||
|
toggle_vrct_visibility:
|
||||||
|
label: "切換 VRCT 最小化/啟動"
|
||||||
|
toggle_translation:
|
||||||
|
label: "切換「{{translation}}」開/關"
|
||||||
|
toggle_transcription_send:
|
||||||
|
label: "切換「{{transcription_send}}」開/關"
|
||||||
|
toggle_transcription_receive:
|
||||||
|
label: "切換「{{transcription_receive}}」開/關"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "已下載版本:{{downloaded_version}}"
|
||||||
|
latest_version: "最新版本:{{latest_version}}"
|
||||||
|
available_after_updating: "更新到最新版後可用"
|
||||||
|
unavailable_downloaded: "目前不可用 與使用中的 VRCT 版本不相容"
|
||||||
|
no_latest_info: "無法取得最新資訊"
|
||||||
|
using_latest_version: "正在使用最新版本"
|
||||||
|
available_latest_version: "最新版本可用"
|
||||||
|
unavailable_latest_version: "最新版本目前不可用"
|
||||||
|
available_in_latest_vrct_version: "可在 VRCT 最新版中使用"
|
||||||
|
unavailable_not_downloaded: "目前不可用"
|
||||||
|
|
||||||
|
advanced_settings:
|
||||||
|
osc_ip_address:
|
||||||
|
label: "OSC IP 位址"
|
||||||
|
osc_port:
|
||||||
|
label: "OSC 端口"
|
||||||
|
open_config_filepath:
|
||||||
|
label: "打開設定文件"
|
||||||
|
switch_compute_device:
|
||||||
|
label: "切換 VRCT CPU/GPU 版本"
|
||||||
|
enable_websocket:
|
||||||
|
label: "啟用 WebSocket 伺服器"
|
||||||
|
websocket_host:
|
||||||
|
label: "WebSocket 主機"
|
||||||
|
websocket_port:
|
||||||
|
label: "WebSocket 連接埠"
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
save_success: "設定已儲存。"
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: "正在下載外掛程式。"
|
||||||
|
downloaded_success: "外掛程式下載完成。"
|
||||||
|
downloaded_error: "外掛程式下載失敗。"
|
||||||
|
|
||||||
|
updating: "正在更新外掛程式。"
|
||||||
|
updated_success: "外掛程式更新完成。"
|
||||||
|
updated_error: "外掛程式更新失敗。"
|
||||||
|
|
||||||
|
disabled_out_of_support: "與目前版本不相容。已停用外掛程式。"
|
||||||
|
disabled_due_to_an_error: "外掛程式執行時偵測到錯誤。請向外掛程式開發者回報。"
|
||||||
|
|
||||||
|
is_enabled: "外掛程式已啟用。"
|
||||||
|
is_disabled: "外掛程式已停用。"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// 使用例: node map-stack.js
|
||||||
|
// カレントディレクトリの error.txt を読み込み、各エラーフレームから
|
||||||
|
// 対応するソースマップ(dist/assets/ 以下の *.js.map)を用いて元の位置情報を出力する
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { SourceMapConsumer } from "source-map";
|
||||||
|
|
||||||
|
// 各スタックフレームにマッチする正規表現
|
||||||
|
const FRAME_REGEX = /^\s*at\s+(.*?)\s+\((.*):(\d+):(\d+)\)$/;
|
||||||
|
|
||||||
|
// スタックトレースのパース関数
|
||||||
|
const parseStackTrace = (text) => {
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => {
|
||||||
|
const match = line.match(FRAME_REGEX);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
original: line,
|
||||||
|
functionName: match[1],
|
||||||
|
file: match[2],
|
||||||
|
line: Number(match[3]),
|
||||||
|
column: Number(match[4])
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { original: line };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// エラースタックをソースマップから逆引きする関数(位置情報のみを表示)
|
||||||
|
const mapStackTrace = async () => {
|
||||||
|
const errorTxtPath = path.resolve(process.cwd(), "error.txt");
|
||||||
|
const stackTraceText = fs.readFileSync(errorTxtPath, "utf8");
|
||||||
|
const frames = parseStackTrace(stackTraceText);
|
||||||
|
|
||||||
|
const consumerMap = new Map();
|
||||||
|
|
||||||
|
const mappedFrames = await Promise.all(frames.map(async (frame) => {
|
||||||
|
if (frame.file && frame.line && frame.column) {
|
||||||
|
const relativeFile = frame.file.replace(/^\//, ""); // 例: "assets/main-Td8-sruo.js"
|
||||||
|
const mapFilePath = path.resolve(process.cwd(), "dist", relativeFile + ".map");
|
||||||
|
let consumer = consumerMap.get(mapFilePath);
|
||||||
|
if (!consumer) {
|
||||||
|
const rawSourceMap = fs.readFileSync(mapFilePath, "utf8");
|
||||||
|
consumer = await new SourceMapConsumer(rawSourceMap);
|
||||||
|
consumerMap.set(mapFilePath, consumer);
|
||||||
|
}
|
||||||
|
const pos = consumer.originalPositionFor({
|
||||||
|
line: frame.line,
|
||||||
|
column: frame.column
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pos && pos.source && pos.line != null && pos.column != null) {
|
||||||
|
return ` at ${frame.functionName} (${pos.source}:${pos.line}:${pos.column})`;
|
||||||
|
} else {
|
||||||
|
return frame.original;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return frame.original;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
consumerMap.forEach((consumer) => consumer.destroy());
|
||||||
|
return mappedFrames.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
mapStackTrace()
|
||||||
|
.then((mapped) => {
|
||||||
|
console.log("--- Mapped Stack Trace ---");
|
||||||
|
console.log(mapped);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"name": "vrct",
|
||||||
|
"private": true,
|
||||||
|
"version": "3.4.2",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"setup-python": "bat\\install.bat",
|
||||||
|
"build-python": "bat\\build.bat",
|
||||||
|
"build-python-cuda": "bat\\build_cuda.bat",
|
||||||
|
"vite": "vite",
|
||||||
|
"vite-build": "vite build",
|
||||||
|
"vite-preview": "vite preview",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"tauri-dev": "tauri dev",
|
||||||
|
"task-kill": "python utils\\task_kill.py",
|
||||||
|
"clean": "python utils\\clean.py",
|
||||||
|
"update-version": "python utils\\update_version.py",
|
||||||
|
"dev": "npm run task-kill && npm run clean && npm run update-version && npm run build-python && npm run dev-ui",
|
||||||
|
"dev-cuda": "npm run task-kill && npm run clean && npm run update-version && npm run build-python-cuda && npm run dev-ui",
|
||||||
|
"dev-ui": "npm run task-kill && npm-run-all --parallel vite tauri-dev",
|
||||||
|
"build": "npm run task-kill && npm run clean && npm run update-version && npm run build-python && npm run vite-build && npm run tauri build",
|
||||||
|
"build-cuda": "npm run task-kill && npm run clean && npm run update-version && npm run build-python-cuda && npm run vite-build && npm run tauri build",
|
||||||
|
"release": "npm run update-version && npm run build && python utils\\zip.py --zip_name VRCT.zip",
|
||||||
|
"release-cuda": "npm run update-version && npm run build-cuda && python utils\\zip.py --zip_name VRCT_cuda.zip",
|
||||||
|
"release-all": "npm run update-version && npm run release && npm run release-cuda"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/standalone": "7.27.0",
|
||||||
|
"@emotion/react": "11.14.0",
|
||||||
|
"@emotion/styled": "11.14.0",
|
||||||
|
"@mui/material": "7.0.2",
|
||||||
|
"@tauri-apps/api": "2.5.0",
|
||||||
|
"@tauri-apps/plugin-fs": "2.2.1",
|
||||||
|
"@tauri-apps/plugin-global-shortcut": "2.2.0",
|
||||||
|
"@tauri-apps/plugin-http": "2.4.3",
|
||||||
|
"@tauri-apps/plugin-opener": "2.2.6",
|
||||||
|
"@tauri-apps/plugin-shell": "2.2.1",
|
||||||
|
"clsx": "2.1.1",
|
||||||
|
"i18next": "25.0.1",
|
||||||
|
"jotai": "2.12.3",
|
||||||
|
"js-base64": "3.7.7",
|
||||||
|
"jszip": "3.10.1",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-error-boundary": "5.0.0",
|
||||||
|
"react-i18next": "15.5.1",
|
||||||
|
"react-resizable-layout": "0.7.2",
|
||||||
|
"react-toastify": "11.0.5",
|
||||||
|
"sass": "1.79.4",
|
||||||
|
"semver": "7.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-yaml": "4.1.2",
|
||||||
|
"@tauri-apps/cli": "2.5.0",
|
||||||
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
|
"eslint": "9.25.1",
|
||||||
|
"eslint-plugin-react": "7.37.5",
|
||||||
|
"npm-run-all": "4.1.5",
|
||||||
|
"source-map": "0.7.4",
|
||||||
|
"vite": "6.3.4",
|
||||||
|
"vite-plugin-svgr": "4.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
torch==2.7.0
|
||||||
|
faster-whisper==1.1.1
|
||||||
|
ctranslate2==4.6.0
|
||||||
|
transformers==4.40.2
|
||||||
|
pillow == 10.0.0
|
||||||
|
PyAudioWPatch == 0.2.12.6
|
||||||
|
python-osc == 1.9.0
|
||||||
|
PyYAML==6.0.2
|
||||||
|
deepl == 1.22.0
|
||||||
|
flashtext ==2.7
|
||||||
|
pyinstaller==6.10.0
|
||||||
|
numpy==1.26.4
|
||||||
|
sentencepiece==0.2.0
|
||||||
|
openvr==1.26.701
|
||||||
|
pydub==0.25.1
|
||||||
|
psutil==5.9.8
|
||||||
|
pycaw==20240210
|
||||||
|
websockets==15.0.1
|
||||||
|
huggingface_hub==0.32.2
|
||||||
|
hf-xet==1.1.2
|
||||||
|
setuptools==80.8.0
|
||||||
|
langchain-openai==0.3.32
|
||||||
|
langchain-google-genai==2.1.10
|
||||||
|
google-genai==1.45.0
|
||||||
|
grpcio==1.67.1
|
||||||
|
langchain-ollama==0.3.10
|
||||||
|
SudachiPy==0.6.10
|
||||||
|
SudachiDict-core==20250825
|
||||||
|
SudachiDict-full==20250825
|
||||||
|
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
|
||||||
|
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
|
||||||
|
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3
|
||||||
|
aptabase==0.1.0
|
||||||
|
PyAutoGUI==0.9.54
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
torch==2.7.0
|
||||||
|
--extra-index-url https://download.pytorch.org/whl/cu128
|
||||||
|
faster-whisper==1.1.1
|
||||||
|
ctranslate2==4.6.0
|
||||||
|
transformers==4.40.2
|
||||||
|
pillow == 10.0.0
|
||||||
|
PyAudioWPatch == 0.2.12.6
|
||||||
|
python-osc == 1.9.0
|
||||||
|
PyYAML==6.0.2
|
||||||
|
deepl == 1.22.0
|
||||||
|
flashtext ==2.7
|
||||||
|
pyinstaller==6.10.0
|
||||||
|
numpy==1.26.4
|
||||||
|
sentencepiece==0.2.0
|
||||||
|
openvr==1.26.701
|
||||||
|
pydub==0.25.1
|
||||||
|
psutil==5.9.8
|
||||||
|
pycaw==20240210
|
||||||
|
websockets==15.0.1
|
||||||
|
huggingface_hub==0.32.2
|
||||||
|
hf-xet==1.1.2
|
||||||
|
setuptools==80.8.0
|
||||||
|
langchain-openai==0.3.32
|
||||||
|
langchain-google-genai==2.1.10
|
||||||
|
google-genai==1.45.0
|
||||||
|
grpcio==1.67.1
|
||||||
|
langchain-ollama==0.3.10
|
||||||
|
SudachiPy==0.6.10
|
||||||
|
SudachiDict-core==20250825
|
||||||
|
SudachiDict-full==20250825
|
||||||
|
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
|
||||||
|
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
|
||||||
|
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3
|
||||||
|
aptabase==0.1.0
|
||||||
|
PyAutoGUI==0.9.54
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['..\\src-python\\mainloop.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
('./../src-python/models/overlay/fonts', 'fonts/'),
|
||||||
|
('./../src-python/models/translation/translation_settings/prompt', 'translation_settings/prompt/'),
|
||||||
|
('./../src-python/models/translation/translation_settings/languages', 'translation_settings/languages/'),
|
||||||
|
('./../.venv/Lib/site-packages/zeroconf', 'zeroconf/'),
|
||||||
|
('./../.venv/Lib/site-packages/openvr', 'openvr/'),
|
||||||
|
('./../.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'),
|
||||||
|
('./../.venv/Lib/site-packages/hf_xet', 'hf_xet/')
|
||||||
|
],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=['pandas', 'matplotlib', 'PyQt5'],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='VRCT-sidecar-x86_64-pc-windows-msvc',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=[],
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='.',
|
||||||
|
)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['..\\src-python\\mainloop.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
('./../src-python/models/overlay/fonts', 'fonts/'),
|
||||||
|
('./../src-python/models/translation/translation_settings/prompt', 'translation_settings/prompt/'),
|
||||||
|
('./../src-python/models/translation/translation_settings/languages', 'translation_settings/languages/'),
|
||||||
|
('./../.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'),
|
||||||
|
('./../.venv_cuda/Lib/site-packages/openvr', 'openvr/'),
|
||||||
|
('./../.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'),
|
||||||
|
('./../.venv/Lib/site-packages/hf_xet', 'hf_xet/')
|
||||||
|
],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=['pandas', 'matplotlib', 'PyQt5'],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='VRCT-sidecar-x86_64-pc-windows-msvc',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=[],
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='.',
|
||||||
|
)
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
from typing import Callable, Dict, List, Optional, Any
|
||||||
|
from time import sleep
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
# Optional, Windows-specific dependencies. Guard imports so module can be imported on non-Windows systems.
|
||||||
|
try:
|
||||||
|
import comtypes
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
comtypes = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyaudiowpatch import PyAudio, paWASAPI
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
PyAudio = None # type: ignore
|
||||||
|
paWASAPI = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pycaw.callbacks import MMNotificationClient
|
||||||
|
from pycaw.utils import AudioUtilities
|
||||||
|
except Exception: # pragma: no cover - optional runtime
|
||||||
|
MMNotificationClient = object # type: ignore
|
||||||
|
AudioUtilities = None # type: ignore
|
||||||
|
|
||||||
|
from utils import errorLogging
|
||||||
|
|
||||||
|
class Client(MMNotificationClient):
|
||||||
|
"""Callback client used by pycaw to detect device changes.
|
||||||
|
|
||||||
|
This subclass is lightweight: it flips a flag when events arrive so the
|
||||||
|
monitoring loop can break and refresh device lists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# If MMNotificationClient is the placeholder object (non-windows), avoid calling super
|
||||||
|
try:
|
||||||
|
super().__init__()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.loop: bool = True
|
||||||
|
|
||||||
|
def on_default_device_changed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.loop = False
|
||||||
|
|
||||||
|
def on_device_added(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.loop = False
|
||||||
|
|
||||||
|
def on_device_removed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.loop = False
|
||||||
|
|
||||||
|
def on_device_state_changed(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.loop = False
|
||||||
|
|
||||||
|
# def on_property_value_changed(self, device_id, key):
|
||||||
|
# self.loop = False
|
||||||
|
|
||||||
|
class DeviceManager:
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super(DeviceManager, cls).__new__(cls)
|
||||||
|
# do NOT auto-init monitoring-heavy resources on import; require explicit init
|
||||||
|
# Still perform a light-weight init so that callers observing the singleton
|
||||||
|
# do not see uninitialized internal structures (which caused NoDevice to
|
||||||
|
# be seen when import order differed).
|
||||||
|
cls._instance._initialized = False
|
||||||
|
try:
|
||||||
|
# Call init() to populate internal containers. This will NOT start
|
||||||
|
# the monitoring thread (startMonitoring must be called explicitly).
|
||||||
|
cls._instance.init()
|
||||||
|
except Exception:
|
||||||
|
# Avoid import-time crashes; log and continue.
|
||||||
|
try:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
"""Initialize internal state. This is intentionally separate from object
|
||||||
|
creation so importing the module won't start threads or access OS
|
||||||
|
audio APIs. Call `device_manager.init()` and then
|
||||||
|
`device_manager.startMonitoring()` explicitly when ready.
|
||||||
|
"""
|
||||||
|
if getattr(self, "_initialized", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.mic_devices: Dict[str, List[Dict[str, Any]]] = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
|
self.default_mic_device: Dict[str, Any] = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
self.speaker_devices: List[Dict[str, Any]] = [{"index": -1, "name": "NoDevice"}]
|
||||||
|
self.default_speaker_device: Dict[str, Any] = {"device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
|
||||||
|
# Initialize previous state trackers
|
||||||
|
self.prev_mic_host: List[str] = [host for host in self.mic_devices]
|
||||||
|
self.prev_mic_devices: Dict[str, List[Dict[str, Any]]] = self.mic_devices
|
||||||
|
self.prev_default_mic_device: Dict[str, Any] = self.default_mic_device
|
||||||
|
self.prev_speaker_devices: List[Dict[str, Any]] = self.speaker_devices
|
||||||
|
self.prev_default_speaker_device: Dict[str, Any] = self.default_speaker_device
|
||||||
|
|
||||||
|
# Update flags
|
||||||
|
self.update_flag_default_mic_device: bool = False
|
||||||
|
self.update_flag_default_speaker_device: bool = False
|
||||||
|
self.update_flag_host_list: bool = False
|
||||||
|
self.update_flag_mic_device_list: bool = False
|
||||||
|
self.update_flag_speaker_device_list: bool = False
|
||||||
|
|
||||||
|
# Callbacks
|
||||||
|
self.callback_default_mic_device: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_default_speaker_device: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_host_list: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_mic_device_list: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_speaker_device_list: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_before_update_mic_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_after_update_mic_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_before_update_speaker_devices: Optional[Callable[..., None]] = None
|
||||||
|
self.callback_process_after_update_speaker_devices: Optional[Callable[..., None]] = None
|
||||||
|
|
||||||
|
# Monitoring control
|
||||||
|
self.monitoring_flag: bool = False
|
||||||
|
self.th_monitoring: Optional[Thread] = None
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
# Best-effort single update: if PyAudio is available, attempt to populate
|
||||||
|
# real device lists. Keep this short and ignore errors to avoid import-time
|
||||||
|
# failures.
|
||||||
|
try:
|
||||||
|
if PyAudio is not None:
|
||||||
|
try:
|
||||||
|
# update() is robust and will fall back to defaults if audio libs
|
||||||
|
# are missing or fail; do not let exceptions bubble up.
|
||||||
|
self.update()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
# defensive: if errorLogging isn't available or other issues occur,
|
||||||
|
# swallow to avoid breaking initialization
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
buffer_mic_devices: Dict[str, List[Dict[str, Any]]] = {}
|
||||||
|
buffer_default_mic_device: Dict[str, Any] = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
buffer_speaker_devices: List[Dict[str, Any]] = []
|
||||||
|
buffer_default_speaker_device: Dict[str, Any] = {"device": {"index": -1, "name": "NoDevice"}}
|
||||||
|
|
||||||
|
if PyAudio is None:
|
||||||
|
# PyAudio not available; leave defaults in place
|
||||||
|
self.mic_devices = buffer_mic_devices or {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
|
self.default_mic_device = buffer_default_mic_device
|
||||||
|
self.speaker_devices = buffer_speaker_devices or [{"index": -1, "name": "NoDevice"}]
|
||||||
|
self.default_speaker_device = buffer_default_speaker_device
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with PyAudio() as p:
|
||||||
|
# gather input devices grouped by host
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
|
device_count = host.get('deviceCount', 0)
|
||||||
|
for device_index in range(device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if device.get("maxInputChannels", 0) > 0 and not device.get("isLoopbackDevice", True):
|
||||||
|
buffer_mic_devices.setdefault(host["name"], []).append(device)
|
||||||
|
if not buffer_mic_devices:
|
||||||
|
buffer_mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
|
||||||
|
|
||||||
|
api_info = p.get_default_host_api_info()
|
||||||
|
default_mic_device = api_info.get("defaultInputDevice", -1)
|
||||||
|
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
|
device_count = host.get('deviceCount', 0)
|
||||||
|
for device_index in range(device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if device.get("index") == default_mic_device:
|
||||||
|
buffer_default_mic_device = {"host": host, "device": device}
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
# collect speaker loopback devices (requires WASAPI)
|
||||||
|
speaker_devices: List[Dict[str, Any]] = []
|
||||||
|
if paWASAPI is not None:
|
||||||
|
try:
|
||||||
|
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
||||||
|
wasapi_name = wasapi_info.get("name")
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host = p.get_host_api_info_by_index(host_index)
|
||||||
|
if host.get("name") == wasapi_name:
|
||||||
|
device_count = host.get('deviceCount', 0)
|
||||||
|
for device_index in range(device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if not device.get("isLoopbackDevice", True):
|
||||||
|
for loopback in p.get_loopback_device_info_generator():
|
||||||
|
# match by name inclusion
|
||||||
|
if device.get("name") in loopback.get("name", ""):
|
||||||
|
speaker_devices.append(loopback)
|
||||||
|
except Exception:
|
||||||
|
# WASAPI not available or failed; ignore and continue
|
||||||
|
pass
|
||||||
|
|
||||||
|
# deduplicate and sort
|
||||||
|
speaker_devices = [dict(t) for t in {tuple(d.items()) for d in speaker_devices}] or [{"index": -1, "name": "NoDevice"}]
|
||||||
|
buffer_speaker_devices = sorted(speaker_devices, key=lambda d: d.get('index', -1))
|
||||||
|
|
||||||
|
# default speaker
|
||||||
|
if paWASAPI is not None:
|
||||||
|
try:
|
||||||
|
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
|
||||||
|
default_speaker_device_index = wasapi_info.get("defaultOutputDevice", -1)
|
||||||
|
for host_index in range(p.get_host_api_count()):
|
||||||
|
host_info = p.get_host_api_info_by_index(host_index)
|
||||||
|
device_count = host_info.get('deviceCount', 0)
|
||||||
|
for device_index in range(0, device_count):
|
||||||
|
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
|
||||||
|
if device.get("index") == default_speaker_device_index:
|
||||||
|
default_speakers = device
|
||||||
|
if not default_speakers.get("isLoopbackDevice", True):
|
||||||
|
for loopback in p.get_loopback_device_info_generator():
|
||||||
|
if default_speakers.get("name") in loopback.get("name", ""):
|
||||||
|
buffer_default_speaker_device = {"device": loopback}
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
if buffer_default_speaker_device["device"].get("name") != "NoDevice":
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
# best-effort; ignore failures
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
self.mic_devices = buffer_mic_devices
|
||||||
|
self.default_mic_device = buffer_default_mic_device
|
||||||
|
self.speaker_devices = buffer_speaker_devices
|
||||||
|
self.default_speaker_device = buffer_default_speaker_device
|
||||||
|
|
||||||
|
def checkUpdate(self):
|
||||||
|
if self.prev_default_mic_device["device"]["name"] != self.default_mic_device["device"]["name"]:
|
||||||
|
self.update_flag_default_mic_device = True
|
||||||
|
self.prev_default_mic_device = self.default_mic_device
|
||||||
|
if self.prev_default_speaker_device["device"]["name"] != self.default_speaker_device["device"]["name"]:
|
||||||
|
self.update_flag_default_speaker_device = True
|
||||||
|
self.prev_default_speaker_device = self.default_speaker_device
|
||||||
|
if self.prev_mic_host != [host for host in self.mic_devices]:
|
||||||
|
self.update_flag_host_list = True
|
||||||
|
self.prev_mic_host = [host for host in self.mic_devices]
|
||||||
|
if {key: [device['name'] for device in devices] for key, devices in self.prev_mic_devices.items()} != {key: [device['name'] for device in devices] for key, devices in self.mic_devices.items()}:
|
||||||
|
self.update_flag_mic_device_list = True
|
||||||
|
self.prev_mic_devices = self.mic_devices
|
||||||
|
if [device['name'] for device in self.prev_speaker_devices] != [device['name'] for device in self.speaker_devices]:
|
||||||
|
self.update_flag_speaker_device_list = True
|
||||||
|
self.prev_speaker_devices = self.speaker_devices
|
||||||
|
|
||||||
|
update_flag = (
|
||||||
|
self.update_flag_default_mic_device or
|
||||||
|
self.update_flag_default_speaker_device or
|
||||||
|
self.update_flag_host_list or
|
||||||
|
self.update_flag_mic_device_list or
|
||||||
|
self.update_flag_speaker_device_list
|
||||||
|
)
|
||||||
|
return update_flag
|
||||||
|
|
||||||
|
def monitoring(self):
|
||||||
|
try:
|
||||||
|
while self.monitoring_flag is True:
|
||||||
|
try:
|
||||||
|
# Use COM only when available (Windows). If comtypes is not present,
|
||||||
|
# fall back to periodic polling using PyAudio only.
|
||||||
|
if comtypes is not None and AudioUtilities is not None:
|
||||||
|
try:
|
||||||
|
comtypes.CoInitialize()
|
||||||
|
cb = Client()
|
||||||
|
enumerator = AudioUtilities.GetDeviceEnumerator()
|
||||||
|
enumerator.RegisterEndpointNotificationCallback(cb)
|
||||||
|
while cb.loop is True and self.monitoring_flag is True:
|
||||||
|
sleep(1)
|
||||||
|
try:
|
||||||
|
enumerator.UnregisterEndpointNotificationCallback(cb)
|
||||||
|
except Exception:
|
||||||
|
# best-effort unregister
|
||||||
|
pass
|
||||||
|
comtypes.CoUninitialize()
|
||||||
|
except Exception:
|
||||||
|
# if COM monitoring fails, log and fall through to polling
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
# polling and update cycle
|
||||||
|
self.runProcessBeforeUpdateMicDevices()
|
||||||
|
self.runProcessBeforeUpdateSpeakerDevices()
|
||||||
|
sleep(2)
|
||||||
|
for _ in range(10):
|
||||||
|
self.update()
|
||||||
|
if self.checkUpdate():
|
||||||
|
break
|
||||||
|
sleep(2)
|
||||||
|
self.noticeUpdateDevices()
|
||||||
|
self.runProcessAfterUpdateMicDevices()
|
||||||
|
self.runProcessAfterUpdateSpeakerDevices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def startMonitoring(self):
|
||||||
|
if self.monitoring_flag:
|
||||||
|
return
|
||||||
|
self.monitoring_flag = True
|
||||||
|
self.th_monitoring = Thread(target=self.monitoring)
|
||||||
|
self.th_monitoring.daemon = True
|
||||||
|
self.th_monitoring.start()
|
||||||
|
|
||||||
|
def stopMonitoring(self):
|
||||||
|
self.monitoring_flag = False
|
||||||
|
if getattr(self, "th_monitoring", None) is not None:
|
||||||
|
try:
|
||||||
|
self.th_monitoring.join(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
# If join fails or thread is not joinable, ignore - it's a best-effort stop
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setCallbackDefaultMicDevice(self, callback):
|
||||||
|
self.callback_default_mic_device = callback
|
||||||
|
|
||||||
|
def clearCallbackDefaultMicDevice(self):
|
||||||
|
self.callback_default_mic_device = None
|
||||||
|
|
||||||
|
def setCallbackDefaultSpeakerDevice(self, callback):
|
||||||
|
self.callback_default_speaker_device = callback
|
||||||
|
|
||||||
|
def clearCallbackDefaultSpeakerDevice(self):
|
||||||
|
self.callback_default_speaker_device = None
|
||||||
|
|
||||||
|
def setCallbackHostList(self, callback):
|
||||||
|
self.callback_host_list = callback
|
||||||
|
|
||||||
|
def clearCallbackHostList(self):
|
||||||
|
self.callback_host_list = None
|
||||||
|
|
||||||
|
def setCallbackMicDeviceList(self, callback):
|
||||||
|
self.callback_mic_device_list = callback
|
||||||
|
|
||||||
|
def clearCallbackMicDeviceList(self):
|
||||||
|
self.callback_mic_device_list = None
|
||||||
|
|
||||||
|
def setCallbackSpeakerDeviceList(self, callback):
|
||||||
|
self.callback_speaker_device_list = callback
|
||||||
|
|
||||||
|
def clearCallbackSpeakerDeviceList(self):
|
||||||
|
self.callback_speaker_device_list = None
|
||||||
|
|
||||||
|
def setCallbackProcessBeforeUpdateMicDevices(self, callback):
|
||||||
|
self.callback_process_before_update_mic_devices = callback
|
||||||
|
|
||||||
|
def clearCallbackProcessBeforeUpdateMicDevices(self):
|
||||||
|
self.callback_process_before_update_mic_devices = None
|
||||||
|
|
||||||
|
def runProcessBeforeUpdateMicDevices(self):
|
||||||
|
if isinstance(self.callback_process_before_update_mic_devices, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_process_before_update_mic_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setCallbackProcessAfterUpdateMicDevices(self, callback):
|
||||||
|
self.callback_process_after_update_mic_devices = callback
|
||||||
|
|
||||||
|
def clearCallbackProcessAfterUpdateMicDevices(self):
|
||||||
|
self.callback_process_after_update_mic_devices = None
|
||||||
|
|
||||||
|
def runProcessAfterUpdateMicDevices(self):
|
||||||
|
if isinstance(self.callback_process_after_update_mic_devices, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_process_after_update_mic_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setCallbackProcessBeforeUpdateSpeakerDevices(self, callback):
|
||||||
|
self.callback_process_before_update_speaker_devices = callback
|
||||||
|
|
||||||
|
def clearCallbackProcessBeforeUpdateSpeakerDevices(self):
|
||||||
|
self.callback_process_before_update_speaker_devices = None
|
||||||
|
|
||||||
|
def runProcessBeforeUpdateSpeakerDevices(self):
|
||||||
|
if isinstance(self.callback_process_before_update_speaker_devices, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_process_before_update_speaker_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setCallbackProcessAfterUpdateSpeakerDevices(self, callback):
|
||||||
|
self.callback_process_after_update_speaker_devices = callback
|
||||||
|
|
||||||
|
def clearCallbackProcessAfterUpdateSpeakerDevices(self):
|
||||||
|
self.callback_process_after_update_speaker_devices = None
|
||||||
|
|
||||||
|
def runProcessAfterUpdateSpeakerDevices(self):
|
||||||
|
if isinstance(self.callback_process_after_update_speaker_devices, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_process_after_update_speaker_devices()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def noticeUpdateDevices(self):
|
||||||
|
if self.update_flag_default_mic_device is True:
|
||||||
|
self.setMicDefaultDevice()
|
||||||
|
if self.update_flag_default_speaker_device is True:
|
||||||
|
self.setSpeakerDefaultDevice()
|
||||||
|
if self.update_flag_host_list is True:
|
||||||
|
self.setMicHostList()
|
||||||
|
if self.update_flag_mic_device_list is True:
|
||||||
|
self.setMicDeviceList()
|
||||||
|
if self.update_flag_speaker_device_list is True:
|
||||||
|
self.setSpeakerDeviceList()
|
||||||
|
|
||||||
|
self.update_flag_default_mic_device = False
|
||||||
|
self.update_flag_default_speaker_device = False
|
||||||
|
self.update_flag_host_list = False
|
||||||
|
self.update_flag_mic_device_list = False
|
||||||
|
self.update_flag_speaker_device_list = False
|
||||||
|
|
||||||
|
def setMicDefaultDevice(self):
|
||||||
|
if isinstance(self.callback_default_mic_device, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_default_mic_device(self.default_mic_device["host"]["name"], self.default_mic_device["device"]["name"])
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setSpeakerDefaultDevice(self):
|
||||||
|
if isinstance(self.callback_default_speaker_device, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_default_speaker_device(self.default_speaker_device["device"]["name"])
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setMicHostList(self):
|
||||||
|
if isinstance(self.callback_host_list, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_host_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setMicDeviceList(self):
|
||||||
|
if isinstance(self.callback_mic_device_list, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_mic_device_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def setSpeakerDeviceList(self):
|
||||||
|
if isinstance(self.callback_speaker_device_list, Callable):
|
||||||
|
try:
|
||||||
|
self.callback_speaker_device_list()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def getMicDevices(self):
|
||||||
|
# Ensure initialized and return devices (safe default if still not populated)
|
||||||
|
if not getattr(self, '_initialized', False):
|
||||||
|
try:
|
||||||
|
self.init()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(self, 'mic_devices', {"NoHost": [{"index": -1, "name": "NoDevice"}]})
|
||||||
|
|
||||||
|
def getDefaultMicDevice(self):
|
||||||
|
# Ensure initialized and return default mic device (safe default if still not populated)
|
||||||
|
if not getattr(self, '_initialized', False):
|
||||||
|
try:
|
||||||
|
self.init()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(self, 'default_mic_device', {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}})
|
||||||
|
|
||||||
|
def getSpeakerDevices(self):
|
||||||
|
# Ensure initialized and return speaker devices (safe default if still not populated)
|
||||||
|
if not getattr(self, '_initialized', False):
|
||||||
|
try:
|
||||||
|
self.init()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(self, 'speaker_devices', [{"index": -1, "name": "NoDevice"}])
|
||||||
|
|
||||||
|
def getDefaultSpeakerDevice(self):
|
||||||
|
# Ensure initialized and return default speaker device (safe default if still not populated)
|
||||||
|
if not getattr(self, '_initialized', False):
|
||||||
|
try:
|
||||||
|
self.init()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(self, 'default_speaker_device', {"device": {"index": -1, "name": "NoDevice"}})
|
||||||
|
|
||||||
|
def forceUpdateAndSetMicDevices(self):
|
||||||
|
self.update()
|
||||||
|
self.setMicHostList()
|
||||||
|
self.setMicDeviceList()
|
||||||
|
self.setMicDefaultDevice()
|
||||||
|
|
||||||
|
def forceUpdateAndSetSpeakerDevices(self):
|
||||||
|
self.update()
|
||||||
|
self.setSpeakerDeviceList()
|
||||||
|
self.setSpeakerDefaultDevice()
|
||||||
|
|
||||||
|
# Provide a module-level singleton. Call `device_manager.init()` explicitly to
|
||||||
|
# initialize audio resources and `device_manager.startMonitoring()` to begin
|
||||||
|
# background monitoring. This avoids side-effects during simple imports.
|
||||||
|
device_manager = DeviceManager()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("DeviceManager demo. Call device_manager.init() and device_manager.startMonitoring() to run live monitoring.")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("exiting")
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# config.py ドキュメント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
`config.py` は、アプリケーションの全設定を一元管理するシングルトンクラス `Config` を提供するモジュール。設定値の読み込み・保存・検証を行い、JSON ファイルへの永続化をデバウンス機能付きで実現する。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
- シングルトンパターンによる設定の一元管理
|
||||||
|
- JSONファイル (`config.json`) からの設定読み込みと自動保存
|
||||||
|
- デバウンス機能による書き込み最適化(デフォルト2秒)
|
||||||
|
- 読み取り専用プロパティと読み書き可能プロパティの明確な分離
|
||||||
|
- オプショナルモジュールのセーフガードインポート(環境依存の依存関係を安全に処理)
|
||||||
|
- プロパティセッター内での型チェックとバリデーション
|
||||||
|
- `@json_serializable` デコレータによる永続化対象プロパティの管理
|
||||||
|
|
||||||
|
## アーキテクチャ
|
||||||
|
|
||||||
|
### デザインパターン
|
||||||
|
- **シングルトンパターン**: `__new__` メソッドで単一インスタンスを保証
|
||||||
|
- **プロパティパターン**: getter/setter による型安全なアクセス制御
|
||||||
|
|
||||||
|
### 設定の分類
|
||||||
|
1. **読み取り専用設定** (Read Only)
|
||||||
|
- アプリケーションバージョン、パス、URL、定数など
|
||||||
|
- プロパティのみ(setter なし)
|
||||||
|
|
||||||
|
2. **ランタイム設定** (Read Write)
|
||||||
|
- 機能の有効/無効フラグ
|
||||||
|
- 実行時の状態管理
|
||||||
|
- JSON保存されない一時的な設定
|
||||||
|
|
||||||
|
3. **永続化設定** (Save Json Data)
|
||||||
|
- ユーザー設定、デバイス選択、UI設定など
|
||||||
|
- `@json_serializable` デコレータでマーク
|
||||||
|
- `saveConfig()` 経由で自動保存
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
# 設定値の取得(読み取り専用)
|
||||||
|
version = config.VERSION
|
||||||
|
app_path = config.PATH_LOCAL
|
||||||
|
|
||||||
|
# 設定値の取得(読み書き可能)
|
||||||
|
current_tab = config.SELECTED_TAB_NO
|
||||||
|
mic_threshold = config.MIC_THRESHOLD
|
||||||
|
|
||||||
|
# 設定値の変更(自動保存される)
|
||||||
|
config.SELECTED_TAB_NO = "2"
|
||||||
|
config.MIC_THRESHOLD = 500
|
||||||
|
config.TRANSPARENCY = 80
|
||||||
|
|
||||||
|
# 即座に保存する場合
|
||||||
|
config.MAIN_WINDOW_GEOMETRY = {"x_pos": 100, "y_pos": 200, "width": 900, "height": 700}
|
||||||
|
# MESSAGE_BOX_RATIO と MAIN_WINDOW_GEOMETRY は immediate_save=True で即座に保存
|
||||||
|
```
|
||||||
|
|
||||||
|
### デバウンス保存の仕組み
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 通常の設定変更: 2秒後に保存
|
||||||
|
config.UI_LANGUAGE = "ja"
|
||||||
|
config.FONT_FAMILY = "Arial" # 前のタイマーがキャンセルされ、新たに2秒のタイマー開始
|
||||||
|
|
||||||
|
# 即座保存が必要な設定: デバウンスなし
|
||||||
|
config.MESSAGE_BOX_RATIO = 15 # 即座にファイル書き込み
|
||||||
|
```
|
||||||
|
|
||||||
|
## 動作環境・依存関係
|
||||||
|
|
||||||
|
### 必須依存
|
||||||
|
- Python 3.10以上(match-case 構文使用)
|
||||||
|
- `torch`: CUDA利用可否の判定に使用
|
||||||
|
- `threading`: デバウンスタイマー用
|
||||||
|
|
||||||
|
### オプション依存(セーフガード付き)
|
||||||
|
以下のモジュールはインポートに失敗しても動作する:
|
||||||
|
- `device_manager`: デバイス管理(マイク/スピーカー)
|
||||||
|
- `models.translation.translation_languages`: 翻訳言語リスト
|
||||||
|
- `models.translation.translation_utils`: CTranslate2 重みリスト
|
||||||
|
- `models.transcription.transcription_languages`: 音声認識言語リスト
|
||||||
|
- `models.transcription.transcription_whisper`: Whisper モデルリスト
|
||||||
|
|
||||||
|
### プロジェクト内依存
|
||||||
|
- `utils`: エラーロギング、辞書構造検証、計算デバイスリスト取得
|
||||||
|
|
||||||
|
## ファイル構成
|
||||||
|
|
||||||
|
### 主要クラス: `Config`
|
||||||
|
|
||||||
|
#### クラス属性
|
||||||
|
```python
|
||||||
|
_instance: Config | None # シングルトンインスタンス
|
||||||
|
_config_data: Dict[str, Any] # JSON保存用データ
|
||||||
|
_timer: Optional[threading.Timer] # デバウンスタイマー
|
||||||
|
_debounce_time: int = 2 # デバウンス時間(秒)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 主要メソッド
|
||||||
|
|
||||||
|
**初期化・保存**
|
||||||
|
- `__new__(cls)`: シングルトンインスタンス生成・初期化
|
||||||
|
- `init_config()`: デフォルト値の設定
|
||||||
|
- `load_config()`: JSONファイルから設定読み込み
|
||||||
|
- `saveConfig(key, value, immediate_save=False)`: 設定の保存(デバウンス付き)
|
||||||
|
- `saveConfigToFile()`: JSONファイルへの即座書き込み
|
||||||
|
|
||||||
|
**デコレータ**
|
||||||
|
- `@json_serializable(var_name)`: 永続化対象プロパティのマーク
|
||||||
|
|
||||||
|
### 設定プロパティ一覧
|
||||||
|
|
||||||
|
#### 読み取り専用設定(23項目)
|
||||||
|
|
||||||
|
| プロパティ名 | 型 | 説明 | デフォルト値 |
|
||||||
|
|------------|----|----|------------|
|
||||||
|
| `VERSION` | str | アプリケーションバージョン | "3.3.0" |
|
||||||
|
| `PATH_LOCAL` | str | アプリケーションローカルパス | 実行時決定 |
|
||||||
|
| `PATH_CONFIG` | str | 設定ファイルパス | `{PATH_LOCAL}/config.json` |
|
||||||
|
| `PATH_LOGS` | str | ログディレクトリパス | `{PATH_LOCAL}/logs` |
|
||||||
|
| `GITHUB_URL` | str | GitHub API URL | リポジトリURL |
|
||||||
|
| `UPDATER_URL` | str | アップデーターAPIの URL | アップデーターURL |
|
||||||
|
| `BOOTH_URL` | str | Booth 販売ページURL | Booth URL |
|
||||||
|
| `DOCUMENTS_URL` | str | ドキュメントURL | Notion URL |
|
||||||
|
| `DEEPL_AUTH_KEY_PAGE_URL` | str | DeepL認証キー取得ページ | DeepL URL |
|
||||||
|
| `MAX_MIC_THRESHOLD` | int | マイクしきい値の最大値 | 2000 |
|
||||||
|
| `MAX_SPEAKER_THRESHOLD` | int | スピーカーしきい値の最大値 | 4000 |
|
||||||
|
| `WATCHDOG_TIMEOUT` | int | Watchdog タイムアウト(秒) | 60 |
|
||||||
|
| `WATCHDOG_INTERVAL` | int | Watchdog チェック間隔(秒) | 20 |
|
||||||
|
| `SELECTABLE_TAB_NO_LIST` | List[str] | 選択可能タブ番号 | ["1", "2", "3"] |
|
||||||
|
| `SELECTED_TAB_TARGET_LANGUAGES_NO_LIST` | List[str] | ターゲット言語タブ番号 | ["1", "2", "3"] |
|
||||||
|
| `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST` | List[str] | CTranslate2重みタイプリスト | 動的取得 |
|
||||||
|
| `SELECTABLE_WHISPER_WEIGHT_TYPE_LIST` | List[str] | Whisper重みタイプリスト | 動的取得 |
|
||||||
|
| `SELECTABLE_TRANSLATION_ENGINE_LIST` | List[str] | 翻訳エンジンリスト | 動的取得 |
|
||||||
|
| `SELECTABLE_TRANSCRIPTION_ENGINE_LIST` | List[str] | 音声認識エンジンリスト | 動的取得 |
|
||||||
|
| `SELECTABLE_UI_LANGUAGE_LIST` | List[str] | UI言語リスト | ["en", "ja", "ko", "zh-Hant", "zh-Hans"] |
|
||||||
|
| `COMPUTE_MODE` | str | 計算モード | "cuda" or "cpu" |
|
||||||
|
| `SELECTABLE_COMPUTE_DEVICE_LIST` | List[Dict] | 選択可能な計算デバイスリスト | 動的取得 |
|
||||||
|
| `SEND_MESSAGE_BUTTON_TYPE_LIST` | List[str] | 送信ボタンタイプリスト | ["show", "hide", "show_and_disable_enter_key"] |
|
||||||
|
|
||||||
|
#### ランタイム設定(10項目)
|
||||||
|
|
||||||
|
| プロパティ名 | 型 | 説明 | デフォルト値 | JSON保存 |
|
||||||
|
|------------|----|----|-----------|---------|
|
||||||
|
| `ENABLE_TRANSLATION` | bool | 翻訳機能有効フラグ | False | なし |
|
||||||
|
| `ENABLE_TRANSCRIPTION_SEND` | bool | 送信音声認識有効フラグ | False | なし |
|
||||||
|
| `ENABLE_TRANSCRIPTION_RECEIVE` | bool | 受信音声認識有効フラグ | False | なし |
|
||||||
|
| `ENABLE_FOREGROUND` | bool | フォアグラウンド有効フラグ | False | なし |
|
||||||
|
| `ENABLE_CHECK_ENERGY_SEND` | bool | 送信エネルギーチェック有効 | False | なし |
|
||||||
|
| `ENABLE_CHECK_ENERGY_RECEIVE` | bool | 受信エネルギーチェック有効 | False | なし |
|
||||||
|
| `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` | Dict[str, bool] | CTranslate2重み状態辞書 | {} | なし |
|
||||||
|
| `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` | Dict[str, bool] | Whisper重み状態辞書 | {} | なし |
|
||||||
|
| `SELECTABLE_TRANSLATION_ENGINE_STATUS` | Dict[str, bool] | 翻訳エンジン状態辞書 | {} | なし |
|
||||||
|
| `SELECTABLE_TRANSCRIPTION_ENGINE_STATUS` | Dict[str, bool] | 音声認識エンジン状態辞書 | {} | なし |
|
||||||
|
|
||||||
|
#### 永続化設定(60項目以上)
|
||||||
|
|
||||||
|
**メインウィンドウ設定**
|
||||||
|
- `SELECTED_TAB_NO`: 選択中のタブ番号
|
||||||
|
- `SELECTED_TRANSLATION_ENGINES`: タブごとの翻訳エンジン選択
|
||||||
|
- `SELECTED_YOUR_LANGUAGES`: タブごとの入力言語設定
|
||||||
|
- `SELECTED_TARGET_LANGUAGES`: タブごとのターゲット言語設定
|
||||||
|
- `SELECTED_TRANSCRIPTION_ENGINE`: 音声認識エンジン
|
||||||
|
- `CONVERT_MESSAGE_TO_ROMAJI`: ローマ字変換有効フラグ
|
||||||
|
- `CONVERT_MESSAGE_TO_HIRAGANA`: ひらがな変換有効フラグ
|
||||||
|
- `MAIN_WINDOW_SIDEBAR_COMPACT_MODE`: サイドバーコンパクトモード
|
||||||
|
- `SEND_MESSAGE_FORMAT_PARTS`: 送信メッセージフォーマット
|
||||||
|
- `RECEIVED_MESSAGE_FORMAT_PARTS`: 受信メッセージフォーマット
|
||||||
|
|
||||||
|
**UIウィンドウ設定**
|
||||||
|
- `TRANSPARENCY`: ウィンドウ透明度(0-100)
|
||||||
|
- `UI_SCALING`: UIスケーリング(%)
|
||||||
|
- `TEXTBOX_UI_SCALING`: テキストボックススケーリング(%)
|
||||||
|
- `MESSAGE_BOX_RATIO`: メッセージボックス比率(即座保存)
|
||||||
|
- `SEND_MESSAGE_BUTTON_TYPE`: 送信ボタンタイプ
|
||||||
|
- `SHOW_RESEND_BUTTON`: 再送信ボタン表示フラグ
|
||||||
|
- `FONT_FAMILY`: フォントファミリー
|
||||||
|
- `UI_LANGUAGE`: UI言語
|
||||||
|
- `MAIN_WINDOW_GEOMETRY`: ウィンドウ位置・サイズ(即座保存)
|
||||||
|
|
||||||
|
**マイク設定**
|
||||||
|
- `AUTO_MIC_SELECT`: 自動マイク選択
|
||||||
|
- `SELECTED_MIC_HOST`: 選択されたマイクホスト
|
||||||
|
- `SELECTED_MIC_DEVICE`: 選択されたマイクデバイス
|
||||||
|
- `MIC_THRESHOLD`: マイクしきい値
|
||||||
|
- `MIC_AUTOMATIC_THRESHOLD`: 自動しきい値調整
|
||||||
|
- `MIC_RECORD_TIMEOUT`: 録音タイムアウト(秒)
|
||||||
|
- `MIC_PHRASE_TIMEOUT`: フレーズタイムアウト(秒)
|
||||||
|
- `MIC_MAX_PHRASES`: 最大フレーズ数
|
||||||
|
- `MIC_WORD_FILTER`: ワードフィルターリスト
|
||||||
|
- `MIC_AVG_LOGPROB`: 平均対数確率しきい値
|
||||||
|
- `MIC_NO_SPEECH_PROB`: 無音確率しきい値
|
||||||
|
|
||||||
|
**スピーカー設定**
|
||||||
|
- `AUTO_SPEAKER_SELECT`: 自動スピーカー選択
|
||||||
|
- `SELECTED_SPEAKER_DEVICE`: 選択されたスピーカーデバイス
|
||||||
|
- `SPEAKER_THRESHOLD`: スピーカーしきい値
|
||||||
|
- `SPEAKER_AUTOMATIC_THRESHOLD`: 自動しきい値調整
|
||||||
|
- `SPEAKER_RECORD_TIMEOUT`: 録音タイムアウト(秒)
|
||||||
|
- `SPEAKER_PHRASE_TIMEOUT`: フレーズタイムアウト(秒)
|
||||||
|
- `SPEAKER_MAX_PHRASES`: 最大フレーズ数
|
||||||
|
- `SPEAKER_AVG_LOGPROB`: 平均対数確率しきい値
|
||||||
|
- `SPEAKER_NO_SPEECH_PROB`: 無音確率しきい値
|
||||||
|
|
||||||
|
**モデル設定**
|
||||||
|
- `SELECTED_TRANSLATION_COMPUTE_DEVICE`: 翻訳計算デバイス
|
||||||
|
- `SELECTED_TRANSCRIPTION_COMPUTE_DEVICE`: 音声認識計算デバイス
|
||||||
|
- `CTRANSLATE2_WEIGHT_TYPE`: CTranslate2重みタイプ
|
||||||
|
- `SELECTED_TRANSLATION_COMPUTE_TYPE`: 翻訳計算タイプ
|
||||||
|
- `WHISPER_WEIGHT_TYPE`: Whisper重みタイプ
|
||||||
|
- `SELECTED_TRANSCRIPTION_COMPUTE_TYPE`: 音声認識計算タイプ
|
||||||
|
|
||||||
|
**通信設定**
|
||||||
|
- `OSC_IP_ADDRESS`: OSC IPアドレス(デフォルト: "127.0.0.1")
|
||||||
|
- `OSC_PORT`: OSCポート(デフォルト: 9000)
|
||||||
|
- `AUTH_KEYS`: 認証キー辞書(DeepL API, Groq API, OpenRouter API等)
|
||||||
|
- `WEBSOCKET_HOST`: WebSocketホスト
|
||||||
|
- `WEBSOCKET_PORT`: WebSocketポート
|
||||||
|
- `WEBSOCKET_SERVER`: WebSocketサーバー有効フラグ(非永続化)
|
||||||
|
|
||||||
|
**翻訳エンジン モデル選択**
|
||||||
|
- `SELECTABLE_GROQ_MODEL_LIST`: 利用可能な Groq モデルリスト(非永続化)
|
||||||
|
- `SELECTED_GROQ_MODEL`: 選択中の Groq モデル
|
||||||
|
- `SELECTABLE_OPENROUTER_MODEL_LIST`: 利用可能な OpenRouter モデルリスト(非永続化)
|
||||||
|
- `SELECTED_OPENROUTER_MODEL`: 選択中の OpenRouter モデル
|
||||||
|
|
||||||
|
**オーバーレイ設定**
|
||||||
|
- `OVERLAY_SMALL_LOG`: 小ログオーバーレイ有効
|
||||||
|
- `OVERLAY_SMALL_LOG_SETTINGS`: 小ログオーバーレイ設定(位置、回転、表示時間等)
|
||||||
|
- `OVERLAY_LARGE_LOG`: 大ログオーバーレイ有効
|
||||||
|
- `OVERLAY_LARGE_LOG_SETTINGS`: 大ログオーバーレイ設定
|
||||||
|
- `OVERLAY_SHOW_ONLY_TRANSLATED_MESSAGES`: 翻訳メッセージのみ表示
|
||||||
|
|
||||||
|
**その他設定**
|
||||||
|
- `HOTKEYS`: ホットキー設定辞書(即座保存)
|
||||||
|
- `PLUGINS_STATUS`: プラグイン状態リスト(即座保存)
|
||||||
|
- `USE_EXCLUDE_WORDS`: 除外ワード機能使用フラグ
|
||||||
|
- `AUTO_CLEAR_MESSAGE_BOX`: メッセージボックス自動クリア
|
||||||
|
- `SEND_ONLY_TRANSLATED_MESSAGES`: 翻訳メッセージのみ送信
|
||||||
|
- `SEND_MESSAGE_TO_VRC`: VRChatへメッセージ送信
|
||||||
|
- `SEND_RECEIVED_MESSAGE_TO_VRC`: 受信メッセージをVRChatへ送信
|
||||||
|
- `LOGGER_FEATURE`: ロガー機能有効
|
||||||
|
- `VRC_MIC_MUTE_SYNC`: VRChatマイクミュート同期
|
||||||
|
- `NOTIFICATION_VRC_SFX`: VRChat通知効果音
|
||||||
|
|
||||||
|
## 内部実装の詳細
|
||||||
|
|
||||||
|
### デバウンス保存の実装
|
||||||
|
|
||||||
|
```python
|
||||||
|
def saveConfig(self, key: str, value: Any, immediate_save: bool = False) -> None:
|
||||||
|
self._config_data[key] = value
|
||||||
|
|
||||||
|
# 既存のタイマーをキャンセル
|
||||||
|
if isinstance(self._timer, threading.Timer) and self._timer.is_alive():
|
||||||
|
self._timer.cancel()
|
||||||
|
|
||||||
|
if immediate_save:
|
||||||
|
self.saveConfigToFile()
|
||||||
|
else:
|
||||||
|
# 2秒後に保存するタイマーをセット
|
||||||
|
self._timer = threading.Timer(self._debounce_time, self.saveConfigToFile)
|
||||||
|
self._timer.daemon = True
|
||||||
|
self._timer.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
### プロパティのバリデーション例
|
||||||
|
|
||||||
|
```python
|
||||||
|
@SELECTED_TAB_NO.setter
|
||||||
|
def SELECTED_TAB_NO(self, value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value in self.SELECTABLE_TAB_NO_LIST:
|
||||||
|
self._SELECTED_TAB_NO = value
|
||||||
|
self.saveConfig(inspect.currentframe().f_code.co_name, value)
|
||||||
|
```
|
||||||
|
|
||||||
|
各setterは以下のパターンを実装:
|
||||||
|
1. 型チェック (`isinstance`):`ManagedProperty` による型チェックは `None` を許容するが、個別 setter が数値変換などを行う場合は `None` を拒否するケースがある
|
||||||
|
2. 値の範囲・有効性チェック
|
||||||
|
3. 内部変数への代入
|
||||||
|
4. `saveConfig` 呼び出し(永続化対象の場合)
|
||||||
|
|
||||||
|
#### 型チェックの詳細(v3.3.0+)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 型チェック実装:ManagedProperty 経由では None を常に許可
|
||||||
|
if self.type_ is not None and value is not None and not isinstance(value, self.type_):
|
||||||
|
return # 無視する
|
||||||
|
```
|
||||||
|
|
||||||
|
この仕様は `ManagedProperty` を通じた型チェックに適用される。個別の setter で追加のバリデーションやキャストを行う場合、`None` は別途拒否されることがある。
|
||||||
|
|
||||||
|
### メッセージフォーマット構造
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"prefix": "", # メッセージ前置文字列
|
||||||
|
"suffix": "" # メッセージ後置文字列
|
||||||
|
},
|
||||||
|
"separator": "\n", # メッセージと翻訳の区切り
|
||||||
|
"translation": {
|
||||||
|
"prefix": "", # 翻訳前置文字列
|
||||||
|
"separator": "\n", # 複数翻訳の区切り
|
||||||
|
"suffix": "" # 翻訳後置文字列
|
||||||
|
},
|
||||||
|
"translation_first": False # 翻訳を先に表示するか
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### オーバーレイ設定構造
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"x_pos": 0.0, # X座標
|
||||||
|
"y_pos": 0.0, # Y座標
|
||||||
|
"z_pos": 0.0, # Z座標
|
||||||
|
"x_rotation": 0.0, # X軸回転
|
||||||
|
"y_rotation": 0.0, # Y軸回転
|
||||||
|
"z_rotation": 0.0, # Z軸回転
|
||||||
|
"display_duration": 5, # 表示時間(秒)
|
||||||
|
"fadeout_duration": 2, # フェードアウト時間(秒)
|
||||||
|
"opacity": 1.0, # 不透明度(0.0-1.0)
|
||||||
|
"ui_scaling": 1.0, # UIスケーリング
|
||||||
|
"tracker": "HMD" # トラッカー ("HMD", "LeftHand", "RightHand")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### セーフガードインポート
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
from device_manager import device_manager
|
||||||
|
except Exception:
|
||||||
|
device_manager = None # フォールバック値
|
||||||
|
```
|
||||||
|
|
||||||
|
全ての外部モジュールインポートはtry-exceptでラップされており、インポート失敗時でも `Config` クラスは正常に動作する。
|
||||||
|
|
||||||
|
### 初期化エラー
|
||||||
|
```python
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super(Config, cls).__new__(cls)
|
||||||
|
try:
|
||||||
|
cls._instance.init_config()
|
||||||
|
except Exception:
|
||||||
|
errorLogging() # エラーをログに記録
|
||||||
|
try:
|
||||||
|
cls._instance.load_config()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
return cls._instance
|
||||||
|
```
|
||||||
|
|
||||||
|
初期化とロード処理はそれぞれ独立してエラーハンドリングされる。
|
||||||
|
|
||||||
|
### 設定ロード時のエラー
|
||||||
|
```python
|
||||||
|
for key, value in self._config_data.items():
|
||||||
|
try:
|
||||||
|
setattr(self, key, value)
|
||||||
|
except Exception:
|
||||||
|
errorLogging() # 個別設定の読み込み失敗は継続
|
||||||
|
```
|
||||||
|
|
||||||
|
JSONから読み込んだ設定のうち、不正な値があっても他の設定の読み込みは継続される。
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
1. **デバウンス保存**: 頻繁な設定変更時にI/Oを削減
|
||||||
|
2. **遅延初期化**: オプションモジュールは必要時のみロード
|
||||||
|
3. **シングルトン**: 設定オブジェクトの複製を防止
|
||||||
|
4. **デーモンスレッド**: タイマースレッドはメインスレッド終了時に自動終了
|
||||||
|
|
||||||
|
## セキュリティ考慮事項
|
||||||
|
|
||||||
|
1. **認証キー**: `AUTH_KEYS` に格納される外部APIキーは平文でJSON保存される
|
||||||
|
2. **パス検証**: IP アドレスは `isValidIpAddress` でバリデーション
|
||||||
|
3. **型安全性**: 全てのセッターで型チェック実施
|
||||||
|
|
||||||
|
## テスト推奨事項
|
||||||
|
|
||||||
|
### 単体テスト
|
||||||
|
```python
|
||||||
|
def test_config_singleton():
|
||||||
|
config1 = Config()
|
||||||
|
config2 = Config()
|
||||||
|
assert config1 is config2
|
||||||
|
|
||||||
|
def test_debounce_save():
|
||||||
|
config.UI_LANGUAGE = "ja"
|
||||||
|
time.sleep(1)
|
||||||
|
config.UI_LANGUAGE = "en"
|
||||||
|
# 2秒以内の変更は1回のみ保存される
|
||||||
|
time.sleep(2.5)
|
||||||
|
# ここで保存完了
|
||||||
|
```
|
||||||
|
|
||||||
|
### バリデーションテスト
|
||||||
|
```python
|
||||||
|
def test_invalid_tab_no():
|
||||||
|
config.SELECTED_TAB_NO = "invalid" # 無視される
|
||||||
|
assert config.SELECTED_TAB_NO != "invalid"
|
||||||
|
```
|
||||||
|
|
||||||
|
### オプション依存のテスト
|
||||||
|
```python
|
||||||
|
def test_missing_device_manager():
|
||||||
|
# device_manager が None でも動作すること
|
||||||
|
assert config.SELECTABLE_COMPUTE_DEVICE_LIST is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
## マイグレーション
|
||||||
|
|
||||||
|
### 設定ファイルのバージョンアップ
|
||||||
|
`load_config()` は存在しないキーを無視し、`init_config()` のデフォルト値を使用する。新しいバージョンでキーが追加された場合:
|
||||||
|
|
||||||
|
1. 既存キーはJSONから読み込まれる
|
||||||
|
2. 新規キーは `init_config()` のデフォルト値が使用される
|
||||||
|
3. 次回保存時に全てのキーがJSON に書き込まれる
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
1. **マルチプロセス**: シングルトンはプロセス単位。マルチプロセス環境では各プロセスが独立したインスタンスを持つ
|
||||||
|
2. **スレッドセーフティ**: プロパティアクセス自体はスレッドセーフではない(保存タイマーのみスレッド対応)
|
||||||
|
3. **循環参照**: `device_manager` と `config` 間の循環参照に注意
|
||||||
|
4. **JSON制限**: JSON にシリアライズ可能な型のみ保存可能
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
プロジェクトのルートディレクトリの `LICENSE` ファイルを参照
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
- `controller.md`: Controller クラスの設定使用方法
|
||||||
|
- `mainloop.md`: メインループでの設定参照
|
||||||
|
- `仕様書.md`: 全体仕様
|
||||||
|
- `設計書.md`: システム設計
|
||||||
|
|
||||||
|
## 変更履歴
|
||||||
|
|
||||||
|
### v3.3.0
|
||||||
|
- 現行バージョン
|
||||||
|
- WebSocket サーバー設定追加
|
||||||
|
- オーバーレイ設定の拡張
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# Clipboard 機能設計書
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
VRCT のクリップボード機能は、VRChat 内でのテキスト送信効率を向上させるため、翻訳結果をクリップボードにコピーし、ペースト機能を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 1. テキストコピー
|
||||||
|
- 翻訳結果をシステムクリップボードにコピー
|
||||||
|
- 複数のバックエンド対応(Windows clip、pyperclip、tkinter)
|
||||||
|
- UTF-16LE BOM 対応でクロスプラットフォーム互換性を確保
|
||||||
|
|
||||||
|
### 2. テキストペースト
|
||||||
|
- `Ctrl+V` による自動ペースト
|
||||||
|
- VRChat ウィンドウの自動フォーカス
|
||||||
|
- カウントダウン機能で準備時間を確保
|
||||||
|
|
||||||
|
### 3. VR アプリ検出
|
||||||
|
- OpenVR を用いた VRChat アプリ名の自動検出
|
||||||
|
- バックグラウンドスレッドによる非同期監視
|
||||||
|
- SteamVR の起動を待機・検出
|
||||||
|
|
||||||
|
## クラス設計
|
||||||
|
|
||||||
|
### Clipboard クラス
|
||||||
|
```python
|
||||||
|
class Clipboard:
|
||||||
|
def __init__(self)
|
||||||
|
def enable(self) -> None
|
||||||
|
def disable(self) -> None
|
||||||
|
def copy(self, message: str) -> bool
|
||||||
|
def paste(self, window_name: str|None = None, countdown: int = 0) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
#### メンバ変数
|
||||||
|
- `is_enabled`: クリップボード機能の有効/無効状態
|
||||||
|
- `app_name`: VRChat アプリケーション名(自動検出)
|
||||||
|
- `_vr_monitor_thread`: SteamVR 監視スレッド
|
||||||
|
- `_stop_monitoring`: スレッド停止フラグ
|
||||||
|
|
||||||
|
### 主要メソッド
|
||||||
|
|
||||||
|
#### `__init__()`
|
||||||
|
初期化処理:
|
||||||
|
1. VR 監視スレッドを起動
|
||||||
|
2. SteamVR の起動を待機(10秒間隔で確認)
|
||||||
|
|
||||||
|
#### `enable()`
|
||||||
|
クリップボード機能を有効化:
|
||||||
|
1. `is_enabled` フラグを True に設定
|
||||||
|
2. `_initialize()` を呼び出し VR 監視を再開
|
||||||
|
|
||||||
|
#### `disable()`
|
||||||
|
クリップボード機能を無効化:
|
||||||
|
1. `is_enabled` フラグを False に設定
|
||||||
|
2. `_stop_monitoring` フラグを設定しスレッド停止
|
||||||
|
3. スレッド終了を待機(タイムアウト 1秒)
|
||||||
|
|
||||||
|
#### `copy(message: str) -> bool`
|
||||||
|
**パラメータ:**
|
||||||
|
- `message`: クリップボードにコピーするテキスト
|
||||||
|
|
||||||
|
**戻り値:**
|
||||||
|
- `True`: コピー成功
|
||||||
|
- `False`: コピー失敗または無効状態
|
||||||
|
|
||||||
|
**動作:**
|
||||||
|
1. `is_enabled` チェック
|
||||||
|
2. `copy_to_clipboard()` 関数を呼び出し
|
||||||
|
|
||||||
|
#### `paste(window_name: str|None = None, countdown: int = 0) -> bool`
|
||||||
|
**パラメータ:**
|
||||||
|
- `window_name`: フォーカス対象ウィンドウ(タイトル部分一致 or プロセス名)
|
||||||
|
- `countdown`: ペースト前の待機秒数
|
||||||
|
|
||||||
|
**戻り値:**
|
||||||
|
- `True`: ペースト送信成功
|
||||||
|
- `False`: ペースト失敗または無効状態
|
||||||
|
|
||||||
|
**動作:**
|
||||||
|
1. `is_enabled` チェック
|
||||||
|
2. `window_name` 未指定時は自動検出した VRChat アプリ名を使用
|
||||||
|
3. Windows 環境下でウィンドウをフォーカス(以下の順序)
|
||||||
|
- ウィンドウタイトルで部分一致検索
|
||||||
|
- マッチしない場合、プロセス名で検索
|
||||||
|
4. ウィンドウフォーカス成功時、`paste_via_pyautogui()` でペースト実行
|
||||||
|
|
||||||
|
### 内部メソッド
|
||||||
|
|
||||||
|
#### `_initialize()`
|
||||||
|
VR 監視スレッドの初期化:
|
||||||
|
1. `_stop_monitoring` を False に設定
|
||||||
|
2. デーモンスレッドで `_monitor_steamvr()` を起動
|
||||||
|
|
||||||
|
#### `_monitor_steamvr()`
|
||||||
|
バックグラウンドスレッド処理:
|
||||||
|
1. 10秒間隔で `checkSteamvrRunning()` を確認
|
||||||
|
2. SteamVR 検出時に `_setup_vr_app_name()` を呼び出し
|
||||||
|
3. `_stop_monitoring` が True になるまで監視
|
||||||
|
|
||||||
|
#### `_setup_vr_app_name()`
|
||||||
|
OpenVR を用いた VR アプリ名検出:
|
||||||
|
1. OpenVR 初期化(`VRApplication_Background` モード)
|
||||||
|
2. すべてのアプリケーション情報を取得
|
||||||
|
3. `steam.app` で始まるキーを検索し `app_name` に設定
|
||||||
|
4. 例外発生時は `app_name` を None に設定
|
||||||
|
|
||||||
|
## 支援関数
|
||||||
|
|
||||||
|
### `checkSteamvrRunning() -> bool`
|
||||||
|
SteamVR が起動しているかを確認
|
||||||
|
- プロセス名 `vrmonitor.exe`(Windows)または `vrmonitor`(その他)を検索
|
||||||
|
|
||||||
|
### `copy_to_clipboard(text: str) -> bool`
|
||||||
|
複数バックエンド対応のコピー関数
|
||||||
|
1. Windows: `clip` コマンド + UTF-16LE BOM
|
||||||
|
2. 汎用: `pyperclip` ライブラリ
|
||||||
|
3. フォールバック: `tkinter` 使用
|
||||||
|
|
||||||
|
### `paste_via_pyautogui(countdown: int = 0) -> bool`
|
||||||
|
PyAutoGUI を用いたペースト
|
||||||
|
- `pyautogui.hotkey('ctrl', 'v')` で Ctrl+V を送信
|
||||||
|
- カウントダウン実行
|
||||||
|
|
||||||
|
### ウィンドウ検索関数(Windows のみ)
|
||||||
|
|
||||||
|
#### `find_windows_by_title_substring(substring: str) -> list`
|
||||||
|
ウィンドウタイトルで部分一致検索
|
||||||
|
|
||||||
|
#### `find_windows_by_process_name(proc_name: str) -> list`
|
||||||
|
プロセス名でウィンドウを検索
|
||||||
|
|
||||||
|
#### `focus_window(hwnd) -> bool`
|
||||||
|
指定ウィンドウをアクティブ化
|
||||||
|
|
||||||
|
## 依存ライブラリ
|
||||||
|
|
||||||
|
| ライブラリ | 用途 | オプション |
|
||||||
|
|-----------|------|----------|
|
||||||
|
| pyautogui | キー入力シミュレーション | 必須(ペースト機能) |
|
||||||
|
| pyperclip | クリップボード操作 | オプション(フォールバック) |
|
||||||
|
| tkinter | クリップボード操作 | オプション(フォールバック) |
|
||||||
|
| openvr | VR アプリ名検出 | 必須 |
|
||||||
|
| psutil | プロセス検索 | 必須 |
|
||||||
|
|
||||||
|
## 動作フロー
|
||||||
|
|
||||||
|
### 初期化フロー
|
||||||
|
```
|
||||||
|
Clipboard.__init__()
|
||||||
|
└─ _initialize()
|
||||||
|
└─ スレッド起動: _monitor_steamvr()
|
||||||
|
└─ 10秒間隔で SteamVR 監視
|
||||||
|
└─ 検出時: _setup_vr_app_name()
|
||||||
|
└─ VRChat アプリ名を app_name に設定
|
||||||
|
```
|
||||||
|
|
||||||
|
### コピー・ペーストフロー
|
||||||
|
```
|
||||||
|
Model.setCopyToClipboard(text)
|
||||||
|
└─ clipboard.copy(text)
|
||||||
|
└─ copy_to_clipboard(text) [成功/失敗]
|
||||||
|
|
||||||
|
Model.setPasteFromClipboard()
|
||||||
|
└─ clipboard.paste(window_name=self.app_name)
|
||||||
|
└─ find_windows_by_title_substring() [Windows]
|
||||||
|
or find_windows_by_process_name()
|
||||||
|
└─ focus_window(hwnd)
|
||||||
|
└─ paste_via_pyautogui(countdown)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 有効/無効制御
|
||||||
|
|
||||||
|
### コンフィグ設定
|
||||||
|
- `config.ENABLE_CLIPBOARD`: True/False でクリップボード機能を制御
|
||||||
|
- Controller 側で `config.ENABLE_CLIPBOARD` をチェックし、True の場合のみ `setCopyToClipboard()` と `setPasteFromClipboard()` を呼び出し
|
||||||
|
|
||||||
|
### 制御メソッド(Clipboard クラス内部)
|
||||||
|
```python
|
||||||
|
# 有効化(内部API、通常は使用しない)
|
||||||
|
clipboard.enable()
|
||||||
|
|
||||||
|
# 無効化(内部API、通常は使用しない)
|
||||||
|
clipboard.disable()
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 現在の実装では、Controller は `clipboard.enable()/disable()` を呼び出さず、`config.ENABLE_CLIPBOARD` フラグのみで制御します。Clipboard インスタンスは常に初期化されており、VR 監視スレッドも稼働し続けます。
|
||||||
|
|
||||||
|
### API エンドポイント(Controller)
|
||||||
|
```
|
||||||
|
/get/data/clipboard - 現在の有効/無効状態を取得
|
||||||
|
/set/enable/clipboard - config.ENABLE_CLIPBOARD を True に設定
|
||||||
|
/set/disable/clipboard - config.ENABLE_CLIPBOARD を False に設定
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 失敗時の動作
|
||||||
|
- コピー失敗: `False` を返却、リトライなし
|
||||||
|
- ペースト失敗: `False` を返却、リトライなし
|
||||||
|
- VR アプリ名検出失敗: `app_name` を None に設定、現在フォーカス中のウィンドウにペースト
|
||||||
|
|
||||||
|
### 例外処理
|
||||||
|
- OpenVR 初期化失敗時は `app_name = None` に設定
|
||||||
|
- プロセス検索例外も握りつぶし
|
||||||
|
- ウィンドウフォーカス失敗はログのみ出力
|
||||||
|
|
||||||
|
## パフォーマンス考慮
|
||||||
|
|
||||||
|
### スレッド設計
|
||||||
|
- VR 監視スレッドは **デーモンスレッド**(アプリ終了時の強制停止可能)
|
||||||
|
- 10秒間隔で監視(CPU 負荷最小化)
|
||||||
|
|
||||||
|
### クリップボード操作
|
||||||
|
- コピー・ペースト操作はメインスレッド実行
|
||||||
|
- ウィンドウフォーカス後に 0.2秒スリープで安定性向上
|
||||||
|
|
||||||
|
## セキュリティ・プライバシー
|
||||||
|
|
||||||
|
### データ保護
|
||||||
|
- クリップボードデータは一切ログ出力されない
|
||||||
|
- VR アプリ名のみ app_name として保持
|
||||||
|
|
||||||
|
### OS 依存性
|
||||||
|
- Windows: `clip` コマンド、ctypes によるウィンドウ制御
|
||||||
|
- その他 OS: pyperclip/tkinter フォールバック、自動フォーカス機能なし
|
||||||
|
|
||||||
|
## 制限事項・既知の問題
|
||||||
|
|
||||||
|
1. **非 Windows 環境**
|
||||||
|
- ウィンドウフォーカス機能は Windows のみ対応
|
||||||
|
- その他 OS では現在フォーカス中のウィンドウにペースト
|
||||||
|
|
||||||
|
2. **VRChat アプリ名検出**
|
||||||
|
- OpenVR が利用可能で SteamVR が起動している必要がある
|
||||||
|
- `steam.app` 接頭辞のアプリのみ認識
|
||||||
|
|
||||||
|
3. **PyAutoGUI の制限**
|
||||||
|
- キー入力シミュレーションはOS依存
|
||||||
|
- 一部アプリケーションではセキュリティ制限により失敗する可能性あり
|
||||||
|
|
||||||
|
4. **クリップボードバッファ**
|
||||||
|
- Ctrl+V 送信のタイミングはユーザー判断(カウントダウン機能あり)
|
||||||
|
- クリップボード内容の永続化はしない
|
||||||
|
|
||||||
|
## 参考資料
|
||||||
|
|
||||||
|
- [PyAutoGUI 公式ドキュメント](https://pyautogui.readthedocs.io/)
|
||||||
|
- [OpenVR Python バインディング](https://github.com/ValvePython/openvr)
|
||||||
|
- [psutil ドキュメント](https://psutil.readthedocs.io/)
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# config.py - 設定管理モジュール
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRCTアプリケーションの全設定を一元管理するモジュールです。シングルトンパターンを採用し、アプリケーション全体で統一された設定アクセスを提供します。JSON設定ファイルの読み書き、設定の永続化、デバウンス機能付き保存機能を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### シングルトン設計
|
||||||
|
- アプリケーション全体で単一の設定インスタンス
|
||||||
|
- スレッドセーフな設定アクセス
|
||||||
|
- 遅延初期化による軽量インポート
|
||||||
|
|
||||||
|
### 設定永続化
|
||||||
|
- JSON形式での設定ファイル管理
|
||||||
|
- デバウンス機能付き自動保存
|
||||||
|
- 設定変更の即座反映
|
||||||
|
|
||||||
|
### 動的設定管理
|
||||||
|
- 実行時設定変更対応
|
||||||
|
- デバイス情報の動的取得
|
||||||
|
- 言語・エンジン設定の自動更新
|
||||||
|
|
||||||
|
### 型安全な設定アクセス
|
||||||
|
- プロパティベースのアクセス制御
|
||||||
|
- 読み取り専用・読み書き可能設定の分離
|
||||||
|
- デコレータによるシリアライゼーション管理
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Config クラス
|
||||||
|
```python
|
||||||
|
class Config:
|
||||||
|
_instance = None # シングルトンインスタンス
|
||||||
|
_config_data: Dict[str, Any] # 設定データ
|
||||||
|
_timer: Optional[threading.Timer] # デバウンスタイマー
|
||||||
|
_debounce_time: int = 2 # デバウンス時間(秒)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定カテゴリ
|
||||||
|
|
||||||
|
### 読み取り専用設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def VERSION(self) -> str
|
||||||
|
```
|
||||||
|
- アプリケーションバージョン
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def PATH_LOCAL(self) -> str
|
||||||
|
```
|
||||||
|
- ローカルディレクトリパス
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def PATH_CONFIG(self) -> str
|
||||||
|
```
|
||||||
|
- 設定ファイルパス
|
||||||
|
|
||||||
|
### UI・表示設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def UI_LANGUAGE(self) -> str
|
||||||
|
```
|
||||||
|
- UIの表示言語
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def TRANSPARENCY(self) -> int
|
||||||
|
```
|
||||||
|
- ウィンドウの透明度(0-100)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def UI_SCALING(self) -> int
|
||||||
|
```
|
||||||
|
- UIのスケーリング(50-200%)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def FONT_FAMILY(self) -> str
|
||||||
|
```
|
||||||
|
- 使用フォントファミリー
|
||||||
|
|
||||||
|
### 翻訳設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def ENABLE_TRANSLATION(self) -> bool
|
||||||
|
```
|
||||||
|
- 翻訳機能の有効・無効
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_TRANSLATION_ENGINES(self) -> Dict[str, str]
|
||||||
|
```
|
||||||
|
- 選択されている翻訳エンジン
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_YOUR_LANGUAGES(self) -> Dict[str, Dict[str, Any]]
|
||||||
|
```
|
||||||
|
- 送信言語設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_TARGET_LANGUAGES(self) -> Dict[str, Dict[str, Any]]
|
||||||
|
```
|
||||||
|
- 受信言語設定
|
||||||
|
|
||||||
|
### 音声認識設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def ENABLE_TRANSCRIPTION_SEND(self) -> bool
|
||||||
|
```
|
||||||
|
- 送信音声認識の有効・無効
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_TRANSCRIPTION_ENGINE(self) -> str
|
||||||
|
```
|
||||||
|
- 音声認識エンジン
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_MIC_DEVICE(self) -> str
|
||||||
|
```
|
||||||
|
- 選択されたマイクデバイス
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def MIC_THRESHOLD(self) -> int
|
||||||
|
```
|
||||||
|
- マイク音声しきい値
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def MIC_RECORD_TIMEOUT(self) -> int
|
||||||
|
```
|
||||||
|
- マイク録音タイムアウト(秒)
|
||||||
|
|
||||||
|
### VR設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def OVERLAY_SMALL_LOG(self) -> bool
|
||||||
|
```
|
||||||
|
- 小型ログオーバーレイの有効・無効
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def OVERLAY_SMALL_LOG_SETTINGS(self) -> Dict[str, Any]
|
||||||
|
```
|
||||||
|
- 小型オーバーレイの詳細設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def OVERLAY_LARGE_LOG_SETTINGS(self) -> Dict[str, Any]
|
||||||
|
```
|
||||||
|
- 大型オーバーレイの詳細設定
|
||||||
|
|
||||||
|
### 通信設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def OSC_IP_ADDRESS(self) -> str
|
||||||
|
```
|
||||||
|
- OSC通信IPアドレス
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def OSC_PORT(self) -> int
|
||||||
|
```
|
||||||
|
- OSC通信ポート
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def WEBSOCKET_HOST(self) -> str
|
||||||
|
```
|
||||||
|
- WebSocketサーバーホスト
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def WEBSOCKET_PORT(self) -> int
|
||||||
|
```
|
||||||
|
- WebSocketサーバーポート
|
||||||
|
|
||||||
|
### クリップボード設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def ENABLE_CLIPBOARD(self) -> bool
|
||||||
|
```
|
||||||
|
- クリップボード機能(コピー・ペースト)の有効・無効
|
||||||
|
|
||||||
|
### テレメトリ設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def ENABLE_TELEMETRY(self) -> bool
|
||||||
|
```
|
||||||
|
- テレメトリ(Aptabase)の有効・無効
|
||||||
|
- デフォルト: True(有効)
|
||||||
|
- ユーザーは設定から任意で無効化可能
|
||||||
|
|
||||||
|
### 計算デバイス設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_TRANSLATION_COMPUTE_DEVICE(self) -> Dict[str, Any]
|
||||||
|
```
|
||||||
|
- 翻訳用計算デバイス
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def SELECTED_TRANSCRIPTION_COMPUTE_DEVICE(self) -> Dict[str, Any]
|
||||||
|
```
|
||||||
|
- 音声認識用計算デバイス
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 設定保存
|
||||||
|
|
||||||
|
```python
|
||||||
|
saveConfig(key: str, value: Any, immediate_save: bool = False) -> None
|
||||||
|
```
|
||||||
|
- 設定値の保存(デバウンス付き)
|
||||||
|
- immediate_save=Trueで即座保存
|
||||||
|
|
||||||
|
```python
|
||||||
|
saveConfigToFile() -> None
|
||||||
|
```
|
||||||
|
- 設定ファイルへの直接保存
|
||||||
|
|
||||||
|
### 初期化・設定読み込み
|
||||||
|
|
||||||
|
```python
|
||||||
|
init_config() -> None
|
||||||
|
```
|
||||||
|
- 設定の初期化
|
||||||
|
- デフォルト値の設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
load_config() -> None
|
||||||
|
```
|
||||||
|
- 設定ファイルからの読み込み
|
||||||
|
- 存在しない場合はデフォルト設定を作成
|
||||||
|
|
||||||
|
## デコレータ機能
|
||||||
|
|
||||||
|
### @json_serializable
|
||||||
|
```python
|
||||||
|
@json_serializable("setting_name")
|
||||||
|
@property
|
||||||
|
def SETTING_NAME(self) -> Any:
|
||||||
|
```
|
||||||
|
- 設定のJSONシリアライゼーション対象指定
|
||||||
|
- 自動的にconfig.jsonに保存される設定を定義
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
# 設定値の取得
|
||||||
|
version = config.VERSION
|
||||||
|
ui_language = config.UI_LANGUAGE
|
||||||
|
translation_enabled = config.ENABLE_TRANSLATION
|
||||||
|
|
||||||
|
# 設定値の変更
|
||||||
|
config.UI_LANGUAGE = "ja"
|
||||||
|
config.TRANSPARENCY = 80
|
||||||
|
config.MIC_THRESHOLD = 1500
|
||||||
|
```
|
||||||
|
|
||||||
|
### 複雑な設定の変更
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 翻訳エンジンの設定
|
||||||
|
engines = config.SELECTED_TRANSLATION_ENGINES
|
||||||
|
engines["1"] = "DeepL"
|
||||||
|
config.SELECTED_TRANSLATION_ENGINES = engines
|
||||||
|
|
||||||
|
# オーバーレイ設定の変更
|
||||||
|
overlay_settings = config.OVERLAY_SMALL_LOG_SETTINGS
|
||||||
|
overlay_settings["x_pos"] = 0.5
|
||||||
|
overlay_settings["opacity"] = 0.8
|
||||||
|
config.OVERLAY_SMALL_LOG_SETTINGS = overlay_settings
|
||||||
|
```
|
||||||
|
|
||||||
|
### 即座保存
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 重要な設定変更時の即座保存
|
||||||
|
config.saveConfig("ENABLE_TRANSLATION", True, immediate_save=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定ファイル形式
|
||||||
|
|
||||||
|
設定は`config.json`ファイルにJSON形式で保存されます:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"UI_LANGUAGE": "ja",
|
||||||
|
"TRANSPARENCY": 85,
|
||||||
|
"UI_SCALING": 100,
|
||||||
|
"ENABLE_TRANSLATION": true,
|
||||||
|
"SELECTED_TRANSLATION_ENGINES": {
|
||||||
|
"1": "DeepL",
|
||||||
|
"2": "Google",
|
||||||
|
"3": "CTranslate2"
|
||||||
|
},
|
||||||
|
"OVERLAY_SMALL_LOG_SETTINGS": {
|
||||||
|
"x_pos": 0.0,
|
||||||
|
"y_pos": -0.4,
|
||||||
|
"z_pos": 1.0,
|
||||||
|
"opacity": 1.0,
|
||||||
|
"ui_scaling": 1.0,
|
||||||
|
"display_duration": 5,
|
||||||
|
"fadeout_duration": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## デフォルト設定
|
||||||
|
|
||||||
|
### UI設定
|
||||||
|
- UI言語: "en"(英語)
|
||||||
|
- 透明度: 85%
|
||||||
|
- UIスケーリング: 100%
|
||||||
|
- フォント: "Noto Sans JP"
|
||||||
|
|
||||||
|
### 翻訳設定
|
||||||
|
- 翻訳機能: 無効
|
||||||
|
- デフォルトエンジン: "Google"
|
||||||
|
- 送信言語: English(US)
|
||||||
|
- 受信言語: 日本語
|
||||||
|
|
||||||
|
### 音声認識設定
|
||||||
|
- 送信音声認識: 無効
|
||||||
|
- 受信音声認識: 無効
|
||||||
|
- 音声認識エンジン: "Google"
|
||||||
|
- マイクしきい値: 300
|
||||||
|
|
||||||
|
### VR設定
|
||||||
|
- 小型オーバーレイ: 無効
|
||||||
|
- 大型オーバーレイ: 無効
|
||||||
|
- オーバーレイ位置: HMD正面
|
||||||
|
|
||||||
|
### 通信設定
|
||||||
|
- OSC IP: "127.0.0.1"
|
||||||
|
- OSC ポート: 9000
|
||||||
|
- WebSocket ホスト: "127.0.0.1"
|
||||||
|
- WebSocket ポート: 8765
|
||||||
|
|
||||||
|
### クリップボード設定
|
||||||
|
- クリップボード機能: 無効
|
||||||
|
|
||||||
|
### テレメトリ設定
|
||||||
|
- テレメトリ: **有効(デフォルト)**
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `json`: 設定ファイルのシリアライゼーション
|
||||||
|
- `threading`: デバウンス機能
|
||||||
|
- `typing`: 型注釈
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `device_manager`: デバイス情報取得
|
||||||
|
- `torch`: CUDA計算デバイス情報
|
||||||
|
- 各種モデルモジュール: 言語・エンジン情報
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- 設定ファイル読み込みエラーの適切な処理
|
||||||
|
- 不正な設定値の検証・補正
|
||||||
|
- オプション依存関係の欠如に対するフォールバック
|
||||||
|
- ファイル書き込みエラーの処理
|
||||||
|
|
||||||
|
## パフォーマンス特性
|
||||||
|
|
||||||
|
### デバウンス機能
|
||||||
|
- 設定変更から2秒後に自動保存
|
||||||
|
- 連続する変更の統合
|
||||||
|
- I/O負荷の軽減
|
||||||
|
|
||||||
|
### 遅延初期化
|
||||||
|
- 重い依存関係の遅延読み込み
|
||||||
|
- インポート時間の短縮
|
||||||
|
|
||||||
|
### メモリ効率
|
||||||
|
- 設定データのシングルトン管理
|
||||||
|
- 不要な複製の防止
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 設定変更は即座にメモリに反映される
|
||||||
|
- ファイル保存はデバウンス機能により遅延される
|
||||||
|
- 重要な設定はimmediate_save=Trueを使用
|
||||||
|
- オプション依存関係の欠如時はデフォルト値を使用
|
||||||
|
- 不正な設定値は自動的に補正される
|
||||||
|
- 設定ファイルが破損した場合は新規作成される
|
||||||
|
|
||||||
|
## セキュリティ考慮事項
|
||||||
|
|
||||||
|
- 設定ファイルの適切な権限管理
|
||||||
|
- 外部入力値の検証
|
||||||
|
- APIキー等の機密情報の適切な取り扱い
|
||||||
|
- パスインジェクション攻撃の防止
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
### LMStudio / Ollama ローカル LLM 設定プロパティ追加
|
||||||
|
|
||||||
|
- `LMSTUDIO_URL` / `SELECTABLE_LMSTUDIO_MODEL_LIST` / `SELECTED_LMSTUDIO_MODEL`
|
||||||
|
- `SELECTABLE_OLLAMA_MODEL_LIST` / `SELECTED_OLLAMA_MODEL`
|
||||||
|
|
||||||
|
ローカル推論エンジン接続用 URL と動的モデルリスト取得・選択プロパティを追加。認証は不要で接続テスト後自動でモデルリストを更新。
|
||||||
|
|
||||||
|
### モデル選択プロパティ名称統一
|
||||||
|
|
||||||
|
Plamo / Gemini / OpenAI の選択モデルを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` へ統一。設定 JSON の保存キーも `SELECTED_*` に変更し UI との整合性を確保。
|
||||||
|
|
||||||
|
### CTranslate2 言語マッピング構造変更対応
|
||||||
|
|
||||||
|
`translation_lang` 内の CTranslate2 言語辞書が `translation_lang["CTranslate2"][weight_type]["source"|"target"]` へネスト化。`CTRANSLATE2_WEIGHT_TYPE` プロパティがアクセスのキーとなるため、重みタイプ変更時に翻訳エンジン再初期化が必要。
|
||||||
|
|
||||||
|
### YAML 言語外部定義導入
|
||||||
|
|
||||||
|
`loadTranslationLanguages()` を初期化時に呼び出し、`models/translation/languages/languages.yml` を読み込んで既存マッピングへマージ。失敗時は空辞書フォールバック。動的言語追加がコード改修無しで可能になったため設定初期化の失敗ログ確認が重要。
|
||||||
|
|
||||||
|
### OpenAI モデルリスト自動更新
|
||||||
|
|
||||||
|
`setOpenAIAuthKey()` 成功後に `SELECTABLE_OPENAI_MODEL_LIST` を取得し未選択の場合は先頭を自動選択。Gemini / Plamo / LMStudio / Ollama も同様に認証/接続確立時にリスト更新と未選択モデル補完。
|
||||||
|
|
||||||
|
### フォント設定のパッケージ対応
|
||||||
|
|
||||||
|
`overlay_image.py` で PyInstaller ビルド環境(`_internal/fonts/`)検出を追加。開発環境とバンドル後でフォント探索パスが異なるため、`FONT_FAMILY` はファイル名基準のまま変更無し。
|
||||||
|
|
||||||
|
### 依存関係追加
|
||||||
|
|
||||||
|
- `PyYAML`: 言語マッピング YAML 読み込み
|
||||||
|
- `google-genai`: Gemini 連携
|
||||||
|
- `grpcio`: OpenAI 連携(ストリーミング等)
|
||||||
|
|
||||||
|
### VRAM エラー時の自動フォールバック
|
||||||
|
|
||||||
|
翻訳有効化や翻訳実行時に VRAM 不足検出で `ENABLE_TRANSLATION` を False にし CTranslate2 へ強制切替。設定値は保持されるが UI には無効化状態を通知。再度有効化要求時に重いモデル再初期化を試行。
|
||||||
|
|
||||||
|
### テスト関連
|
||||||
|
|
||||||
|
包括的翻訳ペアテストにより `SELECTED_*` モデルと言語マッピング組合せを大量実行。設定値の変更頻度増加に伴いデバウンス 2 秒でファイル書き込み負荷を抑制。
|
||||||
|
|
||||||
|
### 影響まとめ
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| ローカルLLM | LMStudio / Ollama の導入でオフライン翻訳拡張 |
|
||||||
|
| プロパティ統一 | SELECTED_* 命名で一貫性・ドキュメント整備性向上 |
|
||||||
|
| 言語ネスト化 | CTranslate2 重みタイプ切替処理の再初期化必要性増加 |
|
||||||
|
| YAML外部化 | 言語追加が設定初期化のみで反映可能 |
|
||||||
|
| モデルリスト自動更新 | 認証後の選択ミス防止・初回 UX 改善 |
|
||||||
|
| フォント探索 | PyInstaller ビルド後でも同一コードで動作 |
|
||||||
|
| 依存追加 | 新機能対応で環境構築ステップ増加 |
|
||||||
|
| VRAM検知 | 安全停止と軽量エンジン切替で安定性向上 |
|
||||||
|
| テスト増強 | 大量ペア検証で言語/モデル設定の信頼性向上 |
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
# controller.py - VRCTコントローラーモジュール
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
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翻訳エンジン統合
|
||||||
|
|
||||||
|
- LMStudio / Ollama への接続確認エンドポイント追加: `/run/lmstudio_connection`, `/run/ollama_connection`
|
||||||
|
- LMStudio URL 設定: `/get|set/data/lmstudio_url`
|
||||||
|
- モデルリスト取得と選択: `/get/data/*_model_list`, `/get|set/data/*_model` (lmstudio / ollama / plamo / gemini / openai)
|
||||||
|
- 認証・接続成功時に `selectable_*_model_list` / `selected_*_model` を run 経由で通知 (例: `/run/selectable_lmstudio_model_list`, `/run/selected_lmstudio_model`)
|
||||||
|
|
||||||
|
### モデルリスト自動更新フロー
|
||||||
|
|
||||||
|
- Plamo / Gemini / OpenAI 認証後に動的に最新モデルリストを取得し未選択時は先頭モデルへ自動設定
|
||||||
|
- LMStudio / Ollama は接続成功時にローカル列挙したモデルを即座に選択候補へ反映
|
||||||
|
|
||||||
|
### VRAMエラー検出と自動フォールバック
|
||||||
|
|
||||||
|
- 翻訳処理中の `CUDA out of memory` / `CUBLAS_STATUS_ALLOC_FAILED` などを検出し `/run/error_translation_*_vram_overflow` で通知
|
||||||
|
- 自動で翻訳機能を無効化し CTranslate2 へフォールバック、再度有効化試行時も VRAM エラー検出で安全に解除
|
||||||
|
|
||||||
|
### CTranslate2 ウェイト / Whisper ウェイト管理
|
||||||
|
|
||||||
|
- ダウンロード進捗/完了/エラー用 run エンドポイント: `download_progress_ctranslate2_weight`, `downloaded_ctranslate2_weight`, `error_ctranslate2_weight` / Whisper も同様
|
||||||
|
- `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` / `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` の値更新を完了時に反映
|
||||||
|
|
||||||
|
### 命名・構造整理
|
||||||
|
|
||||||
|
- 翻訳エンジン選択はタブ別 `SELECTED_TRANSLATION_ENGINES[tab_no]` を使用
|
||||||
|
- 言語選択構造: `SELECTED_YOUR_LANGUAGES[tab_no]`, `SELECTED_TARGET_LANGUAGES[tab_no]` のネストを前提にエンジン適合性判定
|
||||||
|
- CTranslate2 の言語マッピングはウェイト種別 (`CTRANSLATE2_WEIGHT_TYPE`) でネストされた構造に対応
|
||||||
|
|
||||||
|
### 言語マッピング外部化
|
||||||
|
|
||||||
|
- `getListLanguageAndCountry()` が YAML からロード済み `translation_lang` / `transcription_lang` を統合して互換言語のみ抽出
|
||||||
|
|
||||||
|
### APIキー検証の厳格化
|
||||||
|
|
||||||
|
- Plamo API: キー長判定を「==72」から「>=72」へ変更し 72 文字以上を許容
|
||||||
|
- Gemini API: 最小キー長を 20 → 39 へ引き上げ
|
||||||
|
- OpenAI API: "sk-" 接頭辞必須かつ長さ 164 文字以上の厳格化、エラーメッセージを「無効」に統一
|
||||||
|
|
||||||
|
### 翻訳モデル選択時の適用確実化
|
||||||
|
|
||||||
|
- OpenAI / Plamo / Gemini / LMStudio / Ollama でモデル設定後に `setTranslatorXModel()` と `updateTranslatorXClient()` を必ず呼び出してクライアント状態を確実反映
|
||||||
|
- デフォルトモデル自動選択時もモデル適用を即座実行
|
||||||
|
|
||||||
|
### デバイス自動選択改善
|
||||||
|
|
||||||
|
- マイク/スピーカー自動選択機能で更新前後に適切な停止/再開コールバックをチェーン設定 (energy チェック再起動含む)
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 翻訳柔軟性 | ローカルLLM (LMStudio/Ollama) によりネットワーク不要運用が可能 |
|
||||||
|
| 回復性 | VRAMエラー時自動フォールバックで異常終了を防止 |
|
||||||
|
| 拡張性 | モデルリスト自動更新により新モデル追加時の手動変更不要化 |
|
||||||
|
| 一貫性 | 統一された SELECTED_/SELECTABLE_ 命名とタブ別管理で UI 連携が簡易化 |
|
||||||
|
| 可読性 | 外部 YAML 言語定義によりコード側のハードコード削減 |
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 機能制御
|
||||||
|
|
||||||
|
- 翻訳機能の有効化・無効化
|
||||||
|
- 音声認識機能の制御
|
||||||
|
- VRオーバーレイの管理
|
||||||
|
- WebSocketサーバーの制御
|
||||||
|
|
||||||
|
### 設定管理
|
||||||
|
|
||||||
|
- アプリケーション設定の取得・更新
|
||||||
|
- デバイス設定の管理
|
||||||
|
- 言語・エンジン設定の制御
|
||||||
|
|
||||||
|
### 状態管理
|
||||||
|
|
||||||
|
- システム状態の監視
|
||||||
|
- エラー状態の管理
|
||||||
|
- 初期化プロセスの制御
|
||||||
|
|
||||||
|
### 通信制御
|
||||||
|
|
||||||
|
- OSC通信の管理
|
||||||
|
- WebSocket通信の制御
|
||||||
|
- 外部アプリケーション連携
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Controller クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Controller:
|
||||||
|
def __init__(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
中核となるコントローラークラス
|
||||||
|
|
||||||
|
### 内部ヘルパークラス
|
||||||
|
|
||||||
|
#### DownloadCTranslate2 クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DownloadCTranslate2:
|
||||||
|
def progressBar(self, progress) -> None
|
||||||
|
def downloaded(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 翻訳モデルのダウンロード進捗管理
|
||||||
|
|
||||||
|
#### DownloadWhisper クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DownloadWhisper:
|
||||||
|
def progressBar(self, progress) -> None
|
||||||
|
def downloaded(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声認識モデルのダウンロード進捗管理
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 初期化・設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
init() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- コントローラーの初期化
|
||||||
|
- 各コンポーネントの起動
|
||||||
|
- 初期設定の適用
|
||||||
|
|
||||||
|
```python
|
||||||
|
setInitMapping(init_mapping: dict) -> None
|
||||||
|
setRunMapping(run_mapping: dict) -> None
|
||||||
|
setRun(run: Callable) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- エンドポイント・コールバック設定
|
||||||
|
|
||||||
|
### 翻訳機能制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
setEnableTranslation(data) -> dict
|
||||||
|
setDisableTranslation(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 翻訳機能の有効化・無効化
|
||||||
|
|
||||||
|
```python
|
||||||
|
setSelectedTranslationEngines(data) -> dict
|
||||||
|
getSelectedTranslationEngines(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 翻訳エンジンの選択・取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
setSelectedYourLanguages(data) -> dict
|
||||||
|
setSelectedTargetLanguages(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 送信・受信言語の設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
sendMessageBox(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- メッセージの翻訳・送信処理
|
||||||
|
|
||||||
|
### 音声認識機能制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
setEnableTranscriptionSend(data) -> dict
|
||||||
|
setEnableTranscriptionReceive(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声認識機能の有効化
|
||||||
|
|
||||||
|
```python
|
||||||
|
setSelectedTranscriptionEngine(data) -> dict
|
||||||
|
getSelectedTranscriptionEngine(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声認識エンジンの選択・取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
setSelectedMicDevice(data) -> dict
|
||||||
|
setSelectedSpeakerDevice(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声デバイスの選択
|
||||||
|
|
||||||
|
```python
|
||||||
|
setMicThreshold(data) -> dict
|
||||||
|
setSpeakerThreshold(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声しきい値の設定
|
||||||
|
|
||||||
|
### VRオーバーレイ制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
setEnableOverlaySmallLog(data) -> dict
|
||||||
|
setEnableOverlayLargeLog(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- VRオーバーレイの有効化
|
||||||
|
|
||||||
|
```python
|
||||||
|
setOverlaySmallLogSettings(data) -> dict
|
||||||
|
setOverlayLargeLogSettings(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- オーバーレイ設定の更新
|
||||||
|
|
||||||
|
### WebSocket制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
setEnableWebSocketServer(data) -> dict
|
||||||
|
setDisableWebSocketServer(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- WebSocketサーバーの制御
|
||||||
|
|
||||||
|
### クリップボード制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
getClipboard() -> dict
|
||||||
|
setEnableClipboard() -> dict
|
||||||
|
setDisableClipboard() -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- クリップボード機能(コピー・ペースト)の状態取得・設定変更
|
||||||
|
- `config.ENABLE_CLIPBOARD` フラグを変更(True/False)
|
||||||
|
- 取得結果は `{"status": 200, "result": bool}`
|
||||||
|
- **注意**: `model.clipboard.enable()/disable()` は呼び出されません
|
||||||
|
|
||||||
|
### テレメトリ制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
getTelemetry() -> dict
|
||||||
|
setEnableTelemetry() -> dict
|
||||||
|
setDisableTelemetry() -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- テレメトリ(Aptabase)の有効化・無効化・状態取得
|
||||||
|
- 有効化時は `telemetryInit()` を呼び出し
|
||||||
|
- 無効化時は `telemetryShutdown()` を呼び出し
|
||||||
|
|
||||||
|
### WebSocket接続設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
setWebSocketHost(data) -> dict
|
||||||
|
setWebSocketPort(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- WebSocketホスト・ポートの設定
|
||||||
|
|
||||||
|
### システム管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
updateSoftware(data) -> dict
|
||||||
|
updateCudaSoftware(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- ソフトウェアアップデート
|
||||||
|
|
||||||
|
```python
|
||||||
|
downloadCtranslate2Weight(data) -> dict
|
||||||
|
downloadWhisperWeight(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- AIモデルのダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
feedWatchdog(data) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
- ウォッチドッグの生存シグナル送信
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from controller import Controller
|
||||||
|
|
||||||
|
# コントローラーの初期化
|
||||||
|
controller = Controller()
|
||||||
|
controller.init()
|
||||||
|
|
||||||
|
# 翻訳機能の有効化
|
||||||
|
result = controller.setEnableTranslation(None)
|
||||||
|
print(f"翻訳機能: {result}")
|
||||||
|
|
||||||
|
# メッセージ送信
|
||||||
|
message_data = {"id": "123", "message": "Hello World"}
|
||||||
|
result = controller.sendMessageBox(message_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### エンドポイント設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# マッピング設定
|
||||||
|
mapping = {
|
||||||
|
"/set/enable/translation": controller.setEnableTranslation,
|
||||||
|
"/get/data/version": controller.getVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 実行関数の設定
|
||||||
|
def run_callback(status, endpoint, result):
|
||||||
|
print(f"Status: {status}, Endpoint: {endpoint}, Result: {result}")
|
||||||
|
|
||||||
|
controller.setRun(run_callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 音声認識の設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# マイクデバイスの選択
|
||||||
|
host_data = "DirectSound"
|
||||||
|
result = controller.setSelectedMicHost(host_data)
|
||||||
|
|
||||||
|
device_data = "マイク (USB Audio Device)"
|
||||||
|
result = controller.setSelectedMicDevice(device_data)
|
||||||
|
|
||||||
|
# 音声認識の開始
|
||||||
|
result = controller.setEnableTranscriptionSend(None)
|
||||||
|
```
|
||||||
|
|
||||||
|
## レスポンス形式
|
||||||
|
|
||||||
|
全てのメソッドは統一されたレスポンス形式を返します:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"status": int, # HTTPステータスコード(200, 400, 500等)
|
||||||
|
"result": any # 処理結果(成功時)または エラーメッセージ(失敗時)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 成功レスポンス例
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"result": "翻訳機能が有効化されました"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### エラーレスポンス例
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"status": 400,
|
||||||
|
"result": "Invalid device selection"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 詳細状態管理
|
||||||
|
|
||||||
|
### システム状態
|
||||||
|
|
||||||
|
- 各機能の有効・無効状態
|
||||||
|
- デバイスの接続状態
|
||||||
|
- ネットワーク接続状態
|
||||||
|
|
||||||
|
### エラー状態
|
||||||
|
|
||||||
|
- デバイスエラー
|
||||||
|
- 翻訳エンジンエラー
|
||||||
|
- VRAMオーバーフローエラー
|
||||||
|
|
||||||
|
### 初期化状態
|
||||||
|
|
||||||
|
- 段階的な初期化プロセス
|
||||||
|
- 依存関係の解決状態
|
||||||
|
|
||||||
|
## イベント処理
|
||||||
|
|
||||||
|
### 音声認識イベント
|
||||||
|
|
||||||
|
```python
|
||||||
|
micMessage(result: dict) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- マイク音声認識結果の処理
|
||||||
|
- 翻訳・フィルタリング・送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
speakerMessage(result: dict) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- スピーカー音声認識結果の処理
|
||||||
|
|
||||||
|
### ダウンロードイベント
|
||||||
|
|
||||||
|
- 進捗通知
|
||||||
|
- 完了通知
|
||||||
|
- エラー通知
|
||||||
|
|
||||||
|
### デバイス変更イベント
|
||||||
|
|
||||||
|
- マイク・スピーカーの選択変更
|
||||||
|
- 計算デバイスの変更
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 直接依存
|
||||||
|
|
||||||
|
- `config`: 設定管理
|
||||||
|
- `model`: コアモデル機能
|
||||||
|
- `device_manager`: デバイス管理
|
||||||
|
- `utils`: ユーティリティ機能
|
||||||
|
|
||||||
|
### 間接依存
|
||||||
|
|
||||||
|
- 各種モデルモジュール(翻訳、音声認識等)
|
||||||
|
- VRオーバーレイモジュール
|
||||||
|
- 通信モジュール
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### エラー構造
|
||||||
|
- すべて `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` など)
|
||||||
|
|
||||||
|
### 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 に格納
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
### 遅延初期化
|
||||||
|
|
||||||
|
- 必要な時点での機能初期化
|
||||||
|
- メモリ使用量の最適化
|
||||||
|
|
||||||
|
### 非同期処理
|
||||||
|
|
||||||
|
- バックグラウンドでの重い処理
|
||||||
|
- UI の応答性維持
|
||||||
|
|
||||||
|
### キャッシュ機能
|
||||||
|
|
||||||
|
- 設定値のキャッシュ
|
||||||
|
- 翻訳結果のキャッシュ
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- すべてのメソッドは例外安全である
|
||||||
|
- 設定変更は即座に config に反映される
|
||||||
|
- 重い処理は別スレッドで実行される
|
||||||
|
- VR機能は適切な環境でのみ動作する
|
||||||
|
- ネットワーク機能はオフライン時に制限される
|
||||||
|
|
||||||
|
## セキュリティ考慮事項
|
||||||
|
|
||||||
|
- 外部入力の適切な検証
|
||||||
|
- APIキーの安全な管理
|
||||||
|
- ファイルアクセスの制限
|
||||||
|
- ネットワーク通信の暗号化(該当する場合)
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
# mainloop.py - VRCTメインループモジュール
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRCTアプリケーションのメインイベントループを管理するモジュールです。標準入力からのJSONリクエストを処理し、適切なコントローラーメソッドを呼び出してレスポンスを返す、アプリケーションの中枢的な役割を担います。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
### 新規エンドポイントと run_mapping 拡張
|
||||||
|
|
||||||
|
- VRAM 関連エラー通知エンドポイント追加: `/run/error_translation_chat_vram_overflow` など 5 種類 (翻訳/音声認識送受信別)
|
||||||
|
- ローカル LLM (LMStudio/Ollama) モデルリスト通知: `/run/selectable_lmstudio_model_list`, `/run/selectable_ollama_model_list` と選択モデル `/run/selected_*_model`
|
||||||
|
- 従来の Plamo/Gemini/OpenAI モデル取得通知と形式統一
|
||||||
|
- LMStudio/Ollama 接続確認エンドポイント: `/get/data/lmstudio_connection`, `/get/data/ollama_connection` を `/run/lmstudio_connection`, `/run/ollama_connection` に移動して非同期通知統一
|
||||||
|
- 文字変換エンドポイント追加: `/set/data/convert_message_to_romaji`, `/set/data/convert_message_to_hiragana`, `/set/enable/convert_message_to_romaji`, `/set/enable/convert_message_to_hiragana`, `/set/disable/convert_message_to_romaji`, `/set/disable/convert_message_to_hiragana` で音訳機能制御
|
||||||
|
|
||||||
|
### エンドポイントロックキー正規化
|
||||||
|
|
||||||
|
- `/set/enable/*` `/set/disable/*` の競合を `/lock/set/<name>` に正規化し排他制御強化
|
||||||
|
- ロック取得失敗時は再キュー投入し軽量リトライでデッドロック防止
|
||||||
|
|
||||||
|
### 並列ワーカー処理の安定化
|
||||||
|
|
||||||
|
- ハンドラ処理後に短い `sleep(0.2)` により大量高速連続要求時のスレッド飢餓を緩和
|
||||||
|
- 423 (Locked) ステータス時に指数的ではなく固定短期リトライ採用で応答時間予測性向上
|
||||||
|
|
||||||
|
### VRAM エラーフォールバック連携
|
||||||
|
|
||||||
|
- Controller が VRAM 検出し翻訳 OFF / CTranslate2 フォールバック後、run_mapping 経由で UI へ状態反映
|
||||||
|
- ハンドラはエラー時でもスレッド継続し `Internal error` を 500 応答で返しつつログ出力
|
||||||
|
|
||||||
|
### モデルリスト動的更新通知
|
||||||
|
|
||||||
|
- 認証・接続成功後に対象モデルリスト/選択モデルを run で逐次通知 (Plamo/Gemini/OpenAI/LMStudio/Ollama)
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 安定性 | 排他制御と再キュー投入で競合時の落ち込み回避 |
|
||||||
|
| 可観測性 | VRAM/ダウンロード進捗/モデル更新イベントを run 経由で即時通知 |
|
||||||
|
| 拡張性 | 新規ローカル LLM エンジン追加に伴う汎用モデル通知フォーマット統一 |
|
||||||
|
| 応答予測性 | 固定リトライ戦略で待ち時間が読みやすい |
|
||||||
|
| フォールバック | VRAM エラー時の自動翻訳停止と CTranslate2 への切替連携 |
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### リクエスト処理システム
|
||||||
|
|
||||||
|
- JSON形式の標準入力からのリクエスト受信
|
||||||
|
- エンドポイントベースのルーティング
|
||||||
|
- 非同期・並列処理対応
|
||||||
|
|
||||||
|
### エンドポイント管理
|
||||||
|
|
||||||
|
- RESTライクなエンドポイント構造
|
||||||
|
- 機能別のエンドポイント分類
|
||||||
|
- 排他制御によるスレッドセーフティ
|
||||||
|
|
||||||
|
### 初期化システム
|
||||||
|
|
||||||
|
- アプリケーション設定の初期化
|
||||||
|
- コンポーネント間の依存関係解決
|
||||||
|
- 段階的な機能有効化
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Main クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Main:
|
||||||
|
def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
- メインループの制御
|
||||||
|
- ワーカースレッドプール管理
|
||||||
|
- エンドポイント排他制御
|
||||||
|
|
||||||
|
## エンドポイント分類
|
||||||
|
|
||||||
|
### 機能制御系
|
||||||
|
|
||||||
|
```text
|
||||||
|
/set/enable/* - 各機能の有効化
|
||||||
|
/set/disable/* - 各機能の無効化
|
||||||
|
```
|
||||||
|
|
||||||
|
### データ操作系
|
||||||
|
|
||||||
|
```text
|
||||||
|
/get/data/* - 設定データの取得
|
||||||
|
/set/data/* - 設定データの更新
|
||||||
|
/delete/data/* - データの削除
|
||||||
|
```
|
||||||
|
|
||||||
|
### 実行系
|
||||||
|
|
||||||
|
```text
|
||||||
|
/run/* - 各種処理の実行
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要エンドポイント
|
||||||
|
|
||||||
|
### 翻訳機能
|
||||||
|
|
||||||
|
- `/set/enable/translation`: 翻訳機能の有効化
|
||||||
|
- `/set/disable/translation`: 翻訳機能の無効化
|
||||||
|
- `/set/data/selected_translation_engines`: 翻訳エンジンの選択
|
||||||
|
- `/run/send_message_box`: メッセージ送信
|
||||||
|
|
||||||
|
### 音声認識機能
|
||||||
|
|
||||||
|
- `/set/enable/transcription_send`: 送信音声認識の有効化
|
||||||
|
- `/set/enable/transcription_receive`: 受信音声認識の有効化
|
||||||
|
- `/set/data/selected_transcription_engine`: 音声認識エンジン選択
|
||||||
|
|
||||||
|
### VR機能
|
||||||
|
|
||||||
|
- `/set/data/overlay_small_log_settings`: 小型オーバーレイ設定
|
||||||
|
- `/set/data/overlay_large_log_settings`: 大型オーバーレイ設定
|
||||||
|
|
||||||
|
### WebSocket機能
|
||||||
|
|
||||||
|
- `/set/enable/websocket_server`: WebSocketサーバー有効化
|
||||||
|
- `/set/data/websocket_host`: サーバーホスト設定
|
||||||
|
- `/set/data/websocket_port`: サーバーポート設定
|
||||||
|
|
||||||
|
### クリップボード機能
|
||||||
|
|
||||||
|
- `/get/data/clipboard`: クリップボード機能の状態取得(`config.ENABLE_CLIPBOARD` の値)
|
||||||
|
- `/set/enable/clipboard`: `config.ENABLE_CLIPBOARD` を True に設定
|
||||||
|
- `/set/disable/clipboard`: `config.ENABLE_CLIPBOARD` を False に設定
|
||||||
|
|
||||||
|
### テレメトリ機能
|
||||||
|
|
||||||
|
- `/get/data/telemetry`: テレメトリの状態取得
|
||||||
|
- `/set/enable/telemetry`: テレメトリの有効化(Aptabase初期化)
|
||||||
|
- `/set/disable/telemetry`: テレメトリの無効化(シャットダウン)
|
||||||
|
|
||||||
|
### システム管理
|
||||||
|
|
||||||
|
- `/run/update_software`: ソフトウェアアップデート
|
||||||
|
- `/run/download_ctranslate2_weight`: 翻訳モデルダウンロード
|
||||||
|
- `/run/download_whisper_weight`: 音声認識モデルダウンロード
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### リクエスト処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
receiver() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 標準入力からのJSONリクエスト受信
|
||||||
|
- パースエラーの適切な処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
handleRequest(endpoint: str, data: Any = None) -> tuple
|
||||||
|
```
|
||||||
|
|
||||||
|
- エンドポイント処理の実行
|
||||||
|
- ステータスコードと結果の返却
|
||||||
|
|
||||||
|
```python
|
||||||
|
handler() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- ワーカースレッドのメイン処理
|
||||||
|
- キューからのリクエスト取得・処理
|
||||||
|
|
||||||
|
### スレッド管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
startReceiver() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- レシーバースレッドの起動
|
||||||
|
|
||||||
|
```python
|
||||||
|
startHandler() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- ハンドラースレッドプールの起動
|
||||||
|
|
||||||
|
```python
|
||||||
|
start() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 全スレッドの起動
|
||||||
|
|
||||||
|
```python
|
||||||
|
stop(wait: float = 2.0) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 全スレッドの安全な停止
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mainloop import main_instance
|
||||||
|
|
||||||
|
# メインループの開始
|
||||||
|
main_instance.start()
|
||||||
|
|
||||||
|
# ウォッチドッグコールバックの設定
|
||||||
|
main_instance.controller.setWatchdogCallback(main_instance.stop)
|
||||||
|
|
||||||
|
# コントローラーの初期化
|
||||||
|
main_instance.controller.init()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 直接リクエスト処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# エンドポイントの直接呼び出し
|
||||||
|
result, status = main_instance.handleRequest("/get/data/version", None)
|
||||||
|
print(f"バージョン: {result}")
|
||||||
|
|
||||||
|
# 翻訳機能の有効化
|
||||||
|
result, status = main_instance.handleRequest("/set/enable/translation", None)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 標準入力からの処理
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "/run/send_message_box",
|
||||||
|
"data": "eyJpZCI6ICIxMjMiLCAibWVzc2FnZSI6ICJIZWxsbyBXb3JsZCJ9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## リクエスト形式
|
||||||
|
|
||||||
|
### 入力形式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "string", // 必須:処理対象のエンドポイント
|
||||||
|
"data": "string|null" // オプション:Base64エンコード済みデータ
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出力形式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200, // HTTPステータスコード
|
||||||
|
"endpoint": "string", // 処理されたエンドポイント
|
||||||
|
"result": "any" // 処理結果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ステータスコード
|
||||||
|
|
||||||
|
- `200`: 成功
|
||||||
|
- `400`: 不正なリクエスト
|
||||||
|
- `404`: 存在しないエンドポイント
|
||||||
|
- `423`: ロック中(機能が無効化されている)
|
||||||
|
- `500`: 内部エラー
|
||||||
|
|
||||||
|
## 排他制御
|
||||||
|
|
||||||
|
### ロック機能
|
||||||
|
|
||||||
|
- enable/disableペアは同一ロックキーを共有
|
||||||
|
- 同一機能の同時実行を防止
|
||||||
|
- デッドロックを回避する設計
|
||||||
|
|
||||||
|
### ロックキー正規化
|
||||||
|
|
||||||
|
```python
|
||||||
|
/set/enable/translation -> /lock/set/translation
|
||||||
|
/set/disable/translation -> /lock/set/translation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 初期化プロセス
|
||||||
|
|
||||||
|
### 段階的初期化
|
||||||
|
|
||||||
|
1. コントローラーの初期化
|
||||||
|
2. デバイスマネージャーの初期化
|
||||||
|
3. モデルの初期化
|
||||||
|
4. 各機能の段階的有効化
|
||||||
|
|
||||||
|
### 初期化mapping
|
||||||
|
|
||||||
|
- `/get/data/*`エンドポイントから初期化設定を自動抽出
|
||||||
|
- システム起動時の設定復元
|
||||||
|
|
||||||
|
## ログ機能
|
||||||
|
|
||||||
|
### プロセスログ
|
||||||
|
|
||||||
|
- 全リクエスト・レスポンスの記録
|
||||||
|
- JSON形式での構造化ログ
|
||||||
|
|
||||||
|
### エラーログ
|
||||||
|
|
||||||
|
- 例外の詳細記録
|
||||||
|
- スタックトレースの保存
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 直接依存
|
||||||
|
|
||||||
|
- `controller`: ビジネスロジック制御
|
||||||
|
- `utils`: ユーティリティ機能(ログ、エンコード等)
|
||||||
|
|
||||||
|
### 間接依存
|
||||||
|
|
||||||
|
- `config`: 設定管理
|
||||||
|
- `model`: コアモデル機能
|
||||||
|
- `device_manager`: デバイス管理
|
||||||
|
|
||||||
|
## 設定項目
|
||||||
|
|
||||||
|
### ワーカー数
|
||||||
|
|
||||||
|
```python
|
||||||
|
DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数
|
||||||
|
```
|
||||||
|
|
||||||
|
### タイムアウト
|
||||||
|
|
||||||
|
- キュー待機タイムアウト: 0.5秒
|
||||||
|
- スレッド停止待機: 2.0秒
|
||||||
|
- 処理安定化待機: 0.2秒
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- JSONパースエラーの適切な処理
|
||||||
|
- エンドポイント実行エラーのキャッチ
|
||||||
|
- スレッドセーフなエラーログ記録
|
||||||
|
- グレースフルシャットダウン
|
||||||
|
|
||||||
|
## パフォーマンス特性
|
||||||
|
|
||||||
|
### スループット
|
||||||
|
|
||||||
|
- 複数ワーカーによる並列処理
|
||||||
|
- ノンブロッキングI/O
|
||||||
|
|
||||||
|
### レイテンシ
|
||||||
|
|
||||||
|
- キューイング遅延の最小化
|
||||||
|
- 排他制御による一時的な遅延あり
|
||||||
|
|
||||||
|
### メモリ使用量
|
||||||
|
|
||||||
|
- リクエストキューのサイズ制限なし(要注意)
|
||||||
|
- スレッドプールによる固定オーバーヘッド
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 標準入力をブロッキングで読み取るため、パイプ経由での使用を想定
|
||||||
|
- エンドポイント名の大文字小文字は区別される
|
||||||
|
- Base64データは自動的にデコードされる
|
||||||
|
- 長時間のブロッキング処理は他のリクエストに影響する可能性
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
# model.py - VRCTコアモデルクラス
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRCTアプリケーションの中核となるModelクラスを定義するモジュールです。音声認識、翻訳、VRオーバーレイ、OSC通信、WebSocketサーバーなどの主要機能を統合管理し、システム全体の動作を制御します。
|
||||||
|
|
||||||
|
## 最近の更新 (2026-01-11)
|
||||||
|
|
||||||
|
### クリップボード機能
|
||||||
|
|
||||||
|
- `clipboard` インスタンスを Model 内に保持
|
||||||
|
- `setCopyToClipboard(text: str) -> bool`: 翻訳結果をクリップボードにコピー
|
||||||
|
- `setPasteFromClipboard() -> bool`: Ctrl+V でペースト実行(VRChat ウィンドウ自動フォーカス)
|
||||||
|
- OpenVR による VRChat アプリ名の自動検出
|
||||||
|
|
||||||
|
### テレメトリ機能
|
||||||
|
|
||||||
|
- `telemetryInit(enabled: bool, app_version: str)`: Aptabase を用いたテレメトリ初期化
|
||||||
|
- `telemetryShutdown()`: テレメトリのシャットダウン(app_closed イベント送信)
|
||||||
|
- `telemetryTouchActivity()`: ユーザーアクティビティの記録
|
||||||
|
- `telemetry.*()`: イベント送信メソッド群
|
||||||
|
- デフォルト有効、ユーザー制御可能な設計
|
||||||
|
|
||||||
|
### その他の更新
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
### VRAMエラー検出とフォールバック
|
||||||
|
|
||||||
|
- `detectVRAMError()` を追加し CUDA メモリ関連メッセージ/独自例外 `VRAM_OUT_OF_MEMORY` を判別
|
||||||
|
- 翻訳/音声認識実行中に VRAM エラー検出時、Controller 側で翻訳機能を無効化し CTranslate2 へフォールバックする運用を支援
|
||||||
|
- エラー詳細文字列を UI へ通知するためのメッセージ抽出を標準化
|
||||||
|
|
||||||
|
### CTranslate2 言語マッピングネスト対応
|
||||||
|
|
||||||
|
- `getListLanguageAndCountry()` / `findTranslationEngines()` が `translation_lang['CTranslate2'][CTRANSLATE2_WEIGHT_TYPE]['source']` を参照するネスト構造へ更新
|
||||||
|
- ウェイト種別切替時に対応言語集合が動的に変化しエンジン再判定をトリガー
|
||||||
|
|
||||||
|
### ローカル LLM 翻訳エンジン統合
|
||||||
|
|
||||||
|
- LMStudio / Ollama 用クライアント初期化・モデルリスト取得メソッド追加: `authenticationTranslatorLMStudio()`, `getTranslatorLMStudioModelList()`, `setTranslatorLMStudioModel()`, `updateTranslatorLMStudioClient()` など
|
||||||
|
- Ollama も同様のインターフェースで統一 (`getTranslatorOllamaModelList`, `setTranslatorOllamaModel`, `updateTranslatorOllamaClient`)
|
||||||
|
- Plamo / Gemini / OpenAI と同一フォーマットでモデル選択ロジックを実装し Controller からの呼び出しを簡素化
|
||||||
|
|
||||||
|
### トークナイザ・リソース取得安定化
|
||||||
|
|
||||||
|
- CTranslate2 トークナイザダウンロード処理を `downloadCTranslate2ModelTokenizer()` で明示化し PyInstaller パス周りの不整合回避
|
||||||
|
- フォントパス探索は OverlayImage 側へ委譲 (`OverlayImage(config.PATH_LOCAL)`) し Model は生成と更新呼び出しのみ保持
|
||||||
|
|
||||||
|
### 翻訳失敗時のフェールセーフ再試行
|
||||||
|
|
||||||
|
- `getTranslate()` 内で翻訳失敗(非文字列)時に CTranslate2 をリトライループして安定した結果を返却
|
||||||
|
- 成功判定フラグを返却し上位層でエンジン制限エラー検出/フォールバックを容易化
|
||||||
|
|
||||||
|
### キーワードフィルタ再初期化改善
|
||||||
|
|
||||||
|
- `resetKeywordProcessor()` でインスタンス再生成し `addKeywords()` により設定変更後のフィルタ更新即時反映
|
||||||
|
|
||||||
|
### WebSocket サーバー管理強化
|
||||||
|
|
||||||
|
- 非同期サーバー起動を `asyncio.run` ラッパースレッドで安定化
|
||||||
|
- ループフラグ `websocket_server_loop` と状態フラグ `websocket_server_alive` を追加し安全な停止処理と存活確認を標準化
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 安定性 | VRAM 検出とフェールセーフ再試行で異常終了回避 |
|
||||||
|
| 拡張性 | ローカル LLM 統合によりネットワーク不要環境対応 |
|
||||||
|
| 柔軟性 | CTranslate2 ウェイト種別に応じた言語集合動的切替 |
|
||||||
|
| 保守性 | トークナイザ/フォント取得責務分離で可読性向上 |
|
||||||
|
| 観測性 | エラー詳細標準化により UI/ログでの診断容易 |
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### シングルトンパターン
|
||||||
|
|
||||||
|
- アプリケーション全体で単一のModelインスタンスを保証
|
||||||
|
- 遅延初期化による軽量なインポート
|
||||||
|
|
||||||
|
### 音声認識機能
|
||||||
|
|
||||||
|
- マイク音声のリアルタイム文字起こし
|
||||||
|
- スピーカー出力の音声認識
|
||||||
|
- エネルギーレベル監視
|
||||||
|
- 複数言語対応
|
||||||
|
|
||||||
|
### 翻訳機能
|
||||||
|
|
||||||
|
- 複数の翻訳エンジン対応(DeepL、Google、CTranslate2等)
|
||||||
|
- 言語自動検出
|
||||||
|
- バッチ翻訳処理
|
||||||
|
|
||||||
|
### VRオーバーレイ
|
||||||
|
|
||||||
|
- OpenVR統合
|
||||||
|
- 小型・大型ログオーバーレイ
|
||||||
|
- 動的配置・透明度制御
|
||||||
|
|
||||||
|
### OSC通信
|
||||||
|
|
||||||
|
- VRChatとのOSC通信
|
||||||
|
- タイピング状態の同期
|
||||||
|
- ミュート状態の監視
|
||||||
|
|
||||||
|
### WebSocketサーバー
|
||||||
|
|
||||||
|
- 外部アプリケーションとの通信
|
||||||
|
- リアルタイムメッセージ配信
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### threadFnc クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class threadFnc(Thread):
|
||||||
|
def __init__(self, fnc, end_fnc=None, daemon: bool = True, *args, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 関数を繰り返し実行するスレッドラッパー
|
||||||
|
- 一時停止・再開機能
|
||||||
|
- エラー保護機能
|
||||||
|
|
||||||
|
### Model クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Model:
|
||||||
|
def __new__(cls) # シングルトンパターン
|
||||||
|
def init(self) # 重い初期化処理
|
||||||
|
def ensure_initialized(self) # 遅延初期化
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 初期化・管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
init() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 全コンポーネントの初期化
|
||||||
|
- 重い処理のため明示的に呼び出し
|
||||||
|
|
||||||
|
```python
|
||||||
|
ensure_initialized() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 必要時の自動初期化
|
||||||
|
- 安全な遅延初期化
|
||||||
|
|
||||||
|
### 翻訳機能メソッド
|
||||||
|
|
||||||
|
```python
|
||||||
|
getInputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 入力メッセージの多言語翻訳
|
||||||
|
- 成功フラグも同時に返却
|
||||||
|
|
||||||
|
```python
|
||||||
|
getOutputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 出力メッセージの翻訳(逆方向)
|
||||||
|
|
||||||
|
```python
|
||||||
|
authenticationTranslatorDeepLAuthKey(auth_key) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- DeepL APIキーの認証
|
||||||
|
|
||||||
|
### 音声認識機能メソッド
|
||||||
|
|
||||||
|
```python
|
||||||
|
startMicTranscript(fnc: Callable) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- マイク音声認識の開始
|
||||||
|
- コールバック関数で結果を通知
|
||||||
|
|
||||||
|
```python
|
||||||
|
startSpeakerTranscript(fnc: Callable) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- スピーカー音声認識の開始
|
||||||
|
|
||||||
|
```python
|
||||||
|
pauseMicTranscript() -> None
|
||||||
|
resumeMicTranscript() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声認識の一時停止・再開
|
||||||
|
|
||||||
|
```python
|
||||||
|
startCheckMicEnergy(fnc: Callable) -> None
|
||||||
|
startCheckSpeakerEnergy(fnc: Callable) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声エネルギーレベルの監視
|
||||||
|
|
||||||
|
### VRオーバーレイ機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
createOverlayImageSmallLog(message, your_language, translation, target_language) -> Image
|
||||||
|
```
|
||||||
|
|
||||||
|
- 小型ログオーバーレイ画像の生成
|
||||||
|
|
||||||
|
```python
|
||||||
|
createOverlayImageLargeLog(message_type, message, your_language, translation, target_language) -> Image
|
||||||
|
```
|
||||||
|
|
||||||
|
- 大型ログオーバーレイ画像の生成
|
||||||
|
|
||||||
|
```python
|
||||||
|
updateOverlaySmallLogSettings() -> None
|
||||||
|
updateOverlayLargeLogSettings() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- オーバーレイ設定の更新
|
||||||
|
|
||||||
|
### OSC通信機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
oscSendMessage(message: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- VRChatへのメッセージ送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
oscStartSendTyping() -> None
|
||||||
|
oscStopSendTyping() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- タイピング状態の通知
|
||||||
|
|
||||||
|
```python
|
||||||
|
setMuteSelfStatus() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- VRChatミュート状態の取得
|
||||||
|
|
||||||
|
### WebSocket機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
startWebSocketServer(host: str, port: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- WebSocketサーバーの起動
|
||||||
|
|
||||||
|
```python
|
||||||
|
websocketSendMessage(message_dict: dict) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- 全クライアントへのメッセージ送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
checkWebSocketServerAlive() -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- サーバー稼働状態の確認
|
||||||
|
|
||||||
|
### ファイルダウンロード機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
downloadCTranslate2ModelWeight(weight_type, callback=None, end_callback=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 翻訳モデルのダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
downloadWhisperModelWeight(weight_type, callback=None, end_callback=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 音声認識モデルのダウンロード
|
||||||
|
|
||||||
|
### ウォッチドッグ機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
startWatchdog() -> None
|
||||||
|
feedWatchdog() -> None
|
||||||
|
setWatchdogCallback(callback: Callable) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- システム監視とタイムアウト処理
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from model import model
|
||||||
|
|
||||||
|
# 明示的な初期化(推奨)
|
||||||
|
model.init()
|
||||||
|
|
||||||
|
# または自動初期化
|
||||||
|
model.ensure_initialized()
|
||||||
|
|
||||||
|
# 翻訳機能の使用
|
||||||
|
translations, success_flags = model.getInputTranslate("Hello World")
|
||||||
|
|
||||||
|
# 音声認識の開始
|
||||||
|
def on_transcript_result(result):
|
||||||
|
print(f"認識結果: {result}")
|
||||||
|
|
||||||
|
model.startMicTranscript(on_transcript_result)
|
||||||
|
```
|
||||||
|
|
||||||
|
### VRオーバーレイの使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# オーバーレイの開始
|
||||||
|
model.startOverlay()
|
||||||
|
|
||||||
|
# 画像の作成と更新
|
||||||
|
img = model.createOverlayImageSmallLog(
|
||||||
|
message="Hello",
|
||||||
|
your_language="English",
|
||||||
|
translation=["こんにちは"],
|
||||||
|
target_language={"1": {"language": "Japanese", "enable": True}}
|
||||||
|
)
|
||||||
|
model.updateOverlaySmallLog(img)
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocketサーバーの使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# サーバー起動
|
||||||
|
model.startWebSocketServer("127.0.0.1", 8765)
|
||||||
|
|
||||||
|
# メッセージ送信
|
||||||
|
message = {"type": "translation", "text": "Hello", "translation": "こんにちは"}
|
||||||
|
success = model.websocketSendMessage(message)
|
||||||
|
```
|
||||||
|
|
||||||
|
### クリップボード機能の使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# テキストをクリップボードにコピー
|
||||||
|
text = "翻訳結果"
|
||||||
|
success = model.setCopyToClipboard(text)
|
||||||
|
|
||||||
|
# ペースト実行(VRChat ウィンドウに自動フォーカス)
|
||||||
|
success = model.setPasteFromClipboard()
|
||||||
|
```
|
||||||
|
|
||||||
|
### テレメトリの初期化・シャットダウン
|
||||||
|
|
||||||
|
```python
|
||||||
|
# テレメトリ初期化(アプリ起動時)
|
||||||
|
model.telemetryInit(enabled=config.ENABLE_TELEMETRY, app_version=config.VERSION)
|
||||||
|
|
||||||
|
# テレメトリシャットダウン(アプリ終了時)
|
||||||
|
model.telemetryShutdown()
|
||||||
|
|
||||||
|
# アクティビティ記録
|
||||||
|
model.telemetryTouchActivity()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須モジュール
|
||||||
|
|
||||||
|
- `controller`: アプリケーション制御
|
||||||
|
- `config`: 設定管理
|
||||||
|
- `device_manager`: デバイス管理
|
||||||
|
|
||||||
|
### 音声・翻訳関連
|
||||||
|
|
||||||
|
- `models.transcription.*`: 音声認識
|
||||||
|
- `models.translation.*`: 翻訳機能
|
||||||
|
- `models.transliteration.*`: 音写変換
|
||||||
|
|
||||||
|
### VR・通信関連
|
||||||
|
|
||||||
|
- `models.overlay.*`: VRオーバーレイ
|
||||||
|
- `models.osc.*`: OSC通信
|
||||||
|
- `models.websocket.*`: WebSocket通信
|
||||||
|
|
||||||
|
### ユーティリティ
|
||||||
|
|
||||||
|
- `models.watchdog.*`: 監視機能
|
||||||
|
- `utils`: 共通ユーティリティ
|
||||||
|
- `flashtext`: キーワードフィルタリング
|
||||||
|
|
||||||
|
## 設定依存関係
|
||||||
|
|
||||||
|
多くの機能がconfigモジュールの設定に依存:
|
||||||
|
|
||||||
|
- 音声認識設定(しきい値、タイムアウト等)
|
||||||
|
- 翻訳設定(エンジン選択、言語設定等)
|
||||||
|
- VR設定(オーバーレイ位置、透明度等)
|
||||||
|
- OSC設定(IPアドレス、ポート等)
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- 初期化エラーの適切な処理
|
||||||
|
- VRAM不足エラーの検出と対応
|
||||||
|
- ネットワークエラーの回復機能
|
||||||
|
- スレッドセーフティの保証
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 重い初期化処理のため、明示的な初期化を推奨
|
||||||
|
- OpenVR環境が必要(VRオーバーレイ使用時)
|
||||||
|
- CUDA環境推奨(高速な音声認識・翻訳)
|
||||||
|
- WebSocketサーバーは非同期で動作
|
||||||
|
- 音声デバイスのアクセス権限が必要
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
- 遅延初期化によるメモリ使用量の最適化
|
||||||
|
- スレッドプールによる並行処理
|
||||||
|
- モデルの重複読み込み防止
|
||||||
|
- キューイングによる非同期処理
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
# osc.py - OSC通信・OSCQueryプロトコル管理
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRChatとの高度なOSC(Open Sound Control)通信を管理する包括的なシステムです。基本的なOSCメッセージ送信に加え、OSCQueryプロトコルによる双方向通信、パラメータ監視、自動サービス発見機能を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### OSC通信機能
|
||||||
|
- VRChatチャットボックスへのメッセージ送信
|
||||||
|
- タイピング状態の制御
|
||||||
|
- パラメータ値の動的取得
|
||||||
|
|
||||||
|
### OSCQuery対応
|
||||||
|
- 自動サービス発見・接続
|
||||||
|
- リアルタイムパラメータ監視
|
||||||
|
- 双方向エンドポイント公開
|
||||||
|
|
||||||
|
### 堅牢性機能
|
||||||
|
- 防御的プログラミング設計
|
||||||
|
- 欠損ライブラリの優雅な処理
|
||||||
|
- 自動エラー復旧機構
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### OSCHandler クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OSCHandler:
|
||||||
|
def __init__(self, ip_address: str = "127.0.0.1", port: int = 9000) -> None:
|
||||||
|
self.is_osc_query_enabled: bool
|
||||||
|
self.osc_ip_address: str
|
||||||
|
self.osc_port: int
|
||||||
|
self.udp_client: udp_client.SimpleUDPClient
|
||||||
|
self.osc_server: Optional[osc_server.ThreadingOSCUDPServer]
|
||||||
|
self.osc_query_service: Optional[OSCQueryService]
|
||||||
|
self.browser: Optional[OSCQueryBrowser]
|
||||||
|
```
|
||||||
|
|
||||||
|
OSC通信の中核管理クラス
|
||||||
|
|
||||||
|
#### 属性
|
||||||
|
- **is_osc_query_enabled**: OSCQuery機能の有効性フラグ
|
||||||
|
- **osc_ip_address**: 送信先IPアドレス
|
||||||
|
- **osc_port**: UDP通信ポート
|
||||||
|
- **udp_client**: OSC送信クライアント
|
||||||
|
- **osc_server**: ローカルOSCサーバー
|
||||||
|
- **osc_query_service**: OSCQueryサービスインスタンス
|
||||||
|
- **browser**: OSCQueryブラウザー
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### メッセージ送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
def sendMessage(self, message: str = "", notification: bool = True) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
VRChatチャットボックスにメッセージを送信
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **message**: 送信するテキストメッセージ
|
||||||
|
- **notification**: 通知フラグ(音・表示の有無)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def sendTyping(self, flag: bool = False) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
タイピング状態をVRChatに送信
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **flag**: タイピング中フラグ
|
||||||
|
|
||||||
|
### パラメータ監視
|
||||||
|
|
||||||
|
```python
|
||||||
|
def getOSCParameterMuteSelf() -> Optional[bool]
|
||||||
|
```
|
||||||
|
|
||||||
|
VRChatのMuteSelfパラメータ値を取得
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **Optional[bool]**: ミュート状態(取得失敗時はNone)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def getOSCParameterValue(self, address: str) -> Any
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のOSCパラメータ値を取得
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **address**: OSCアドレス(例:"/avatar/parameters/MuteSelf")
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **Any**: パラメータ値(取得失敗時はNone)
|
||||||
|
|
||||||
|
### 設定変更
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setOscIpAddress(self, ip_address: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
送信先IPアドレスを変更し、サービスを再初期化
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **ip_address**: 新しいIPアドレス
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setOscPort(self, port: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
送信ポートを変更し、サービスを再初期化
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **port**: 新しいUDPポート番号
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的なメッセージ送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.osc.osc import OSCHandler
|
||||||
|
|
||||||
|
# OSCハンドラーの初期化
|
||||||
|
osc = OSCHandler(ip_address="127.0.0.1", port=9000)
|
||||||
|
|
||||||
|
# チャットボックスにメッセージを送信
|
||||||
|
osc.sendMessage("こんにちは、VRChat!", notification=True)
|
||||||
|
|
||||||
|
# タイピング状態の制御
|
||||||
|
osc.sendTyping(True) # タイピング開始
|
||||||
|
# ... 実際のタイピング処理 ...
|
||||||
|
osc.sendTyping(False) # タイピング終了
|
||||||
|
|
||||||
|
# 再度メッセージ送信
|
||||||
|
osc.sendMessage("翻訳完了しました", notification=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### リモートVRChatへの接続
|
||||||
|
|
||||||
|
```python
|
||||||
|
# リモートVRChatインスタンスへの接続
|
||||||
|
remote_osc = OSCHandler(ip_address="192.168.1.100", port=9000)
|
||||||
|
|
||||||
|
# OSCQuery機能は自動的に無効化される
|
||||||
|
print(f"OSCQuery有効: {remote_osc.getIsOscQueryEnabled()}") # False
|
||||||
|
|
||||||
|
# 基本的なメッセージ送信は利用可能
|
||||||
|
remote_osc.sendMessage("リモートからの翻訳結果", notification=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### パラメータ監視(ローカル接続時のみ)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ローカル接続でのパラメータ監視
|
||||||
|
local_osc = OSCHandler(ip_address="127.0.0.1", port=9000)
|
||||||
|
|
||||||
|
if local_osc.getIsOscQueryEnabled():
|
||||||
|
# MuteSelfパラメータの監視
|
||||||
|
mute_status = local_osc.getOSCParameterMuteSelf()
|
||||||
|
|
||||||
|
if mute_status is not None:
|
||||||
|
if mute_status:
|
||||||
|
print("ユーザーはミュート中です")
|
||||||
|
else:
|
||||||
|
print("ユーザーはミュート解除中です")
|
||||||
|
else:
|
||||||
|
print("MuteSelfパラメータの取得に失敗しました")
|
||||||
|
|
||||||
|
# カスタムパラメータの監視
|
||||||
|
custom_value = local_osc.getOSCParameterValue("/avatar/parameters/CustomParam")
|
||||||
|
if custom_value is not None:
|
||||||
|
print(f"カスタムパラメータ値: {custom_value}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 双方向OSC通信の設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_mute_change(address, *args):
|
||||||
|
"""ミュート状態変更のハンドラー"""
|
||||||
|
print(f"ミュート状態が変更されました: {args}")
|
||||||
|
|
||||||
|
def handle_typing_change(address, *args):
|
||||||
|
"""タイピング状態変更のハンドラー"""
|
||||||
|
print(f"タイピング状態: {args}")
|
||||||
|
|
||||||
|
def handle_chatbox_input(address, *args):
|
||||||
|
"""チャットボックス入力のハンドラー"""
|
||||||
|
print(f"チャットボックス入力: {args}")
|
||||||
|
|
||||||
|
# OSCパラメータハンドラーの設定
|
||||||
|
osc_handlers = {
|
||||||
|
"/avatar/parameters/MuteSelf": handle_mute_change,
|
||||||
|
"/chatbox/typing": handle_typing_change,
|
||||||
|
"/chatbox/input": handle_chatbox_input
|
||||||
|
}
|
||||||
|
|
||||||
|
osc = OSCHandler()
|
||||||
|
osc.setDictFilterAndTarget(osc_handlers)
|
||||||
|
|
||||||
|
# OSCサーバー開始(OSCQuery自動公開)
|
||||||
|
osc.receiveOscParameters()
|
||||||
|
|
||||||
|
print("OSC受信サーバーが開始されました")
|
||||||
|
print("VRChatからのパラメータ変更を監視中...")
|
||||||
|
|
||||||
|
# メッセージ送信テスト
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
osc.sendMessage("双方向通信テスト", notification=True)
|
||||||
|
|
||||||
|
# 長時間実行
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
# クリーンアップ
|
||||||
|
osc.oscServerStop()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 動的設定変更
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 実行時のIP・ポート変更
|
||||||
|
osc = OSCHandler(ip_address="127.0.0.1", port=9000)
|
||||||
|
|
||||||
|
# 初期設定でローカル接続
|
||||||
|
osc.sendMessage("ローカル接続テスト")
|
||||||
|
|
||||||
|
print("リモート接続に切り替え中...")
|
||||||
|
osc.setOscIpAddress("192.168.1.150") # 自動的にOSCQueryが無効化
|
||||||
|
osc.sendMessage("リモート接続テスト")
|
||||||
|
|
||||||
|
print("ポート変更...")
|
||||||
|
osc.setOscPort(9001)
|
||||||
|
osc.sendMessage("新しいポートでのテスト")
|
||||||
|
|
||||||
|
print("ローカル接続に戻る...")
|
||||||
|
osc.setOscIpAddress("127.0.0.1") # OSCQueryが再度有効化
|
||||||
|
osc.sendMessage("ローカル接続復帰テスト")
|
||||||
|
```
|
||||||
|
|
||||||
|
## OSCQuery詳細機能
|
||||||
|
|
||||||
|
### 自動サービス発見
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VRChatMonitor:
|
||||||
|
"""VRChatサービス監視クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.osc = OSCHandler()
|
||||||
|
self.monitoring = False
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""VRChatパラメータの継続監視開始"""
|
||||||
|
|
||||||
|
if not self.osc.getIsOscQueryEnabled():
|
||||||
|
print("OSCQuery機能が無効です(ローカル接続のみサポート)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# OSCハンドラー設定
|
||||||
|
handlers = {
|
||||||
|
"/avatar/parameters/MuteSelf": self.on_mute_change,
|
||||||
|
"/avatar/parameters/Voice": self.on_voice_change,
|
||||||
|
"/avatar/parameters/Viseme": self.on_viseme_change,
|
||||||
|
"/avatar/parameters/GestureLeft": self.on_gesture_left,
|
||||||
|
"/avatar/parameters/GestureRight": self.on_gesture_right
|
||||||
|
}
|
||||||
|
|
||||||
|
self.osc.setDictFilterAndTarget(handlers)
|
||||||
|
self.osc.receiveOscParameters()
|
||||||
|
|
||||||
|
self.monitoring = True
|
||||||
|
print("VRChatパラメータ監視を開始しました")
|
||||||
|
|
||||||
|
def on_mute_change(self, address, *args):
|
||||||
|
print(f"ミュート状態変更: {args[0] if args else 'Unknown'}")
|
||||||
|
|
||||||
|
def on_voice_change(self, address, *args):
|
||||||
|
print(f"音声レベル: {args[0] if args else 'Unknown'}")
|
||||||
|
|
||||||
|
def on_viseme_change(self, address, *args):
|
||||||
|
print(f"口形変化: {args[0] if args else 'Unknown'}")
|
||||||
|
|
||||||
|
def on_gesture_left(self, address, *args):
|
||||||
|
print(f"左手ジェスチャー: {args[0] if args else 'Unknown'}")
|
||||||
|
|
||||||
|
def on_gesture_right(self, address, *args):
|
||||||
|
print(f"右手ジェスチャー: {args[0] if args else 'Unknown'}")
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""監視停止"""
|
||||||
|
self.osc.oscServerStop()
|
||||||
|
self.monitoring = False
|
||||||
|
print("VRChatパラメータ監視を停止しました")
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
monitor = VRChatMonitor()
|
||||||
|
monitor.start_monitoring()
|
||||||
|
|
||||||
|
# 監視中に他の処理を実行
|
||||||
|
time.sleep(60) # 1分間監視
|
||||||
|
|
||||||
|
monitor.stop_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
### リアルタイムパラメータ追跡
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParameterTracker:
|
||||||
|
"""パラメータ値の追跡・履歴管理"""
|
||||||
|
|
||||||
|
def __init__(self, osc_handler):
|
||||||
|
self.osc = osc_handler
|
||||||
|
self.parameter_history = {}
|
||||||
|
self.tracking_active = False
|
||||||
|
|
||||||
|
def track_parameter(self, address, interval=0.1):
|
||||||
|
"""指定されたパラメータを定期監視"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def monitoring_loop():
|
||||||
|
while self.tracking_active:
|
||||||
|
try:
|
||||||
|
value = self.osc.getOSCParameterValue(address)
|
||||||
|
if value is not None:
|
||||||
|
timestamp = time.time()
|
||||||
|
|
||||||
|
if address not in self.parameter_history:
|
||||||
|
self.parameter_history[address] = []
|
||||||
|
|
||||||
|
# 値が変更された場合のみ記録
|
||||||
|
if (not self.parameter_history[address] or
|
||||||
|
self.parameter_history[address][-1][1] != value):
|
||||||
|
|
||||||
|
self.parameter_history[address].append((timestamp, value))
|
||||||
|
print(f"{address}: {value} (時刻: {timestamp:.2f})")
|
||||||
|
|
||||||
|
# 履歴サイズ制限(最新100件まで)
|
||||||
|
if len(self.parameter_history[address]) > 100:
|
||||||
|
self.parameter_history[address] = self.parameter_history[address][-100:]
|
||||||
|
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"パラメータ追跡エラー: {e}")
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
self.tracking_active = True
|
||||||
|
thread = threading.Thread(target=monitoring_loop, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def stop_tracking(self):
|
||||||
|
"""追跡停止"""
|
||||||
|
self.tracking_active = False
|
||||||
|
|
||||||
|
def get_parameter_history(self, address):
|
||||||
|
"""パラメータの履歴取得"""
|
||||||
|
return self.parameter_history.get(address, [])
|
||||||
|
|
||||||
|
def get_latest_value(self, address):
|
||||||
|
"""最新パラメータ値取得"""
|
||||||
|
history = self.get_parameter_history(address)
|
||||||
|
return history[-1][1] if history else None
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
osc = OSCHandler()
|
||||||
|
tracker = ParameterTracker(osc)
|
||||||
|
|
||||||
|
# MuteSelfパラメータの追跡開始
|
||||||
|
tracker.track_parameter("/avatar/parameters/MuteSelf", interval=0.5)
|
||||||
|
|
||||||
|
# しばらく監視
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
# 結果確認
|
||||||
|
mute_history = tracker.get_parameter_history("/avatar/parameters/MuteSelf")
|
||||||
|
print(f"MuteSelf変更履歴: {len(mute_history)}件")
|
||||||
|
|
||||||
|
for timestamp, value in mute_history[-5:]: # 最新5件表示
|
||||||
|
print(f" {time.ctime(timestamp)}: {value}")
|
||||||
|
|
||||||
|
tracker.stop_tracking()
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング・復旧機構
|
||||||
|
|
||||||
|
### 堅牢な接続管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RobustOSCHandler:
|
||||||
|
"""堅牢性を高めたOSCハンドラー"""
|
||||||
|
|
||||||
|
def __init__(self, ip_address="127.0.0.1", port=9000):
|
||||||
|
self.osc = OSCHandler(ip_address, port)
|
||||||
|
self.connection_retries = 3
|
||||||
|
self.retry_delay = 1.0
|
||||||
|
|
||||||
|
def safe_send_message(self, message, notification=True, max_retries=None):
|
||||||
|
"""安全なメッセージ送信(リトライ機構付き)"""
|
||||||
|
|
||||||
|
retries = max_retries or self.connection_retries
|
||||||
|
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
self.osc.sendMessage(message, notification)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"送信試行 {attempt + 1}/{retries} 失敗: {e}")
|
||||||
|
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(self.retry_delay * (attempt + 1)) # 指数バックオフ
|
||||||
|
|
||||||
|
# 接続再初期化を試行
|
||||||
|
try:
|
||||||
|
self.osc.udp_client = udp_client.SimpleUDPClient(
|
||||||
|
self.osc.osc_ip_address,
|
||||||
|
self.osc.osc_port
|
||||||
|
)
|
||||||
|
except Exception as reconnect_error:
|
||||||
|
print(f"再接続失敗: {reconnect_error}")
|
||||||
|
|
||||||
|
print(f"メッセージ送信に失敗しました: '{message}'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def safe_get_parameter(self, address, timeout=5.0):
|
||||||
|
"""安全なパラメータ取得(タイムアウト付き)"""
|
||||||
|
|
||||||
|
if not self.osc.getIsOscQueryEnabled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
|
||||||
|
result_queue = queue.Queue()
|
||||||
|
|
||||||
|
def parameter_getter():
|
||||||
|
try:
|
||||||
|
value = self.osc.getOSCParameterValue(address)
|
||||||
|
result_queue.put(value)
|
||||||
|
except Exception as e:
|
||||||
|
result_queue.put(e)
|
||||||
|
|
||||||
|
# タイムアウト付きでパラメータ取得
|
||||||
|
thread = threading.Thread(target=parameter_getter, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = result_queue.get(timeout=timeout)
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
raise result
|
||||||
|
return result
|
||||||
|
|
||||||
|
except queue.Empty:
|
||||||
|
print(f"パラメータ取得タイムアウト: {address}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
robust_osc = RobustOSCHandler()
|
||||||
|
|
||||||
|
# 堅牢な送信
|
||||||
|
success = robust_osc.safe_send_message("堅牢性テスト", notification=True)
|
||||||
|
print(f"送信成功: {success}")
|
||||||
|
|
||||||
|
# 安全なパラメータ取得
|
||||||
|
mute_value = robust_osc.safe_get_parameter("/avatar/parameters/MuteSelf", timeout=3.0)
|
||||||
|
print(f"MuteSelf値: {mute_value}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス最適化
|
||||||
|
|
||||||
|
### 効率的な通信管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OptimizedOSCHandler:
|
||||||
|
"""パフォーマンス最適化OSCハンドラー"""
|
||||||
|
|
||||||
|
def __init__(self, ip_address="127.0.0.1", port=9000):
|
||||||
|
self.osc = OSCHandler(ip_address, port)
|
||||||
|
self.message_queue = []
|
||||||
|
self.batch_size = 10
|
||||||
|
self.batch_interval = 0.1
|
||||||
|
self.last_batch_time = 0
|
||||||
|
|
||||||
|
def queue_message(self, message, notification=True):
|
||||||
|
"""メッセージをキューに追加(バッチ送信用)"""
|
||||||
|
|
||||||
|
self.message_queue.append((message, notification))
|
||||||
|
|
||||||
|
# バッチサイズまたは時間間隔でフラッシュ
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if (len(self.message_queue) >= self.batch_size or
|
||||||
|
current_time - self.last_batch_time >= self.batch_interval):
|
||||||
|
self.flush_messages()
|
||||||
|
|
||||||
|
def flush_messages(self):
|
||||||
|
"""キューされたメッセージを一括送信"""
|
||||||
|
|
||||||
|
if not self.message_queue:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 最新のメッセージのみ送信(重複排除)
|
||||||
|
if len(self.message_queue) > 1:
|
||||||
|
# 最後のメッセージを優先
|
||||||
|
last_message, last_notification = self.message_queue[-1]
|
||||||
|
self.osc.sendMessage(last_message, last_notification)
|
||||||
|
else:
|
||||||
|
message, notification = self.message_queue[0]
|
||||||
|
self.osc.sendMessage(message, notification)
|
||||||
|
|
||||||
|
# キューをクリア
|
||||||
|
self.message_queue.clear()
|
||||||
|
self.last_batch_time = time.time()
|
||||||
|
|
||||||
|
def send_immediate(self, message, notification=True):
|
||||||
|
"""即座にメッセージ送信(キューをバイパス)"""
|
||||||
|
self.flush_messages() # 既存キューを先にフラッシュ
|
||||||
|
self.osc.sendMessage(message, notification)
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
optimized_osc = OptimizedOSCHandler()
|
||||||
|
|
||||||
|
# 複数のメッセージを効率的に送信
|
||||||
|
for i in range(20):
|
||||||
|
optimized_osc.queue_message(f"バッチメッセージ {i}")
|
||||||
|
time.sleep(0.05) # 短い間隔
|
||||||
|
|
||||||
|
# 残りのメッセージをフラッシュ
|
||||||
|
optimized_osc.flush_messages()
|
||||||
|
|
||||||
|
# 即座に送信が必要な重要メッセージ
|
||||||
|
optimized_osc.send_immediate("緊急メッセージ", notification=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係・要件
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `pythonosc`: 基本OSC通信ライブラリ
|
||||||
|
- `threading`: 並行処理制御
|
||||||
|
- `time`: 時間管理機能
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `tinyoscquery`: OSCQuery機能(ローカル接続時のみ)
|
||||||
|
- `utils`: エラーログ機能(フォールバック処理あり)
|
||||||
|
|
||||||
|
### システム要件
|
||||||
|
```python
|
||||||
|
# 最小システム要件
|
||||||
|
requirements = {
|
||||||
|
"python_version": "3.7+",
|
||||||
|
"network": "UDP通信対応",
|
||||||
|
"vrchat_version": "OSCサポート版(2022年8月以降)",
|
||||||
|
"local_ports": "空きUDP/TCPポート(OSCQuery使用時)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 推奨環境
|
||||||
|
recommended = {
|
||||||
|
"network_latency": "< 10ms(ローカル接続)",
|
||||||
|
"cpu_usage": "OSCQuery使用時は追加CPU負荷",
|
||||||
|
"memory": "tinyoscquery使用時は追加メモリ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項・制限
|
||||||
|
|
||||||
|
### OSCQuery制限
|
||||||
|
- ローカルホスト(127.0.0.1/localhost)接続時のみ利用可能
|
||||||
|
- tinyoscqueryライブラリが必要
|
||||||
|
- ファイアウォール設定によっては動作しない可能性
|
||||||
|
|
||||||
|
### 通信制限
|
||||||
|
- UDPプロトコルのため送達保証なし
|
||||||
|
- VRChatのOSC受信制限(レート制限あり)
|
||||||
|
- ネットワーク環境による遅延・パケット loss
|
||||||
|
|
||||||
|
### プラットフォーム依存
|
||||||
|
```python
|
||||||
|
# 既知の制限事項
|
||||||
|
limitations = {
|
||||||
|
"windows": "Windowsファイアウォールの設定が必要な場合あり",
|
||||||
|
"macos": "セキュリティ設定によるポート制限の可能性",
|
||||||
|
"linux": "一部のLinuxディストリビューションでの互換性問題",
|
||||||
|
"vrchat_platform": "PC版VRChatのみOSCサポート"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `config.py`: OSC設定管理
|
||||||
|
- `controller.py`: OSC機能制御インターフェース
|
||||||
|
- `model.py`: OSC機能統合
|
||||||
|
- `utils.py`: エラーログ・ネットワークユーティリティ
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- より高度なOSCQueryパラメータ監視
|
||||||
|
- カスタムOSCプロトコル拡張
|
||||||
|
- パフォーマンス監視・分析機能
|
||||||
|
- 自動再接続・復旧機構の改善
|
||||||
|
- VRChatアバター固有パラメータ対応
|
||||||
@@ -0,0 +1,775 @@
|
|||||||
|
# overlay - VRオーバーレイ統合システム
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRChat向けのOpenVRオーバーレイシステムです。翻訳結果や字幕をVR空間内に表示する機能を提供し、HMD・コントローラー追跡、フェード効果、多言語フォント対応を統合的に管理します。
|
||||||
|
|
||||||
|
## 主要コンポーネント
|
||||||
|
|
||||||
|
### overlay.py - メインオーバーレイ管理
|
||||||
|
- OpenVRオーバーレイの生成・配置・制御
|
||||||
|
- HMD・左手・右手への追跡設定
|
||||||
|
- フェードイン・フェードアウト効果
|
||||||
|
|
||||||
|
### overlay_image.py - 画像生成・描画
|
||||||
|
- 多言語対応テキスト画像生成
|
||||||
|
- メッセージログ・履歴表示
|
||||||
|
- フォント・レイアウト管理
|
||||||
|
|
||||||
|
### overlay_utils.py - 数学的変換ユーティリティ
|
||||||
|
- 3D座標変換行列計算
|
||||||
|
- オイラー角・回転行列変換
|
||||||
|
- 同次座標系変換
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Overlay クラス (overlay.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Overlay:
|
||||||
|
def __init__(self, settings_dict: Dict[str, Dict[str, Any]]) -> None:
|
||||||
|
self.system: Optional[Any] = None # OpenVRシステム
|
||||||
|
self.overlay: Optional[Any] = None # オーバーレイインターface
|
||||||
|
self.handle: Dict[str, Any] = {} # サイズ別ハンドル
|
||||||
|
self.settings: Dict[str, Dict[str, Any]] # サイズ別設定
|
||||||
|
self.lastUpdate: Dict[str, float] = {} # 最終更新時刻
|
||||||
|
self.fadeRatio: Dict[str, float] = {} # フェード比率
|
||||||
|
```
|
||||||
|
|
||||||
|
VRオーバーレイの総合管理クラス
|
||||||
|
|
||||||
|
#### 主要機能
|
||||||
|
- OpenVRの初期化・管理
|
||||||
|
- 複数サイズオーバーレイの同時管理
|
||||||
|
- リアルタイムフェード効果処理
|
||||||
|
- SteamVR接続状態監視
|
||||||
|
|
||||||
|
### OverlayImage クラス (overlay_image.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OverlayImage:
|
||||||
|
LANGUAGES = {
|
||||||
|
"Default": "NotoSansJP-Regular.ttf",
|
||||||
|
"Japanese": "NotoSansJP-Regular.ttf",
|
||||||
|
"Korean": "NotoSansKR-Regular.ttf",
|
||||||
|
"Chinese Simplified": "NotoSansSC-Regular.ttf",
|
||||||
|
"Chinese Traditional": "NotoSansTC-Regular.ttf"
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, root_path: Optional[str] = None) -> None:
|
||||||
|
self.message_log: List[dict] = []
|
||||||
|
self.root_path: str
|
||||||
|
```
|
||||||
|
|
||||||
|
テキスト画像生成・多言語フォント管理クラス
|
||||||
|
|
||||||
|
#### 主要機能
|
||||||
|
- 多言語フォント自動選択
|
||||||
|
- メッセージ履歴管理
|
||||||
|
- 動的画像生成・合成
|
||||||
|
- UI要素のサイズ計算
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### Overlay クラス
|
||||||
|
|
||||||
|
#### 初期化・制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
def startOverlay(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
オーバーレイシステム開始
|
||||||
|
|
||||||
|
```python
|
||||||
|
def shutdownOverlay(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
オーバーレイシステム終了・リソース解放
|
||||||
|
|
||||||
|
```python
|
||||||
|
def reStartOverlay(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
オーバーレイシステム再起動
|
||||||
|
|
||||||
|
#### 表示制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
def showOverlay(self, image: Image, size: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
画像をオーバーレイに表示
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **image**: 表示するPIL画像
|
||||||
|
- **size**: オーバーレイサイズ識別子
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setOpacity(self, opacity: float, size: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
オーバーレイ透明度設定
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **opacity**: 透明度(0.0-1.0)
|
||||||
|
- **size**: 対象サイズ
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setTrackedDeviceRelative(self, tracker: str, size: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
追跡デバイスへのオーバーレイ配置
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **tracker**: 追跡デバイス("HMD", "LeftHand", "RightHand")
|
||||||
|
- **size**: オーバーレイサイズ
|
||||||
|
|
||||||
|
### OverlayImage クラス
|
||||||
|
|
||||||
|
#### 画像生成
|
||||||
|
|
||||||
|
```python
|
||||||
|
def createOverlayImage(self, message: str, language: str, ui_size: dict,
|
||||||
|
ui_settings: dict, message_log_settings: dict) -> Image
|
||||||
|
```
|
||||||
|
|
||||||
|
オーバーレイ用画像の生成
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **message**: 表示メッセージ
|
||||||
|
- **language**: 言語設定
|
||||||
|
- **ui_size**: UIサイズ設定
|
||||||
|
- **ui_settings**: UI表示設定
|
||||||
|
- **message_log_settings**: ログ表示設定
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **Image**: 生成されたPIL画像
|
||||||
|
|
||||||
|
#### 履歴管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def addMessageLog(self, message: str, timestamp: datetime) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
メッセージログに新規追加
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **message**: 追加するメッセージ
|
||||||
|
- **timestamp**: タイムスタンプ
|
||||||
|
|
||||||
|
```python
|
||||||
|
def clearMessageLog(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
メッセージログのクリア
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的なオーバーレイ表示
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.overlay.overlay import Overlay
|
||||||
|
from models.overlay.overlay_image import OverlayImage
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# オーバーレイ設定
|
||||||
|
settings = {
|
||||||
|
"small": {
|
||||||
|
"width": 0.3,
|
||||||
|
"height": 0.1,
|
||||||
|
"x_pos": 0.0,
|
||||||
|
"y_pos": -0.2,
|
||||||
|
"z_pos": 1.0,
|
||||||
|
"opacity": 0.8,
|
||||||
|
"display_duration": 3.0,
|
||||||
|
"fadeout_duration": 1.0
|
||||||
|
},
|
||||||
|
"large": {
|
||||||
|
"width": 0.5,
|
||||||
|
"height": 0.2,
|
||||||
|
"x_pos": 0.0,
|
||||||
|
"y_pos": -0.3,
|
||||||
|
"z_pos": 1.2,
|
||||||
|
"opacity": 0.9,
|
||||||
|
"display_duration": 5.0,
|
||||||
|
"fadeout_duration": 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# オーバーレイシステム初期化
|
||||||
|
overlay_system = Overlay(settings)
|
||||||
|
overlay_image = OverlayImage()
|
||||||
|
|
||||||
|
# システム開始
|
||||||
|
overlay_system.startOverlay()
|
||||||
|
|
||||||
|
# 翻訳結果の表示
|
||||||
|
translation_text = "Hello, world! / こんにちは、世界!"
|
||||||
|
|
||||||
|
# 画像生成設定
|
||||||
|
ui_size = OverlayImage.getUiSizeSmallLog()
|
||||||
|
ui_settings = {
|
||||||
|
"font_size": 20,
|
||||||
|
"text_color": (255, 255, 255, 255),
|
||||||
|
"background_color": (0, 0, 0, 180)
|
||||||
|
}
|
||||||
|
message_log_settings = {
|
||||||
|
"enabled": True,
|
||||||
|
"max_lines": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
# 画像生成・表示
|
||||||
|
overlay_img = overlay_image.createOverlayImage(
|
||||||
|
message=translation_text,
|
||||||
|
language="Japanese",
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings=message_log_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
# オーバーレイに表示
|
||||||
|
overlay_system.showOverlay(overlay_img, "small")
|
||||||
|
|
||||||
|
# システム終了
|
||||||
|
import time
|
||||||
|
time.sleep(10)
|
||||||
|
overlay_system.shutdownOverlay()
|
||||||
|
```
|
||||||
|
|
||||||
|
### HMD・コントローラー追跡設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# HMDに固定表示
|
||||||
|
overlay_system.setTrackedDeviceRelative("HMD", "large")
|
||||||
|
|
||||||
|
# 左手コントローラーに追従
|
||||||
|
overlay_system.setTrackedDeviceRelative("LeftHand", "small")
|
||||||
|
|
||||||
|
# 右手コントローラーに追従
|
||||||
|
overlay_system.setTrackedDeviceRelative("RightHand", "small")
|
||||||
|
|
||||||
|
# 位置・回転の微調整(設定変更)
|
||||||
|
overlay_system.settings["small"]["x_pos"] = 0.1
|
||||||
|
overlay_system.settings["small"]["y_pos"] = -0.1
|
||||||
|
overlay_system.settings["small"]["z_pos"] = 0.8
|
||||||
|
overlay_system.settings["small"]["x_rotation"] = -30.0
|
||||||
|
overlay_system.settings["small"]["y_rotation"] = 15.0
|
||||||
|
|
||||||
|
# 設定を適用
|
||||||
|
overlay_system.setTrackedDeviceRelative("LeftHand", "small")
|
||||||
|
```
|
||||||
|
|
||||||
|
### フェード効果制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
# フェード効果設定
|
||||||
|
overlay_system.updateDisplayDuration(4.0, "large") # 4秒表示
|
||||||
|
overlay_system.updateFadeoutDuration(2.0, "large") # 2秒でフェードアウト
|
||||||
|
|
||||||
|
# 即座に透明度変更
|
||||||
|
overlay_system.setOpacity(0.5, "large") # 50%透明度
|
||||||
|
|
||||||
|
# フェード効果を無効にして固定表示
|
||||||
|
overlay_system.settings["small"]["fadeout_duration"] = 0
|
||||||
|
overlay_system.setOpacity(1.0, "small") # 完全不透明で固定
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多言語対応表示
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 日本語表示
|
||||||
|
japanese_text = "これは日本語のテストです"
|
||||||
|
jp_image = overlay_image.createOverlayImage(
|
||||||
|
message=japanese_text,
|
||||||
|
language="Japanese",
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings=message_log_settings
|
||||||
|
)
|
||||||
|
overlay_system.showOverlay(jp_image, "large")
|
||||||
|
|
||||||
|
# 韓国語表示
|
||||||
|
korean_text = "이것은 한국어 테스트입니다"
|
||||||
|
kr_image = overlay_image.createOverlayImage(
|
||||||
|
message=korean_text,
|
||||||
|
language="Korean",
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings=message_log_settings
|
||||||
|
)
|
||||||
|
overlay_system.showOverlay(kr_image, "small")
|
||||||
|
|
||||||
|
# 中国語(簡体字)表示
|
||||||
|
chinese_text = "这是中文测试"
|
||||||
|
cn_image = overlay_image.createOverlayImage(
|
||||||
|
message=chinese_text,
|
||||||
|
language="Chinese Simplified",
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings=message_log_settings
|
||||||
|
)
|
||||||
|
overlay_system.showOverlay(cn_image, "large")
|
||||||
|
```
|
||||||
|
|
||||||
|
### メッセージログ機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# メッセージログの追加
|
||||||
|
overlay_image.addMessageLog("最初のメッセージ", datetime.now())
|
||||||
|
overlay_image.addMessageLog("翻訳結果: Hello -> こんにちは", datetime.now())
|
||||||
|
overlay_image.addMessageLog("音声認識: こんにちは", datetime.now())
|
||||||
|
|
||||||
|
# ログ表示設定
|
||||||
|
log_settings = {
|
||||||
|
"enabled": True,
|
||||||
|
"max_lines": 3, # 最大3行表示
|
||||||
|
"show_timestamp": True, # タイムスタンプ表示
|
||||||
|
"font_size": 16,
|
||||||
|
"text_color": (200, 200, 200, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ログ付きオーバーレイ画像生成
|
||||||
|
logged_image = overlay_image.createOverlayImage(
|
||||||
|
message="新しいメッセージ",
|
||||||
|
language="Japanese",
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings=log_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
overlay_system.showOverlay(logged_image, "large")
|
||||||
|
|
||||||
|
# ログクリア
|
||||||
|
overlay_image.clearMessageLog()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 座標系・変換システム
|
||||||
|
|
||||||
|
### 基本座標設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# HMD基準座標(頭部固定表示)
|
||||||
|
def getHMDBaseMatrix() -> np.ndarray:
|
||||||
|
x_pos = 0.0 # 左右位置
|
||||||
|
y_pos = -0.4 # 上下位置(下方向)
|
||||||
|
z_pos = 1.0 # 前後位置(前方向)
|
||||||
|
x_rotation = 0.0 # X軸回転
|
||||||
|
y_rotation = 0.0 # Y軸回転
|
||||||
|
z_rotation = 0.0 # Z軸回転
|
||||||
|
|
||||||
|
# 左手コントローラー基準座標
|
||||||
|
def getLeftHandBaseMatrix() -> np.ndarray:
|
||||||
|
x_pos = 0.3 # 右側にオフセット
|
||||||
|
y_pos = 0.1 # 上方向にオフセット
|
||||||
|
z_pos = -0.31 # 手前にオフセット
|
||||||
|
x_rotation = -65.0 # 下向きに傾斜
|
||||||
|
y_rotation = 165.0 # Y軸回転
|
||||||
|
z_rotation = 115.0 # Z軸回転
|
||||||
|
|
||||||
|
# 右手コントローラー基準座標
|
||||||
|
def getRightHandBaseMatrix() -> np.ndarray:
|
||||||
|
x_pos = -0.3 # 左側にオフセット
|
||||||
|
y_rotation = -165.0 # 左手と対称
|
||||||
|
z_rotation = -115.0 # 左手と対称
|
||||||
|
```
|
||||||
|
|
||||||
|
### 変換行列計算 (overlay_utils.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np
|
||||||
|
from models.overlay.overlay_utils import *
|
||||||
|
|
||||||
|
# 移動変換
|
||||||
|
translation = (0.1, -0.2, 0.5) # x, y, z移動
|
||||||
|
translation_matrix = calcTranslationMatrix(translation)
|
||||||
|
|
||||||
|
# 回転変換(各軸独立)
|
||||||
|
x_rotation_matrix = calcRotationMatrixX(30.0) # X軸30度回転
|
||||||
|
y_rotation_matrix = calcRotationMatrixY(45.0) # Y軸45度回転
|
||||||
|
z_rotation_matrix = calcRotationMatrixZ(60.0) # Z軸60度回転
|
||||||
|
|
||||||
|
# オイラー角から回転行列生成
|
||||||
|
euler_angles = (30.0, 45.0, 60.0) # X, Y, Z軸回転角度
|
||||||
|
rotation_matrix = euler_to_rotation_matrix(euler_angles)
|
||||||
|
|
||||||
|
# 基本行列への変換適用
|
||||||
|
base_matrix = getHMDBaseMatrix()
|
||||||
|
translation = (0.05, -0.1, 0.2)
|
||||||
|
rotation = (10.0, -5.0, 0.0)
|
||||||
|
transformed_matrix = transform_matrix(base_matrix, translation, rotation)
|
||||||
|
|
||||||
|
# 3x4行列を4x4同次座標に変換
|
||||||
|
homogeneous_matrix = toHomogeneous(transformed_matrix)
|
||||||
|
```
|
||||||
|
|
||||||
|
### カスタム配置設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# カスタム位置でのオーバーレイ配置
|
||||||
|
def createCustomOverlay(overlay_system, custom_pos, custom_rot, size):
|
||||||
|
"""カスタム位置・回転でのオーバーレイ設定"""
|
||||||
|
|
||||||
|
# 設定を動的に変更
|
||||||
|
overlay_system.settings[size]["x_pos"] = custom_pos[0]
|
||||||
|
overlay_system.settings[size]["y_pos"] = custom_pos[1]
|
||||||
|
overlay_system.settings[size]["z_pos"] = custom_pos[2]
|
||||||
|
overlay_system.settings[size]["x_rotation"] = custom_rot[0]
|
||||||
|
overlay_system.settings[size]["y_rotation"] = custom_rot[1]
|
||||||
|
overlay_system.settings[size]["z_rotation"] = custom_rot[2]
|
||||||
|
|
||||||
|
# 追跡デバイス設定を再適用
|
||||||
|
overlay_system.setTrackedDeviceRelative("HMD", size)
|
||||||
|
|
||||||
|
# 使用例:カスタム配置
|
||||||
|
custom_position = (0.2, -0.3, 0.8) # やや右下前方
|
||||||
|
custom_rotation = (-15.0, 10.0, 5.0) # 軽く傾斜
|
||||||
|
createCustomOverlay(overlay_system, custom_position, custom_rotation, "large")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高度な機能
|
||||||
|
|
||||||
|
### 動的サイズ・レイアウト管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AdaptiveOverlayManager:
|
||||||
|
"""適応的オーバーレイ管理クラス"""
|
||||||
|
|
||||||
|
def __init__(self, base_overlay_system, base_overlay_image):
|
||||||
|
self.overlay = base_overlay_system
|
||||||
|
self.image_gen = base_overlay_image
|
||||||
|
self.current_layout = "compact"
|
||||||
|
|
||||||
|
def adaptLayoutToContent(self, message, language):
|
||||||
|
"""コンテンツに応じたレイアウト自動調整"""
|
||||||
|
|
||||||
|
# メッセージ長に応じてサイズ決定
|
||||||
|
if len(message) < 50:
|
||||||
|
layout = "compact"
|
||||||
|
size_key = "small"
|
||||||
|
elif len(message) < 150:
|
||||||
|
layout = "standard"
|
||||||
|
size_key = "medium"
|
||||||
|
else:
|
||||||
|
layout = "expanded"
|
||||||
|
size_key = "large"
|
||||||
|
|
||||||
|
# 言語に応じたフォントサイズ調整
|
||||||
|
if language in ["Chinese Simplified", "Chinese Traditional"]:
|
||||||
|
font_scale = 1.1 # 中国語は少し大きめ
|
||||||
|
elif language == "Korean":
|
||||||
|
font_scale = 1.05 # 韓国語は微調整
|
||||||
|
else:
|
||||||
|
font_scale = 1.0 # 日本語・その他
|
||||||
|
|
||||||
|
# UI設定の動的生成
|
||||||
|
ui_size = self.getAdaptiveUiSize(layout)
|
||||||
|
ui_settings = {
|
||||||
|
"font_size": int(18 * font_scale),
|
||||||
|
"line_height": int(24 * font_scale),
|
||||||
|
"text_color": (255, 255, 255, 255),
|
||||||
|
"background_color": (0, 0, 0, 200),
|
||||||
|
"border_width": 2,
|
||||||
|
"border_color": (100, 150, 255, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui_size, ui_settings, size_key
|
||||||
|
|
||||||
|
def getAdaptiveUiSize(self, layout):
|
||||||
|
"""レイアウトに応じたUIサイズ取得"""
|
||||||
|
|
||||||
|
layouts = {
|
||||||
|
"compact": {
|
||||||
|
"width": 400,
|
||||||
|
"height": 100,
|
||||||
|
"margin": 10,
|
||||||
|
"padding": 8
|
||||||
|
},
|
||||||
|
"standard": {
|
||||||
|
"width": 600,
|
||||||
|
"height": 150,
|
||||||
|
"margin": 15,
|
||||||
|
"padding": 12
|
||||||
|
},
|
||||||
|
"expanded": {
|
||||||
|
"width": 800,
|
||||||
|
"height": 200,
|
||||||
|
"margin": 20,
|
||||||
|
"padding": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return layouts.get(layout, layouts["standard"])
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
adaptive_manager = AdaptiveOverlayManager(overlay_system, overlay_image)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
("Hello!", "English"),
|
||||||
|
("これは中程度の長さのメッセージです。翻訳結果を表示します。", "Japanese"),
|
||||||
|
("这是一个很长的消息,用来测试自适应布局功能。当消息内容很长时,系统会自动选择更大的显示区域,并调整字体大小以确保良好的可读性。", "Chinese Simplified")
|
||||||
|
]
|
||||||
|
|
||||||
|
for message, language in messages:
|
||||||
|
# 自動レイアウト調整
|
||||||
|
ui_size, ui_settings, size_key = adaptive_manager.adaptLayoutToContent(message, language)
|
||||||
|
|
||||||
|
# 画像生成・表示
|
||||||
|
adaptive_image = overlay_image.createOverlayImage(
|
||||||
|
message=message,
|
||||||
|
language=language,
|
||||||
|
ui_size=ui_size,
|
||||||
|
ui_settings=ui_settings,
|
||||||
|
message_log_settings={"enabled": True, "max_lines": 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
overlay_system.showOverlay(adaptive_image, size_key)
|
||||||
|
time.sleep(3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### パフォーマンス監視・最適化
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OverlayPerformanceMonitor:
|
||||||
|
"""オーバーレイパフォーマンス監視クラス"""
|
||||||
|
|
||||||
|
def __init__(self, overlay_system):
|
||||||
|
self.overlay = overlay_system
|
||||||
|
self.frame_times = []
|
||||||
|
self.update_counts = {}
|
||||||
|
|
||||||
|
def monitorFrameRate(self, duration=10.0):
|
||||||
|
"""フレームレート監視"""
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
frame_count = 0
|
||||||
|
|
||||||
|
while time.monotonic() - start_time < duration:
|
||||||
|
frame_start = time.monotonic()
|
||||||
|
|
||||||
|
# フレーム処理(空の処理)
|
||||||
|
time.sleep(1/90) # 90Hz目標
|
||||||
|
|
||||||
|
frame_end = time.monotonic()
|
||||||
|
self.frame_times.append(frame_end - frame_start)
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
# 統計計算
|
||||||
|
avg_frame_time = sum(self.frame_times) / len(self.frame_times)
|
||||||
|
avg_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0
|
||||||
|
|
||||||
|
print(f"平均フレーム時間: {avg_frame_time*1000:.2f}ms")
|
||||||
|
print(f"平均FPS: {avg_fps:.1f}")
|
||||||
|
print(f"総フレーム数: {frame_count}")
|
||||||
|
|
||||||
|
return avg_fps
|
||||||
|
|
||||||
|
def optimizeSettings(self, target_fps=60):
|
||||||
|
"""パフォーマンス目標に基づく設定最適化"""
|
||||||
|
|
||||||
|
current_fps = self.monitorFrameRate(5.0)
|
||||||
|
|
||||||
|
if current_fps < target_fps * 0.8:
|
||||||
|
print("パフォーマンス不足。設定を軽量化します...")
|
||||||
|
|
||||||
|
# フェード処理間隔を延長
|
||||||
|
for size in self.overlay.settings:
|
||||||
|
self.overlay.settings[size]["fadeout_duration"] *= 1.5
|
||||||
|
|
||||||
|
# 更新頻度を下げる
|
||||||
|
# (mainloopの sleep_time 調整は overlay.py 内で実装)
|
||||||
|
|
||||||
|
elif current_fps > target_fps * 1.2:
|
||||||
|
print("パフォーマンスに余裕があります。品質を向上します...")
|
||||||
|
|
||||||
|
# より滑らかなフェード
|
||||||
|
for size in self.overlay.settings:
|
||||||
|
self.overlay.settings[size]["fadeout_duration"] *= 0.8
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
performance_monitor = OverlayPerformanceMonitor(overlay_system)
|
||||||
|
performance_monitor.monitorFrameRate(10.0)
|
||||||
|
performance_monitor.optimizeSettings(target_fps=60)
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング・復旧
|
||||||
|
|
||||||
|
### 堅牢な接続管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RobustOverlaySystem:
|
||||||
|
"""堅牢性を高めたオーバーレイシステム"""
|
||||||
|
|
||||||
|
def __init__(self, settings_dict):
|
||||||
|
self.base_overlay = Overlay(settings_dict)
|
||||||
|
self.connection_retries = 3
|
||||||
|
self.auto_reconnect = True
|
||||||
|
|
||||||
|
def safeStartOverlay(self, max_retries=None):
|
||||||
|
"""安全なオーバーレイ開始(リトライ機構付き)"""
|
||||||
|
|
||||||
|
retries = max_retries or self.connection_retries
|
||||||
|
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
# SteamVR接続確認
|
||||||
|
if not self.base_overlay.checkSteamvrRunning():
|
||||||
|
print("SteamVRが起動していません。待機中...")
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# オーバーレイ開始
|
||||||
|
self.base_overlay.startOverlay()
|
||||||
|
|
||||||
|
# 初期化完了まで待機
|
||||||
|
timeout = 10.0
|
||||||
|
start_time = time.monotonic()
|
||||||
|
|
||||||
|
while not self.base_overlay.initialized and time.monotonic() - start_time < timeout:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if self.base_overlay.initialized:
|
||||||
|
print("オーバーレイシステム開始完了")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"初期化タイムアウト(試行 {attempt + 1}/{retries})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"オーバーレイ開始エラー(試行 {attempt + 1}/{retries}): {e}")
|
||||||
|
|
||||||
|
# 既存システムのクリーンアップ
|
||||||
|
try:
|
||||||
|
self.base_overlay.shutdownOverlay()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(2 ** attempt) # 指数バックオフ
|
||||||
|
|
||||||
|
print("オーバーレイシステムの開始に失敗しました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def monitorConnection(self):
|
||||||
|
"""接続監視・自動復旧"""
|
||||||
|
|
||||||
|
while self.auto_reconnect:
|
||||||
|
try:
|
||||||
|
if self.base_overlay.initialized and not self.base_overlay.checkActive():
|
||||||
|
print("OpenVR接続が切断されました。再接続を試行します...")
|
||||||
|
|
||||||
|
self.base_overlay.shutdownOverlay()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if self.safeStartOverlay():
|
||||||
|
print("オーバーレイシステムが復旧しました")
|
||||||
|
else:
|
||||||
|
print("復旧に失敗しました")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"接続監視エラー: {e}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
robust_overlay = RobustOverlaySystem(settings)
|
||||||
|
|
||||||
|
# 安全な開始
|
||||||
|
if robust_overlay.safeStartOverlay():
|
||||||
|
# 接続監視開始(別スレッド)
|
||||||
|
import threading
|
||||||
|
monitor_thread = threading.Thread(target=robust_overlay.monitorConnection, daemon=True)
|
||||||
|
monitor_thread.start()
|
||||||
|
|
||||||
|
# 通常の操作
|
||||||
|
overlay_img = overlay_image.createOverlayImage(...)
|
||||||
|
robust_overlay.base_overlay.showOverlay(overlay_img, "small")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係・システム要件
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `openvr`: OpenVR Python バインディング
|
||||||
|
- `numpy`: 数値計算・行列演算
|
||||||
|
- `PIL (Pillow)`: 画像処理・生成
|
||||||
|
- `psutil`: プロセス監視
|
||||||
|
|
||||||
|
### システム要件
|
||||||
|
```python
|
||||||
|
system_requirements = {
|
||||||
|
"steamvr": "SteamVR環境必須",
|
||||||
|
"openvr_runtime": "OpenVR Runtime",
|
||||||
|
"vr_headset": "対応VRヘッドセット(Oculus, Vive, Index等)",
|
||||||
|
"graphics": "VR対応GPU",
|
||||||
|
"python": "Python 3.7以上"
|
||||||
|
}
|
||||||
|
|
||||||
|
performance_requirements = {
|
||||||
|
"cpu": "VR処理に十分なCPU性能",
|
||||||
|
"memory": "追加メモリ使用量 ~100-500MB",
|
||||||
|
"disk_space": "フォントファイル用容量 ~50MB"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `utils.errorLogging`: エラーログ機能(フォールバック処理あり)
|
||||||
|
|
||||||
|
## 注意事項・制限
|
||||||
|
|
||||||
|
### VR環境制限
|
||||||
|
- SteamVRが起動していない場合は動作不可
|
||||||
|
- VRヘッドセットが接続されていない場合は制限あり
|
||||||
|
- OpenVRドライバーの互換性に依存
|
||||||
|
|
||||||
|
### パフォーマンス制限
|
||||||
|
- リアルタイム描画処理によるCPU・GPU負荷
|
||||||
|
- フォントレンダリングによるメモリ使用量
|
||||||
|
- 高解像度VRディスプレイでの描画負荷
|
||||||
|
|
||||||
|
### プラットフォーム制限
|
||||||
|
```python
|
||||||
|
platform_limitations = {
|
||||||
|
"windows": "主要サポートプラットフォーム",
|
||||||
|
"linux": "SteamVR Linux版での制限あり",
|
||||||
|
"macos": "SteamVR macOS版サポート終了により制限",
|
||||||
|
"mobile_vr": "OpenVR非対応のため利用不可"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `config.py`: オーバーレイ設定管理
|
||||||
|
- `controller.py`: オーバーレイ制御インターフェース
|
||||||
|
- `model.py`: オーバーレイ機能統合
|
||||||
|
- `utils.py`: エラーログ・ユーティリティ
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
### フォント探索仕様の強化
|
||||||
|
|
||||||
|
`overlay_image.py` に PyInstaller ビルド後の `_internal/fonts/` ディレクトリ検出ロジックを追加。以下の優先順位でフォントディレクトリを探索:
|
||||||
|
|
||||||
|
1. `root_path/_internal/fonts/` (PyInstallerバンドル環境)
|
||||||
|
2. `src-python/models/overlay/fonts/` (開発環境相対パス)
|
||||||
|
3. `models/overlay/fonts/` (直接実行時)
|
||||||
|
|
||||||
|
見つからない場合は `FileNotFoundError` で早期通知。これにより配布バイナリと開発環境で同一コードパスを維持。
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| PyInstaller対応 | バンドル後のフォント読み込み失敗を防止 |
|
||||||
|
| 移植性 | 環境差異をコード内条件分岐で吸収 |
|
||||||
|
| エラー検知 | フォント未配置時の早期例外で不正描画防止 |
|
||||||
|
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- よりリッチなUI要素対応
|
||||||
|
- アニメーション・エフェクト機能
|
||||||
|
- カスタムフォント・テーマシステム
|
||||||
|
- パフォーマンス監視・自動最適化
|
||||||
|
- 他のVRプラットフォーム対応検討
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# transcription_languages.py - 音声認識言語マッピング
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
音声認識エンジンが対応する言語コードのマッピングテーブルを提供するモジュールです。異なる音声認識エンジンの言語コード仕様の差異を吸収し、統一的なインターフェースを提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 言語マッピングテーブル
|
||||||
|
- 表示用言語名から各エンジン固有の言語コードへの変換
|
||||||
|
- 国・地域固有の言語バリエーション対応
|
||||||
|
- 複数音声認識エンジンの統一的な言語管理
|
||||||
|
|
||||||
|
### 対応エンジン
|
||||||
|
- Google Speech Recognition
|
||||||
|
- OpenAI Whisper(faster-whisper)
|
||||||
|
- その他の音声認識エンジン
|
||||||
|
|
||||||
|
## データ構造
|
||||||
|
|
||||||
|
### transcription_lang
|
||||||
|
```python
|
||||||
|
transcription_lang: Dict[str, List[Dict[str, str]]]
|
||||||
|
```
|
||||||
|
|
||||||
|
言語とその地域バリエーションのマッピング
|
||||||
|
|
||||||
|
```python
|
||||||
|
transcription_lang = {
|
||||||
|
"English": [
|
||||||
|
{"country": "United States", "google_language_code": "en-US"},
|
||||||
|
{"country": "United Kingdom", "google_language_code": "en-GB"},
|
||||||
|
{"country": "Australia", "google_language_code": "en-AU"}
|
||||||
|
],
|
||||||
|
"Japanese": [
|
||||||
|
{"country": "Japan", "google_language_code": "ja-JP"}
|
||||||
|
],
|
||||||
|
"Korean": [
|
||||||
|
{"country": "South Korea", "google_language_code": "ko-KR"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な言語コード取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_languages import transcription_lang
|
||||||
|
|
||||||
|
# 日本語の言語コード取得
|
||||||
|
japanese_codes = transcription_lang.get("Japanese", [])
|
||||||
|
if japanese_codes:
|
||||||
|
code = japanese_codes[0]["google_language_code"] # "ja-JP"
|
||||||
|
|
||||||
|
# 英語の地域別言語コード取得
|
||||||
|
english_codes = transcription_lang.get("English", [])
|
||||||
|
for region in english_codes:
|
||||||
|
print(f"{region['country']}: {region['google_language_code']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 利用可能言語の一覧取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 対応言語の一覧
|
||||||
|
supported_languages = list(transcription_lang.keys())
|
||||||
|
print(f"対応言語: {supported_languages}")
|
||||||
|
|
||||||
|
# 言語と国の組み合わせ一覧
|
||||||
|
language_country_pairs = []
|
||||||
|
for lang, countries in transcription_lang.items():
|
||||||
|
for country_data in countries:
|
||||||
|
language_country_pairs.append({
|
||||||
|
"language": lang,
|
||||||
|
"country": country_data["country"],
|
||||||
|
"code": country_data["google_language_code"]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 翻訳システムとの連携
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 翻訳システムで対応している言語の確認
|
||||||
|
from models.translation.translation_languages import translation_lang
|
||||||
|
|
||||||
|
transcription_langs = list(transcription_lang.keys())
|
||||||
|
translation_langs = []
|
||||||
|
for engine in translation_lang.keys():
|
||||||
|
translation_langs.extend(translation_lang[engine]["source"].keys())
|
||||||
|
|
||||||
|
# 音声認識と翻訳の両方で対応している言語
|
||||||
|
supported_langs = list(filter(lambda x: x in transcription_langs, translation_langs))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要対応言語
|
||||||
|
|
||||||
|
### 西欧言語
|
||||||
|
- **English**: US, UK, Australia, Canada, India, South Africa
|
||||||
|
- **Spanish**: Spain, Mexico, Argentina, Colombia
|
||||||
|
- **French**: France, Canada, Belgium
|
||||||
|
- **German**: Germany, Austria, Switzerland
|
||||||
|
- **Italian**: Italy
|
||||||
|
- **Portuguese**: Brazil, Portugal
|
||||||
|
|
||||||
|
### アジア言語
|
||||||
|
- **Japanese**: Japan
|
||||||
|
- **Korean**: South Korea
|
||||||
|
- **Chinese**: China (Simplified), Taiwan (Traditional), Hong Kong
|
||||||
|
- **Thai**: Thailand
|
||||||
|
- **Vietnamese**: Vietnam
|
||||||
|
|
||||||
|
### その他の言語
|
||||||
|
- **Russian**: Russia
|
||||||
|
- **Arabic**: Saudi Arabia, UAE, Egypt
|
||||||
|
- **Hindi**: India
|
||||||
|
- **Dutch**: Netherlands
|
||||||
|
- **Swedish**: Sweden
|
||||||
|
- **Norwegian**: Norway
|
||||||
|
|
||||||
|
## エンジン別言語コード形式
|
||||||
|
|
||||||
|
### Google Speech Recognition
|
||||||
|
- RFC 5646準拠の言語タグ形式
|
||||||
|
- 例: "ja-JP", "en-US", "zh-CN"
|
||||||
|
|
||||||
|
### OpenAI Whisper
|
||||||
|
- ISO 639-1言語コード(2文字)
|
||||||
|
- 例: "ja", "en", "zh"
|
||||||
|
|
||||||
|
### その他のエンジン
|
||||||
|
- エンジン固有の形式に対応
|
||||||
|
- マッピングテーブルによる変換
|
||||||
|
|
||||||
|
## 地域対応
|
||||||
|
|
||||||
|
### 同一言語の地域別対応
|
||||||
|
```python
|
||||||
|
# 英語の地域バリエーション
|
||||||
|
"English": [
|
||||||
|
{"country": "United States", "google_language_code": "en-US"},
|
||||||
|
{"country": "United Kingdom", "google_language_code": "en-GB"},
|
||||||
|
{"country": "Australia", "google_language_code": "en-AU"},
|
||||||
|
{"country": "Canada", "google_language_code": "en-CA"},
|
||||||
|
{"country": "India", "google_language_code": "en-IN"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方言・変種対応
|
||||||
|
```python
|
||||||
|
# 中国語の簡体字・繁体字対応
|
||||||
|
"Chinese Simplified": [
|
||||||
|
{"country": "China", "google_language_code": "zh-CN"}
|
||||||
|
],
|
||||||
|
"Chinese Traditional": [
|
||||||
|
{"country": "Taiwan", "google_language_code": "zh-TW"},
|
||||||
|
{"country": "Hong Kong", "google_language_code": "zh-HK"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 統合利用
|
||||||
|
|
||||||
|
### VRCTでの利用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_supported_transcription_languages():
|
||||||
|
"""音声認識対応言語の取得"""
|
||||||
|
languages = []
|
||||||
|
for language, countries in transcription_lang.items():
|
||||||
|
for country_data in countries:
|
||||||
|
languages.append({
|
||||||
|
"language": language,
|
||||||
|
"country": country_data["country"],
|
||||||
|
"display_name": f"{language} ({country_data['country']})",
|
||||||
|
"code": country_data["google_language_code"]
|
||||||
|
})
|
||||||
|
return languages
|
||||||
|
```
|
||||||
|
|
||||||
|
### エラーハンドリング
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_language_code(language: str, country: str = None) -> str:
|
||||||
|
"""安全な言語コード取得"""
|
||||||
|
try:
|
||||||
|
countries = transcription_lang.get(language, [])
|
||||||
|
if not countries:
|
||||||
|
return "en-US" # フォールバック
|
||||||
|
|
||||||
|
if country:
|
||||||
|
for country_data in countries:
|
||||||
|
if country_data["country"] == country:
|
||||||
|
return country_data["google_language_code"]
|
||||||
|
|
||||||
|
# 国指定なしまたは見つからない場合は最初の項目を返す
|
||||||
|
return countries[0]["google_language_code"]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
return "en-US" # エラー時のフォールバック
|
||||||
|
```
|
||||||
|
|
||||||
|
## 拡張性
|
||||||
|
|
||||||
|
### 新言語の追加
|
||||||
|
```python
|
||||||
|
# 新しい言語の追加例
|
||||||
|
transcription_lang["Turkish"] = [
|
||||||
|
{"country": "Turkey", "google_language_code": "tr-TR"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新エンジンへの対応
|
||||||
|
```python
|
||||||
|
# 新しいエンジンのコードフィールドを追加
|
||||||
|
transcription_lang["English"][0]["azure_language_code"] = "en-US"
|
||||||
|
transcription_lang["English"][0]["aws_language_code"] = "en-US"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 言語コードは各エンジンの仕様に依存
|
||||||
|
- 新しいエンジン追加時は対応コードの追加が必要
|
||||||
|
- 地域固有の音声認識精度差に注意
|
||||||
|
- エンジンによってサポート言語が異なる場合がある
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transcription_transcriber.py`: 音声認識エンジン本体
|
||||||
|
- `translation_languages.py`: 翻訳エンジン言語マッピング
|
||||||
|
- `config.py`: 言語設定管理
|
||||||
|
- `controller.py`: 言語選択UI制御
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
# transcription_recorder.py - 音声録音インターフェース
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
音声認識システムの入力となる音声データを録音するレコーダークラス群です。マイクとスピーカー出力の両方をサポートし、エネルギーレベル監視機能とともに音声データをキューに送信します。pyaudiowpatchライブラリを使用してWindowsの音声システムと統合します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 音声録音機能
|
||||||
|
- マイクからの音声録音
|
||||||
|
- スピーカー出力の録音(ループバック)
|
||||||
|
- リアルタイム音声データキューイング
|
||||||
|
|
||||||
|
### エネルギー監視
|
||||||
|
- 音声エネルギーレベルの監視
|
||||||
|
- 動的しきい値調整
|
||||||
|
- 無音検出
|
||||||
|
|
||||||
|
### デバイス対応
|
||||||
|
- 複数音声デバイスの対応
|
||||||
|
- デバイス固有設定の管理
|
||||||
|
- 自動デバイス選択
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### BaseRecorder クラス
|
||||||
|
```python
|
||||||
|
class BaseRecorder:
|
||||||
|
def __init__(self, source: Any, energy_threshold: int, dynamic_energy_threshold: bool, record_timeout: int)
|
||||||
|
```
|
||||||
|
|
||||||
|
基底レコーダークラス - 共通機能を提供
|
||||||
|
|
||||||
|
### SelectedMicRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedMicRecorder(BaseRecorder):
|
||||||
|
def __init__(self, device: dict, energy_threshold: int, dynamic_energy_threshold: bool, record_timeout: int)
|
||||||
|
```
|
||||||
|
|
||||||
|
選択されたマイクデバイスからの録音
|
||||||
|
|
||||||
|
### SelectedSpeakerRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedSpeakerRecorder(BaseRecorder):
|
||||||
|
def __init__(self, device: dict, energy_threshold: int, dynamic_energy_threshold: bool, record_timeout: int)
|
||||||
|
```
|
||||||
|
|
||||||
|
選択されたスピーカーデバイスからの録音(ループバック)
|
||||||
|
|
||||||
|
### エネルギー監視クラス群
|
||||||
|
|
||||||
|
#### BaseEnergyRecorder クラス
|
||||||
|
```python
|
||||||
|
class BaseEnergyRecorder:
|
||||||
|
def __init__(self, source: Any)
|
||||||
|
```
|
||||||
|
|
||||||
|
エネルギーレベル監視の基底クラス
|
||||||
|
|
||||||
|
#### SelectedMicEnergyRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedMicEnergyRecorder(BaseEnergyRecorder):
|
||||||
|
def __init__(self, device: dict)
|
||||||
|
```
|
||||||
|
|
||||||
|
マイクエネルギーレベルの監視
|
||||||
|
|
||||||
|
#### SelectedSpeakerEnergyRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedSpeakerEnergyRecorder(BaseEnergyRecorder):
|
||||||
|
def __init__(self, device: dict)
|
||||||
|
```
|
||||||
|
|
||||||
|
スピーカーエネルギーレベルの監視
|
||||||
|
|
||||||
|
### 統合録音クラス群
|
||||||
|
|
||||||
|
#### BaseEnergyAndAudioRecorder クラス
|
||||||
|
```python
|
||||||
|
class BaseEnergyAndAudioRecorder:
|
||||||
|
def __init__(self, source: Any, energy_threshold: int, dynamic_energy_threshold: bool,
|
||||||
|
phrase_time_limit: int, phrase_timeout: int, record_timeout: int)
|
||||||
|
```
|
||||||
|
|
||||||
|
音声録音とエネルギー監視を統合
|
||||||
|
|
||||||
|
#### SelectedMicEnergyAndAudioRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedMicEnergyAndAudioRecorder(BaseEnergyAndAudioRecorder):
|
||||||
|
def __init__(self, device: dict, energy_threshold: int, dynamic_energy_threshold: bool,
|
||||||
|
phrase_time_limit: int, phrase_timeout: int = 1, record_timeout: int = 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
マイクの音声録音とエネルギー監視を統合
|
||||||
|
|
||||||
|
#### SelectedSpeakerEnergyAndAudioRecorder クラス
|
||||||
|
```python
|
||||||
|
class SelectedSpeakerEnergyAndAudioRecorder(BaseEnergyAndAudioRecorder):
|
||||||
|
def __init__(self, device: dict, energy_threshold: int, dynamic_energy_threshold: bool,
|
||||||
|
phrase_time_limit: int, phrase_timeout: int = 1, record_timeout: int = 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
スピーカーの音声録音とエネルギー監視を統合
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 録音制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
adjustForNoise() -> None
|
||||||
|
```
|
||||||
|
- 環境ノイズに合わせたしきい値調整
|
||||||
|
- 録音開始前の較正
|
||||||
|
|
||||||
|
```python
|
||||||
|
recordIntoQueue(audio_queue: Queue) -> None
|
||||||
|
```
|
||||||
|
- 音声データの継続的キューイング
|
||||||
|
- バックグラウンドスレッドでの実行
|
||||||
|
|
||||||
|
```python
|
||||||
|
pause() -> None
|
||||||
|
resume() -> None
|
||||||
|
stop() -> None
|
||||||
|
```
|
||||||
|
- 録音の一時停止・再開・停止制御
|
||||||
|
|
||||||
|
### エネルギー監視
|
||||||
|
|
||||||
|
```python
|
||||||
|
recordIntoQueue(energy_queue: Queue) -> None
|
||||||
|
```
|
||||||
|
- エネルギーレベルのキューイング
|
||||||
|
- リアルタイム監視データの提供
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的なマイク録音
|
||||||
|
|
||||||
|
```python
|
||||||
|
from queue import Queue
|
||||||
|
from models.transcription.transcription_recorder import SelectedMicRecorder
|
||||||
|
|
||||||
|
# デバイス設定
|
||||||
|
mic_device = {
|
||||||
|
"name": "マイク (USB Audio Device)",
|
||||||
|
"index": 0,
|
||||||
|
"channels": 1,
|
||||||
|
"sample_rate": 16000
|
||||||
|
}
|
||||||
|
|
||||||
|
# 録音設定
|
||||||
|
energy_threshold = 300
|
||||||
|
dynamic_threshold = True
|
||||||
|
record_timeout = 5
|
||||||
|
|
||||||
|
# レコーダー初期化
|
||||||
|
recorder = SelectedMicRecorder(
|
||||||
|
device=mic_device,
|
||||||
|
energy_threshold=energy_threshold,
|
||||||
|
dynamic_energy_threshold=dynamic_threshold,
|
||||||
|
record_timeout=record_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# 音声キューの作成
|
||||||
|
audio_queue = Queue()
|
||||||
|
|
||||||
|
# 録音開始
|
||||||
|
recorder.adjustForNoise() # ノイズ調整
|
||||||
|
recorder.recordIntoQueue(audio_queue)
|
||||||
|
|
||||||
|
# 音声データの取得
|
||||||
|
while True:
|
||||||
|
if not audio_queue.empty():
|
||||||
|
audio_data = audio_queue.get()
|
||||||
|
print(f"音声データ受信: {len(audio_data)} bytes")
|
||||||
|
```
|
||||||
|
|
||||||
|
### スピーカー録音(ループバック)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_recorder import SelectedSpeakerRecorder
|
||||||
|
|
||||||
|
# スピーカーデバイス設定
|
||||||
|
speaker_device = {
|
||||||
|
"name": "スピーカー (USB Audio Device)",
|
||||||
|
"index": 1,
|
||||||
|
"channels": 2,
|
||||||
|
"sample_rate": 44100
|
||||||
|
}
|
||||||
|
|
||||||
|
# スピーカーレコーダー
|
||||||
|
recorder = SelectedSpeakerRecorder(
|
||||||
|
device=speaker_device,
|
||||||
|
energy_threshold=500,
|
||||||
|
dynamic_energy_threshold=False,
|
||||||
|
record_timeout=3
|
||||||
|
)
|
||||||
|
|
||||||
|
audio_queue = Queue()
|
||||||
|
recorder.recordIntoQueue(audio_queue)
|
||||||
|
```
|
||||||
|
|
||||||
|
### エネルギー監視
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_recorder import SelectedMicEnergyRecorder
|
||||||
|
|
||||||
|
# エネルギー監視のみ
|
||||||
|
energy_recorder = SelectedMicEnergyRecorder(mic_device)
|
||||||
|
energy_queue = Queue()
|
||||||
|
|
||||||
|
energy_recorder.recordIntoQueue(energy_queue)
|
||||||
|
|
||||||
|
# エネルギーレベルの取得
|
||||||
|
while True:
|
||||||
|
if not energy_queue.empty():
|
||||||
|
energy_level = energy_queue.get()
|
||||||
|
print(f"エネルギーレベル: {energy_level}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 統合録音(音声+エネルギー)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder
|
||||||
|
|
||||||
|
# 統合レコーダー
|
||||||
|
integrated_recorder = SelectedMicEnergyAndAudioRecorder(
|
||||||
|
device=mic_device,
|
||||||
|
energy_threshold=300,
|
||||||
|
dynamic_energy_threshold=True,
|
||||||
|
phrase_time_limit=5, # フレーズ制限時間
|
||||||
|
phrase_timeout=1, # フレーズタイムアウト
|
||||||
|
record_timeout=5 # 録音タイムアウト
|
||||||
|
)
|
||||||
|
|
||||||
|
audio_queue = Queue()
|
||||||
|
energy_queue = Queue()
|
||||||
|
|
||||||
|
# 両方のキューに同時出力
|
||||||
|
integrated_recorder.recordIntoQueue(audio_queue, energy_queue)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定パラメータ
|
||||||
|
|
||||||
|
### しきい値設定
|
||||||
|
- **energy_threshold**: 音声検出のエネルギーしきい値
|
||||||
|
- **dynamic_energy_threshold**: 動的しきい値調整の有効・無効
|
||||||
|
|
||||||
|
### タイムアウト設定
|
||||||
|
- **record_timeout**: 録音継続時間の上限
|
||||||
|
- **phrase_timeout**: フレーズ間の無音許容時間
|
||||||
|
- **phrase_time_limit**: 単一フレーズの最大長
|
||||||
|
|
||||||
|
### デバイス設定
|
||||||
|
- **name**: デバイス名
|
||||||
|
- **index**: デバイスインデックス
|
||||||
|
- **channels**: チャンネル数(1=モノラル、2=ステレオ)
|
||||||
|
- **sample_rate**: サンプリングレート(Hz)
|
||||||
|
|
||||||
|
## デバイス対応
|
||||||
|
|
||||||
|
### マイクデバイス
|
||||||
|
- USB マイク
|
||||||
|
- 内蔵マイク
|
||||||
|
- Bluetooth マイク
|
||||||
|
- 仮想マイクデバイス
|
||||||
|
|
||||||
|
### スピーカーデバイス(ループバック)
|
||||||
|
- USB スピーカー/ヘッドフォン
|
||||||
|
- 内蔵スピーカー
|
||||||
|
- Bluetooth スピーカー
|
||||||
|
- 仮想音声デバイス
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### デバイスエラー
|
||||||
|
- デバイス接続失敗の検出
|
||||||
|
- 適切なエラーメッセージの提供
|
||||||
|
|
||||||
|
### 音声フォーマットエラー
|
||||||
|
- 非対応フォーマットの検出
|
||||||
|
- 自動フォーマット変換
|
||||||
|
|
||||||
|
### メモリエラー
|
||||||
|
- キューオーバーフローの防止
|
||||||
|
- メモリ使用量の最適化
|
||||||
|
|
||||||
|
## パフォーマンス特性
|
||||||
|
|
||||||
|
### レイテンシ
|
||||||
|
- 低レイテンシ録音(~10ms)
|
||||||
|
- リアルタイム処理最適化
|
||||||
|
|
||||||
|
### スループット
|
||||||
|
- 連続録音対応
|
||||||
|
- 高サンプリングレート対応
|
||||||
|
|
||||||
|
### メモリ使用量
|
||||||
|
- 効率的なバッファ管理
|
||||||
|
- キューサイズの最適化
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `speech_recognition`: 音声認識ライブラリ
|
||||||
|
- `pyaudiowpatch`: Windows音声システム統合
|
||||||
|
- `queue`: データキューイング
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `datetime`: タイムスタンプ機能
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- Windows専用(pyaudiowpatchによる制限)
|
||||||
|
- 適切な音声デバイスドライバーが必要
|
||||||
|
- 排他制御による同時デバイスアクセス制限
|
||||||
|
- 高サンプリングレート使用時のCPU使用率上昇
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transcription_transcriber.py`: 音声認識エンジン
|
||||||
|
- `device_manager.py`: デバイス管理
|
||||||
|
- `config.py`: 録音設定管理
|
||||||
|
- `model.py`: 録音制御統合
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
# transcription_transcriber.py - 音声文字起こしエンジン
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
音声データを文字テキストに変換する音声認識エンジンのメインクラスです。Google Speech RecognitionとOpenAI Whisper(faster-whisper)の両方をサポートし、オンライン・オフラインの音声認識を統合的に管理します。キューベースの非同期処理により、リアルタイム音声認識を実現します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 音声認識エンジン
|
||||||
|
- Google Speech Recognition(オンライン)
|
||||||
|
- OpenAI Whisper(faster-whisper、オフライン)
|
||||||
|
- エンジン自動切り替え機能
|
||||||
|
|
||||||
|
### リアルタイム処理
|
||||||
|
- 音声キューからの継続的データ処理
|
||||||
|
- 非同期音声認識処理
|
||||||
|
- 結果の即座通知
|
||||||
|
|
||||||
|
### 多言語対応
|
||||||
|
- 複数言語の同時認識
|
||||||
|
- 地域固有言語コードの対応
|
||||||
|
- 自動言語検出
|
||||||
|
|
||||||
|
### 音声品質制御
|
||||||
|
- 音声品質フィルタリング
|
||||||
|
- ノイズ除去機能
|
||||||
|
- 信頼度スコア評価
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### AudioTranscriber クラス
|
||||||
|
```python
|
||||||
|
class AudioTranscriber:
|
||||||
|
def __init__(self, speaker: bool, source: Any, phrase_timeout: int, max_phrases: int,
|
||||||
|
transcription_engine: str, root: Optional[str] = None,
|
||||||
|
whisper_weight_type: Optional[str] = None, device: str = "cpu",
|
||||||
|
device_index: int = 0, compute_type: str = "auto")
|
||||||
|
```
|
||||||
|
|
||||||
|
音声認識の中核クラス
|
||||||
|
|
||||||
|
#### 初期化パラメータ
|
||||||
|
- **speaker**: スピーカー音声かマイク音声か
|
||||||
|
- **source**: 音声ソース
|
||||||
|
- **phrase_timeout**: フレーズタイムアウト(秒)
|
||||||
|
- **max_phrases**: 最大フレーズ数
|
||||||
|
- **transcription_engine**: 認識エンジン("Google"/"Whisper")
|
||||||
|
- **whisper_weight_type**: Whisperモデル種類
|
||||||
|
- **device**: 計算デバイス("cpu"/"cuda")
|
||||||
|
- **device_index**: デバイスインデックス
|
||||||
|
- **compute_type**: 計算精度タイプ
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 音声認識処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
transcribeAudioQueue(audio_queue: Queue, languages: List[str], countries: List[str],
|
||||||
|
avg_logprob: float = -0.8, no_speech_prob: float = 0.6) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
音声キューからの継続的音声認識
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **audio_queue**: 音声データキュー
|
||||||
|
- **languages**: 認識対象言語リスト
|
||||||
|
- **countries**: 地域コードリスト
|
||||||
|
- **avg_logprob**: Whisper平均対数確率しきい値
|
||||||
|
- **no_speech_prob**: Whisper無音判定しきい値
|
||||||
|
|
||||||
|
### 結果管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
getTranscript() -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
最新の認識結果を取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
updateTranscript(result: dict) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
認識結果の更新と通知
|
||||||
|
|
||||||
|
```python
|
||||||
|
clearTranscriptData() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
認識データのクリア
|
||||||
|
|
||||||
|
### 音声データ処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
processMicData() -> AudioData
|
||||||
|
```
|
||||||
|
|
||||||
|
マイク音声データの前処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
processSpeakerData() -> AudioData
|
||||||
|
```
|
||||||
|
|
||||||
|
スピーカー音声データの前処理
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な音声認識
|
||||||
|
|
||||||
|
```python
|
||||||
|
from queue import Queue
|
||||||
|
from models.transcription.transcription_transcriber import AudioTranscriber
|
||||||
|
|
||||||
|
# 音声認識の初期化
|
||||||
|
transcriber = AudioTranscriber(
|
||||||
|
speaker=False, # マイク音声
|
||||||
|
source=mic_source, # 音声ソース
|
||||||
|
phrase_timeout=3, # 3秒のフレーズタイムアウト
|
||||||
|
max_phrases=10, # 最大10フレーズ
|
||||||
|
transcription_engine="Google", # Google音声認識
|
||||||
|
device="cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 音声キューの準備
|
||||||
|
audio_queue = Queue()
|
||||||
|
|
||||||
|
# 認識対象言語の設定
|
||||||
|
languages = ["Japanese", "English"]
|
||||||
|
countries = ["Japan", "United States"]
|
||||||
|
|
||||||
|
# 音声認識の実行
|
||||||
|
def transcription_loop():
|
||||||
|
while True:
|
||||||
|
success = transcriber.transcribeAudioQueue(
|
||||||
|
audio_queue, languages, countries
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
result = transcriber.getTranscript()
|
||||||
|
print(f"認識結果: {result['text']}")
|
||||||
|
print(f"言語: {result['language']}")
|
||||||
|
|
||||||
|
# バックグラウンドで実行
|
||||||
|
import threading
|
||||||
|
thread = threading.Thread(target=transcription_loop)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Whisperエンジンの使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Whisper音声認識の初期化
|
||||||
|
whisper_transcriber = AudioTranscriber(
|
||||||
|
speaker=True, # スピーカー音声
|
||||||
|
source=speaker_source,
|
||||||
|
phrase_timeout=5,
|
||||||
|
max_phrases=5,
|
||||||
|
transcription_engine="Whisper",
|
||||||
|
whisper_weight_type="base", # Whisperモデル
|
||||||
|
device="cuda", # CUDA使用
|
||||||
|
device_index=0,
|
||||||
|
compute_type="float16" # 半精度浮動小数点
|
||||||
|
)
|
||||||
|
|
||||||
|
# Whisper固有パラメータでの認識
|
||||||
|
success = whisper_transcriber.transcribeAudioQueue(
|
||||||
|
audio_queue, languages, countries,
|
||||||
|
avg_logprob=-0.5, # より厳しい品質しきい値
|
||||||
|
no_speech_prob=0.4 # より敏感な無音検出
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### コールバック処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def on_transcription_result(result):
|
||||||
|
"""認識結果のコールバック処理"""
|
||||||
|
if result["text"]:
|
||||||
|
print(f"認識成功: {result['text']}")
|
||||||
|
print(f"言語: {result['language']}")
|
||||||
|
print(f"信頼度: {result.get('confidence', 'N/A')}")
|
||||||
|
else:
|
||||||
|
print("音声認識失敗")
|
||||||
|
|
||||||
|
# 結果通知の設定
|
||||||
|
transcriber.transcript_changed_event.set() # イベント設定
|
||||||
|
```
|
||||||
|
|
||||||
|
### エラーハンドリング付きの使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_transcription(transcriber, audio_queue, languages, countries):
|
||||||
|
"""安全な音声認識処理"""
|
||||||
|
try:
|
||||||
|
success = transcriber.transcribeAudioQueue(
|
||||||
|
audio_queue, languages, countries
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
result = transcriber.getTranscript()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return {"text": False, "language": None, "error": "認識失敗"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"音声認識エラー: {e}")
|
||||||
|
return {"text": False, "language": None, "error": str(e)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 認識エンジン比較
|
||||||
|
|
||||||
|
### Google Speech Recognition
|
||||||
|
|
||||||
|
#### 利点
|
||||||
|
- 高い認識精度
|
||||||
|
- 多言語対応
|
||||||
|
- リアルタイム処理
|
||||||
|
- ノイズ耐性
|
||||||
|
|
||||||
|
#### 制限
|
||||||
|
- インターネット接続必須
|
||||||
|
- API制限
|
||||||
|
- プライバシー懸念
|
||||||
|
- レイテンシ
|
||||||
|
|
||||||
|
### OpenAI Whisper(faster-whisper)
|
||||||
|
|
||||||
|
#### 利点
|
||||||
|
- オフライン動作
|
||||||
|
- プライバシー保護
|
||||||
|
- 高精度
|
||||||
|
- 多言語対応
|
||||||
|
|
||||||
|
#### 制限
|
||||||
|
- 初回起動時間
|
||||||
|
- メモリ使用量
|
||||||
|
- CUDA推奨
|
||||||
|
- モデルファイル必要
|
||||||
|
|
||||||
|
## 設定パラメータ
|
||||||
|
|
||||||
|
### フレーズ制御
|
||||||
|
- **phrase_timeout**: フレーズ間無音時間(秒)
|
||||||
|
- **max_phrases**: バッファ内最大フレーズ数
|
||||||
|
|
||||||
|
### Whisper品質設定
|
||||||
|
- **avg_logprob**: 平均対数確率しきい値(-1.0〜0.0)
|
||||||
|
- **no_speech_prob**: 無音判定しきい値(0.0〜1.0)
|
||||||
|
|
||||||
|
### 計算設定
|
||||||
|
- **device**: "cpu" または "cuda"
|
||||||
|
- **compute_type**: "float32", "float16", "int8" など
|
||||||
|
|
||||||
|
## 音声データフォーマット
|
||||||
|
|
||||||
|
### 入力形式
|
||||||
|
- サンプリングレート: 16kHz推奨
|
||||||
|
- ビット深度: 16bit
|
||||||
|
- チャンネル: モノラル推奨
|
||||||
|
- フォーマット: WAV、FLAC等
|
||||||
|
|
||||||
|
### 処理フロー
|
||||||
|
1. 音声キューからデータ取得
|
||||||
|
2. 音声フォーマット正規化
|
||||||
|
3. 音声認識エンジン実行
|
||||||
|
4. 結果の後処理・フィルタリング
|
||||||
|
5. 最終結果の通知
|
||||||
|
|
||||||
|
## パフォーマンス最適化
|
||||||
|
|
||||||
|
### メモリ管理
|
||||||
|
- 音声バッファの適切なサイズ設定
|
||||||
|
- 不要な音声データの早期解放
|
||||||
|
- Whisperモデルのメモリ効率化
|
||||||
|
|
||||||
|
### 計算最適化
|
||||||
|
- CUDA使用による高速化
|
||||||
|
- 適切な計算精度選択
|
||||||
|
- バッチ処理の活用
|
||||||
|
|
||||||
|
### レイテンシ削減
|
||||||
|
- 音声バッファサイズの最適化
|
||||||
|
- エンジン切り替えの高速化
|
||||||
|
- キャッシュ機能の活用
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### ネットワークエラー
|
||||||
|
- Google API接続失敗の検出
|
||||||
|
- 自動Whisperエンジン切り替え
|
||||||
|
|
||||||
|
### 音声品質エラー
|
||||||
|
- 低品質音声の検出・フィルタリング
|
||||||
|
- ノイズレベル監視
|
||||||
|
|
||||||
|
### リソースエラー
|
||||||
|
- VRAM不足の検出
|
||||||
|
- メモリ不足時の対応
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `speech_recognition`: Google音声認識
|
||||||
|
- `faster_whisper`: Whisper音声認識
|
||||||
|
- `pyaudiowpatch`: 音声入力
|
||||||
|
- `pydub`: 音声処理
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `torch`: CUDA計算
|
||||||
|
- `utils`: エラーログ機能
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- Google APIは使用制限あり
|
||||||
|
- Whisperは初回起動に時間要
|
||||||
|
- CUDA使用時はVRAM消費に注意
|
||||||
|
- 音声品質が認識精度に大きく影響
|
||||||
|
- 多言語認識時は処理負荷増加
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transcription_recorder.py`: 音声録音
|
||||||
|
- `transcription_whisper.py`: Whisperモデル管理
|
||||||
|
- `transcription_languages.py`: 言語コード管理
|
||||||
|
- `config.py`: 認識設定管理
|
||||||
|
- `model.py`: 音声認識統合制御
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
# transcription_whisper.py - Whisperモデル管理
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
OpenAI Whisper(faster-whisper)モデルのダウンロード、検証、読み込みを管理するユーティリティモジュールです。複数のモデルサイズをサポートし、Hugging Face Hubからの自動ダウンロード機能とファイル整合性チェック機能を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### モデル管理
|
||||||
|
- 複数Whisperモデルサイズの対応
|
||||||
|
- Hugging Face Hubからの自動ダウンロード
|
||||||
|
- モデルファイルの整合性検証
|
||||||
|
|
||||||
|
### ダウンロード機能
|
||||||
|
- 進捗表示付きダウンロード
|
||||||
|
- レジューム対応
|
||||||
|
- エラーハンドリング
|
||||||
|
|
||||||
|
### モデル読み込み
|
||||||
|
- 効率的なモデル初期化
|
||||||
|
- CUDA対応
|
||||||
|
- 計算タイプ最適化
|
||||||
|
|
||||||
|
## サポートモデル
|
||||||
|
|
||||||
|
### 利用可能なモデル
|
||||||
|
```python
|
||||||
|
_MODELS = {
|
||||||
|
"tiny": "Systran/faster-whisper-tiny", # ~39MB
|
||||||
|
"base": "Systran/faster-whisper-base", # ~74MB
|
||||||
|
"small": "Systran/faster-whisper-small", # ~244MB
|
||||||
|
"medium": "Systran/faster-whisper-medium", # ~769MB
|
||||||
|
"large-v1": "Systran/faster-whisper-large-v1", # ~1.5GB
|
||||||
|
"large-v2": "Systran/faster-whisper-large-v2", # ~1.5GB
|
||||||
|
"large-v3": "Systran/faster-whisper-large-v3", # ~1.5GB
|
||||||
|
"large-v3-turbo-int8": "Zoont/faster-whisper-large-v3-turbo-int8-ct2", # ~794MB
|
||||||
|
"large-v3-turbo": "deepdml/faster-whisper-large-v3-turbo-ct2" # ~1.58GB
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### モデル特性比較
|
||||||
|
|
||||||
|
#### tiny
|
||||||
|
- **サイズ**: ~39MB
|
||||||
|
- **精度**: 低
|
||||||
|
- **速度**: 最高速
|
||||||
|
- **用途**: リアルタイム処理、リソース制限環境
|
||||||
|
|
||||||
|
#### base
|
||||||
|
- **サイズ**: ~74MB
|
||||||
|
- **精度**: 中程度
|
||||||
|
- **速度**: 高速
|
||||||
|
- **用途**: 一般的な用途、バランス重視
|
||||||
|
|
||||||
|
#### small
|
||||||
|
- **サイズ**: ~244MB
|
||||||
|
- **精度**: 良好
|
||||||
|
- **速度**: 中程度
|
||||||
|
- **用途**: 品質重視、モバイル環境
|
||||||
|
|
||||||
|
#### medium
|
||||||
|
- **サイズ**: ~769MB
|
||||||
|
- **精度**: 高
|
||||||
|
- **速度**: やや低速
|
||||||
|
- **用途**: 高品質認識、デスクトップ環境
|
||||||
|
|
||||||
|
#### large系
|
||||||
|
- **サイズ**: ~1.5GB
|
||||||
|
- **精度**: 最高
|
||||||
|
- **速度**: 低速
|
||||||
|
- **用途**: 最高品質、サーバー環境
|
||||||
|
|
||||||
|
## 主要関数
|
||||||
|
|
||||||
|
### ファイルダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
downloadFile(url: str, path: str, func: Optional[Callable[[float], None]] = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
ファイルのストリームダウンロード
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **url**: ダウンロードURL
|
||||||
|
- **path**: 保存先パス
|
||||||
|
- **func**: 進捗コールバック関数
|
||||||
|
|
||||||
|
### モデル検証
|
||||||
|
|
||||||
|
```python
|
||||||
|
checkWhisperWeight(root: str, weight_type: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Whisperモデルの利用可能性確認
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **root**: アプリケーションルートパス
|
||||||
|
- **weight_type**: モデルタイプ("tiny", "base"等)
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: モデルが利用可能かどうか
|
||||||
|
|
||||||
|
### モデルダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
downloadWhisperWeight(root: str, weight_type: str,
|
||||||
|
callback: Optional[Callable[[float], None]] = None,
|
||||||
|
end_callback: Optional[Callable[[], None]] = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Whisperモデルのダウンロード
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **root**: アプリケーションルートパス
|
||||||
|
- **weight_type**: ダウンロードするモデルタイプ
|
||||||
|
- **callback**: 進捗コールバック
|
||||||
|
- **end_callback**: 完了コールバック
|
||||||
|
|
||||||
|
### モデル読み込み
|
||||||
|
|
||||||
|
```python
|
||||||
|
getWhisperModel(root: str, weight_type: str, device: str = "cpu",
|
||||||
|
device_index: int = 0, compute_type: str = "auto") -> WhisperModel
|
||||||
|
```
|
||||||
|
|
||||||
|
Whisperモデルの初期化
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **root**: アプリケーションルートパス
|
||||||
|
- **weight_type**: 使用するモデルタイプ
|
||||||
|
- **device**: 計算デバイス("cpu"/"cuda")
|
||||||
|
- **device_index**: デバイスインデックス
|
||||||
|
- **compute_type**: 計算精度タイプ
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **WhisperModel**: 初期化されたWhisperモデルインスタンス
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### モデルの確認とダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_whisper import checkWhisperWeight, downloadWhisperWeight
|
||||||
|
|
||||||
|
root_path = "."
|
||||||
|
model_type = "base"
|
||||||
|
|
||||||
|
# モデルの利用可能性確認
|
||||||
|
if not checkWhisperWeight(root_path, model_type):
|
||||||
|
print(f"{model_type}モデルが見つかりません。ダウンロードします...")
|
||||||
|
|
||||||
|
# 進捗コールバック
|
||||||
|
def progress_callback(progress):
|
||||||
|
print(f"ダウンロード進捗: {progress:.1%}")
|
||||||
|
|
||||||
|
# 完了コールバック
|
||||||
|
def completion_callback():
|
||||||
|
print("ダウンロード完了!")
|
||||||
|
|
||||||
|
# モデルダウンロード
|
||||||
|
downloadWhisperWeight(
|
||||||
|
root=root_path,
|
||||||
|
weight_type=model_type,
|
||||||
|
callback=progress_callback,
|
||||||
|
end_callback=completion_callback
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"{model_type}モデルは利用可能です")
|
||||||
|
```
|
||||||
|
|
||||||
|
### モデルの読み込みと使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transcription.transcription_whisper import getWhisperModel
|
||||||
|
|
||||||
|
# CPUでのモデル読み込み
|
||||||
|
model = getWhisperModel(
|
||||||
|
root=".",
|
||||||
|
weight_type="base",
|
||||||
|
device="cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CUDAでのモデル読み込み(GPU使用)
|
||||||
|
gpu_model = getWhisperModel(
|
||||||
|
root=".",
|
||||||
|
weight_type="small",
|
||||||
|
device="cuda",
|
||||||
|
device_index=0,
|
||||||
|
compute_type="float16" # 半精度で高速化
|
||||||
|
)
|
||||||
|
|
||||||
|
# 音声認識の実行
|
||||||
|
audio_file = "audio.wav"
|
||||||
|
segments, info = model.transcribe(audio_file, language="ja")
|
||||||
|
|
||||||
|
for segment in segments:
|
||||||
|
print(f"{segment.start:.1f}s - {segment.end:.1f}s: {segment.text}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### エラーハンドリング付きの使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_model_loading(root, weight_type, device="cpu"):
|
||||||
|
"""安全なモデル読み込み"""
|
||||||
|
try:
|
||||||
|
# モデル存在確認
|
||||||
|
if not checkWhisperWeight(root, weight_type):
|
||||||
|
print(f"モデル {weight_type} をダウンロード中...")
|
||||||
|
downloadWhisperWeight(root, weight_type)
|
||||||
|
|
||||||
|
# モデル読み込み
|
||||||
|
model = getWhisperModel(root, weight_type, device)
|
||||||
|
return model
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"モデル読み込みエラー: {e}")
|
||||||
|
# フォールバック: より小さなモデルを試す
|
||||||
|
if weight_type != "tiny":
|
||||||
|
return safe_model_loading(root, "tiny", device)
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 進捗表示付きダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def download_with_progress(root, weight_type):
|
||||||
|
"""進捗表示付きダウンロード"""
|
||||||
|
def show_progress(progress):
|
||||||
|
bar_length = 40
|
||||||
|
filled_length = int(bar_length * progress)
|
||||||
|
bar = '█' * filled_length + '-' * (bar_length - filled_length)
|
||||||
|
sys.stdout.write(f'\r[{bar}] {progress:.1%}')
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def download_complete():
|
||||||
|
print("\nダウンロード完了!")
|
||||||
|
|
||||||
|
print(f"Whisper {weight_type} モデルをダウンロード中...")
|
||||||
|
downloadWhisperWeight(root, weight_type, show_progress, download_complete)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ディレクトリ構造
|
||||||
|
|
||||||
|
### モデルファイル配置
|
||||||
|
```
|
||||||
|
root/
|
||||||
|
└── weights/
|
||||||
|
└── whisper/
|
||||||
|
├── tiny/
|
||||||
|
│ ├── config.json
|
||||||
|
│ ├── preprocessor_config.json
|
||||||
|
│ ├── model.bin
|
||||||
|
│ ├── tokenizer.json
|
||||||
|
│ └── vocabulary.txt
|
||||||
|
├── base/
|
||||||
|
└── small/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 必要ファイル
|
||||||
|
```python
|
||||||
|
_FILENAMES = [
|
||||||
|
"config.json", # モデル設定
|
||||||
|
"preprocessor_config.json", # 前処理設定
|
||||||
|
"model.bin", # モデルウェイト
|
||||||
|
"tokenizer.json", # トークナイザー
|
||||||
|
"vocabulary.txt", # 語彙ファイル
|
||||||
|
"vocabulary.json" # 語彙ファイル(JSON形式)
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
### メモリ使用量
|
||||||
|
- **tiny**: ~100MB RAM
|
||||||
|
- **base**: ~200MB RAM
|
||||||
|
- **small**: ~500MB RAM
|
||||||
|
- **medium**: ~1.5GB RAM
|
||||||
|
- **large**: ~3GB RAM
|
||||||
|
|
||||||
|
### VRAM使用量(CUDA使用時)
|
||||||
|
- **tiny**: ~200MB VRAM
|
||||||
|
- **base**: ~300MB VRAM
|
||||||
|
- **small**: ~600MB VRAM
|
||||||
|
- **medium**: ~1.8GB VRAM
|
||||||
|
- **large**: ~3.5GB VRAM
|
||||||
|
|
||||||
|
### 処理速度(目安)
|
||||||
|
- **tiny**: リアルタイム処理可能
|
||||||
|
- **base**: 1x-2x リアルタイム
|
||||||
|
- **small**: 0.5x-1x リアルタイム
|
||||||
|
- **medium**: 0.2x-0.5x リアルタイム
|
||||||
|
- **large**: 0.1x-0.3x リアルタイム
|
||||||
|
|
||||||
|
## 計算タイプ設定
|
||||||
|
|
||||||
|
### 利用可能な計算タイプ
|
||||||
|
- **float32**: 最高精度、低速
|
||||||
|
- **float16**: 高精度、中速(CUDA推奨)
|
||||||
|
- **int8**: 中精度、高速
|
||||||
|
- **int8_float16**: 混合精度、バランス
|
||||||
|
|
||||||
|
### 推奨設定
|
||||||
|
```python
|
||||||
|
# CPU使用時
|
||||||
|
compute_type = "int8" # 速度重視
|
||||||
|
|
||||||
|
# CUDA使用時(RTX以上)
|
||||||
|
compute_type = "float16" # 精度と速度のバランス
|
||||||
|
|
||||||
|
# CUDA使用時(VRAM制限)
|
||||||
|
compute_type = "int8_float16" # メモリ効率重視
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### ダウンロードエラー
|
||||||
|
- ネットワーク接続失敗
|
||||||
|
- ディスク容量不足
|
||||||
|
- 権限不足
|
||||||
|
|
||||||
|
### モデル読み込みエラー
|
||||||
|
- VRAM不足
|
||||||
|
- 破損したモデルファイル
|
||||||
|
- 非対応デバイス
|
||||||
|
|
||||||
|
### 対応策
|
||||||
|
```python
|
||||||
|
def robust_model_loading(root, preferred_type="base"):
|
||||||
|
"""堅牢なモデル読み込み"""
|
||||||
|
model_priority = ["tiny", "base", "small", "medium"]
|
||||||
|
|
||||||
|
# 優先モデルを先頭に配置
|
||||||
|
if preferred_type in model_priority:
|
||||||
|
model_priority.remove(preferred_type)
|
||||||
|
model_priority.insert(0, preferred_type)
|
||||||
|
|
||||||
|
for model_type in model_priority:
|
||||||
|
try:
|
||||||
|
if checkWhisperWeight(root, model_type):
|
||||||
|
return getWhisperModel(root, model_type)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{model_type} モデル読み込み失敗: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise RuntimeError("利用可能なWhisperモデルがありません")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `faster_whisper`: Whisperエンジン
|
||||||
|
- `requests`: ファイルダウンロード
|
||||||
|
- `utils`: ユーティリティ機能
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `torch`: CUDA計算(GPU使用時)
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 初回モデル読み込み時はダウンロードに時間がかかる
|
||||||
|
- 大きなモデルほど高精度だが、メモリとVRAMを大量消費
|
||||||
|
- CUDAを使用する場合は適切なGPUドライバーが必要
|
||||||
|
- モデルファイルの整合性チェックが重要
|
||||||
|
- ネットワーク環境によってダウンロード時間が大きく変動
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transcription_transcriber.py`: Whisper音声認識エンジン
|
||||||
|
- `config.py`: Whisperモデル設定管理
|
||||||
|
- `utils.py`: 計算デバイス管理
|
||||||
|
- `model.py`: Whisper統合制御
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# translation_gemini.py - Gemini 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
Google Gemini / Gemma 系モデルを翻訳用途で利用するためのクライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を統一インターフェースで提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
- 新規追加: Gemini クライアント統合
|
||||||
|
- 除外キーワード (`audio`, `image`, `veo`, `tts`, `robotics`, `computer-use`) により非テキスト指向モデルをフィルタ
|
||||||
|
- `generateContent` をサポートするモデルのみ採用
|
||||||
|
- YAML (`prompt/translation_gemini.yml`) からシステムプロンプト (`system_prompt`) をロード
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 正確性 | 非テキスト特化モデル除外で翻訳品質安定 |
|
||||||
|
| 保守性 | 明示的フィルタリングロジックで再利用容易 |
|
||||||
|
| 一貫性 | 他 LLM クライアントとの API 形状統一 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- API Key 認証確認
|
||||||
|
- Gemini/Gemma 系モデル列挙とフィルタリング
|
||||||
|
- モデル選択検証と内部保持
|
||||||
|
- LangChain `ChatGoogleGenerativeAI` インスタンス生成
|
||||||
|
- システムプロンプトによる翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GeminiClient:
|
||||||
|
def __init__(root_path: str = None)
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getAuthKey() -> str | None
|
||||||
|
def setAuthKey(api_key: str) -> bool
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setAuthKey`: `_authentication_check` 成功時のみ内部保存
|
||||||
|
- `getModelList`: フィルタリング適用後ソート
|
||||||
|
- `setModel`: 取得済みモデル一覧内のみ受理
|
||||||
|
- `updateClient`: `ChatGoogleGenerativeAI` を再構築
|
||||||
|
- `translate`: システム + ユーザメッセージ構築→呼び出し→レスポンス正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = GeminiClient()
|
||||||
|
if client.setAuthKey("GEMINI_API_KEY"):
|
||||||
|
models = client.getModelList()
|
||||||
|
if models:
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
result = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `google.genai`: モデル列挙 / 認証
|
||||||
|
- `langchain_google_genai.ChatGoogleGenerativeAI`: LangChain ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 非テキスト向けモデル (画像/音声/ロボティクス等) は除外
|
||||||
|
- ストリーミング無効 (streaming=False)
|
||||||
|
- API Key 必須 (未設定時 getModelList 不可)
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- 詳細エラーを包括的に扱わない (上位層でロギング/フォールバック)
|
||||||
|
- 複雑レスポンス構造は単純文字列へ normalize のみ
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# translation_groq.py - Groq 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
Groq API を用いた高速 LLM 翻訳クライアントラッパー。OpenAI 互換エンドポイント (`https://api.groq.com/openai/v1`) を利用し、モデル一覧取得・認証・モデル選択・翻訳実行を提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-12-10)
|
||||||
|
|
||||||
|
- Groq API サポートを新規追加
|
||||||
|
- OpenAI 互換エンドポイント経由で高速 LLM 推論を実現
|
||||||
|
- 除外キーワード (`whisper`, `embedding`, `image`, `tts`, `audio`, `search`, `transcribe`, `diarize`, `vision`) によるテキスト処理モデルのフィルタリング
|
||||||
|
- YAML (`prompt/translation_groq.yml`) からシステムプロンプトをロード
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 高速化 | Groq の専用ハードウェアによる高速推論 |
|
||||||
|
| 互換性 | OpenAI 互換 API で既存実装との一貫性維持 |
|
||||||
|
| 保守性 | OpenAI クライアントと同様の設計で保守容易 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- Groq API Key (`gsk-` で始まる40文字以上) を用いた認証確認
|
||||||
|
- 利用可能モデルのフィルタリングとソート
|
||||||
|
- 選択モデルの検証と内部保持
|
||||||
|
- LangChain `ChatOpenAI` インスタンス生成(base_url に Groq エンドポイント指定)
|
||||||
|
- システムプロンプトによる翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GroqClient:
|
||||||
|
def __init__(root_path: str = None)
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getAuthKey() -> str | None
|
||||||
|
def setAuthKey(api_key: str) -> bool
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setAuthKey`: `_authentication_check` に成功した場合のみ内部保存
|
||||||
|
- `getModelList`: モデル列挙後フィルタリング適用しソート
|
||||||
|
- `setModel`: 取得済みリスト内のモデルのみ受理
|
||||||
|
- `updateClient`: `ChatOpenAI` を選択モデル・Groq base_url で再生成
|
||||||
|
- `translate`: システム + ユーザメッセージ構築→LLM呼び出し→レスポンス正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = GroqClient()
|
||||||
|
if client.setAuthKey("gsk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"):
|
||||||
|
models = client.getModelList()
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
result = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `openai.OpenAI`: モデル列挙 / 推論(Groq エンドポイント経由)
|
||||||
|
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- base_url は固定で `https://api.groq.com/openai/v1`
|
||||||
|
- ストリーミング無効 (streaming=False) 固定
|
||||||
|
- API Key 無設定時 `getModelList()` は空
|
||||||
|
- API Key は `gsk` で始まり40文字以上であることを検証
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- エラーメッセージ詳細は包括的に扱わない (上位層でロギング)
|
||||||
|
- 翻訳結果の構造が複雑 (list/dict) 場合を単純文字列へ normalize するのみ
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
- `details/translation_openai.md` (類似実装)
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
# translation_languages.py - 翻訳言語マッピング
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
翻訳エンジンが対応する言語コードのマッピングテーブルを提供するモジュールです。複数の翻訳エンジン(DeepL、Google、Bing、Papago等)の言語コード仕様の差異を吸収し、統一的な翻訳言語管理を実現します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 多エンジン対応
|
||||||
|
|
||||||
|
- DeepL(無料版・API版)
|
||||||
|
- Google Translate
|
||||||
|
- Microsoft Translator(Bing)
|
||||||
|
- Papago Translator
|
||||||
|
- その他のWeb翻訳サービス
|
||||||
|
|
||||||
|
### 言語コード統合管理
|
||||||
|
|
||||||
|
- 各エンジン固有の言語コード形式を統一
|
||||||
|
- 送信元(source)と送信先(target)言語の分離管理
|
||||||
|
- 地域固有言語バリエーションの対応
|
||||||
|
|
||||||
|
## データ構造
|
||||||
|
|
||||||
|
### translation_lang
|
||||||
|
|
||||||
|
```python
|
||||||
|
translation_lang: Dict[str, Dict[str, Dict[str, str]]] = {
|
||||||
|
"エンジン名": {
|
||||||
|
"source": {"言語名": "言語コード", ...},
|
||||||
|
"target": {"言語名": "言語コード", ...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepL翻訳エンジン(無料版)
|
||||||
|
|
||||||
|
```python
|
||||||
|
translation_lang["DeepL"] = {
|
||||||
|
"source": {
|
||||||
|
"Arabic": "ar", "Bulgarian": "bg", "Czech": "cs", "Danish": "da",
|
||||||
|
"German": "de", "Greek": "el", "English": "en", "Spanish": "es",
|
||||||
|
"Estonian": "et", "Finnish": "fi", "French": "fr", "Irish": "ga",
|
||||||
|
"Croatian": "hr", "Hungarian": "hu", "Indonesian": "id",
|
||||||
|
"Icelandic": "is", "Italian": "it", "Japanese": "ja",
|
||||||
|
"Korean": "ko", "Lithuanian": "lt", "Latvian": "lv",
|
||||||
|
"Maltese": "mt", "Bokmal": "nb", "Dutch": "nl",
|
||||||
|
"Norwegian": "no", "Polish": "pl", "Portuguese": "pt",
|
||||||
|
"Romanian": "ro", "Russian": "ru", "Slovak": "sk",
|
||||||
|
"Slovenian": "sl", "Swedish": "sv", "Turkish": "tr",
|
||||||
|
"Ukrainian": "uk", "Chinese Simplified": "zh",
|
||||||
|
"Chinese Traditional": "zh"
|
||||||
|
},
|
||||||
|
"target": {/* 同じマッピング */}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepL API(有料版 概要)
|
||||||
|
|
||||||
|
```python
|
||||||
|
translation_lang["DeepL_API"] = {
|
||||||
|
"source": {/* 基本的にDeepLと同様 */},
|
||||||
|
"target": {
|
||||||
|
"Japanese": "ja",
|
||||||
|
"English American": "en-US", # 地域別対応
|
||||||
|
"English British": "en-GB",
|
||||||
|
"Portuguese Brazilian": "pt-BR", # ブラジル・ポルトガル語
|
||||||
|
"Portuguese European": "pt-PT", # ヨーロッパ・ポルトガル語
|
||||||
|
"Chinese Simplified": "zh",
|
||||||
|
"Chinese Traditional": "zh"
|
||||||
|
/* その他の言語 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要対応言語
|
||||||
|
|
||||||
|
### 西欧言語
|
||||||
|
|
||||||
|
- **English**: 英語(米国・英国バリエーション)
|
||||||
|
- **German**: ドイツ語
|
||||||
|
- **French**: フランス語
|
||||||
|
- **Spanish**: スペイン語
|
||||||
|
- **Italian**: イタリア語
|
||||||
|
- **Portuguese**: ポルトガル語(ブラジル・欧州)
|
||||||
|
- **Dutch**: オランダ語
|
||||||
|
- **Swedish**: スウェーデン語
|
||||||
|
- **Norwegian**: ノルウェー語
|
||||||
|
|
||||||
|
### 東欧・スラブ言語
|
||||||
|
|
||||||
|
- **Russian**: ロシア語
|
||||||
|
- **Polish**: ポーランド語
|
||||||
|
- **Czech**: チェコ語
|
||||||
|
- **Slovak**: スロバキア語
|
||||||
|
- **Ukrainian**: ウクライナ語
|
||||||
|
- **Bulgarian**: ブルガリア語
|
||||||
|
- **Croatian**: クロアチア語
|
||||||
|
- **Slovenian**: スロベニア語
|
||||||
|
|
||||||
|
### アジア言語
|
||||||
|
|
||||||
|
- **Japanese**: 日本語
|
||||||
|
- **Korean**: 韓国語
|
||||||
|
- **Chinese Simplified**: 中国語(簡体字)
|
||||||
|
- **Chinese Traditional**: 中国語(繁体字)
|
||||||
|
- **Indonesian**: インドネシア語
|
||||||
|
|
||||||
|
### その他の言語
|
||||||
|
|
||||||
|
- **Arabic**: アラビア語
|
||||||
|
- **Turkish**: トルコ語
|
||||||
|
- **Finnish**: フィンランド語
|
||||||
|
- **Estonian**: エストニア語
|
||||||
|
- **Latvian**: ラトビア語
|
||||||
|
- **Lithuanian**: リトアニア語
|
||||||
|
- **Maltese**: マルタ語
|
||||||
|
- **Irish**: アイルランド語
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な言語コード取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.translation.translation_languages import translation_lang
|
||||||
|
|
||||||
|
# DeepLで日本語から英語への翻訳
|
||||||
|
deepl_source = translation_lang["DeepL"]["source"]["Japanese"] # "ja"
|
||||||
|
deepl_target = translation_lang["DeepL"]["target"]["English"] # "en"
|
||||||
|
|
||||||
|
# DeepL APIで地域固有の英語指定
|
||||||
|
deepl_api_target = translation_lang["DeepL_API"]["target"]["English American"] # "en-US"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 対応言語の確認
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_supported_languages(engine_name):
|
||||||
|
"""指定エンジンの対応言語一覧取得"""
|
||||||
|
if engine_name in translation_lang:
|
||||||
|
engine_data = translation_lang[engine_name]
|
||||||
|
source_langs = list(engine_data["source"].keys())
|
||||||
|
target_langs = list(engine_data["target"].keys())
|
||||||
|
return {
|
||||||
|
"source": source_langs,
|
||||||
|
"target": target_langs,
|
||||||
|
"common": list(set(source_langs) & set(target_langs))
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
deepl_langs = get_supported_languages("DeepL")
|
||||||
|
print(f"DeepL対応言語数: {len(deepl_langs['common'])}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 言語コード変換
|
||||||
|
|
||||||
|
```python
|
||||||
|
def convert_language_code(language_name, from_engine, to_engine, direction="source"):
|
||||||
|
"""エンジン間での言語コード変換"""
|
||||||
|
try:
|
||||||
|
# 元エンジンから言語名を確認
|
||||||
|
from_codes = translation_lang[from_engine][direction]
|
||||||
|
to_codes = translation_lang[to_engine][direction]
|
||||||
|
|
||||||
|
if language_name in from_codes and language_name in to_codes:
|
||||||
|
return to_codes[language_name]
|
||||||
|
return None
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用例:DeepLからGoogle Translateへの変換
|
||||||
|
google_code = convert_language_code("Japanese", "DeepL", "Google", "target")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 翻訳システムでの統合利用
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TranslationLanguageManager:
|
||||||
|
"""翻訳言語管理クラス"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_language_code(engine, language, direction="target"):
|
||||||
|
"""安全な言語コード取得"""
|
||||||
|
try:
|
||||||
|
return translation_lang[engine][direction][language]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_language_supported(engine, language, direction="target"):
|
||||||
|
"""言語サポート確認"""
|
||||||
|
try:
|
||||||
|
return language in translation_lang[engine][direction]
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_compatible_engines(source_lang, target_lang):
|
||||||
|
"""両言語をサポートするエンジン一覧"""
|
||||||
|
compatible = []
|
||||||
|
for engine in translation_lang:
|
||||||
|
source_supported = TranslationLanguageManager.is_language_supported(
|
||||||
|
engine, source_lang, "source"
|
||||||
|
)
|
||||||
|
target_supported = TranslationLanguageManager.is_language_supported(
|
||||||
|
engine, target_lang, "target"
|
||||||
|
)
|
||||||
|
if source_supported and target_supported:
|
||||||
|
compatible.append(engine)
|
||||||
|
return compatible
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
manager = TranslationLanguageManager()
|
||||||
|
|
||||||
|
# 日本語→英語をサポートするエンジン
|
||||||
|
engines = manager.get_compatible_engines("Japanese", "English")
|
||||||
|
print(f"対応エンジン: {engines}")
|
||||||
|
|
||||||
|
# 特定エンジンでの言語コード取得
|
||||||
|
ja_code = manager.get_language_code("DeepL", "Japanese", "source")
|
||||||
|
en_code = manager.get_language_code("DeepL", "English", "target")
|
||||||
|
```
|
||||||
|
|
||||||
|
## エンジン別特徴
|
||||||
|
|
||||||
|
### DeepL(無料版)
|
||||||
|
|
||||||
|
- **強み**: 高精度、自然な翻訳
|
||||||
|
- **制限**: 月間使用量制限、API制限
|
||||||
|
- **対応**: 26言語
|
||||||
|
|
||||||
|
### DeepL API(有料版)
|
||||||
|
|
||||||
|
- **強み**: DeepLの高精度、地域別言語対応
|
||||||
|
- **制限**: 従量課金
|
||||||
|
- **対応**: 地域固有言語バリエーション
|
||||||
|
|
||||||
|
### Google Translate
|
||||||
|
|
||||||
|
- **強み**: 多言語対応、高速
|
||||||
|
- **制限**: API制限、精度のばらつき
|
||||||
|
- **対応**: 100+言語
|
||||||
|
|
||||||
|
### Microsoft Translator
|
||||||
|
|
||||||
|
- **強み**: リアルタイム翻訳、音声対応
|
||||||
|
- **制限**: APIキー必要
|
||||||
|
- **対応**: 70+言語
|
||||||
|
|
||||||
|
## 地域バリエーション対応
|
||||||
|
|
||||||
|
### 英語の地域別対応
|
||||||
|
|
||||||
|
```python
|
||||||
|
# DeepL APIでの英語バリエーション
|
||||||
|
"English American": "en-US", # アメリカ英語
|
||||||
|
"English British": "en-GB", # イギリス英語
|
||||||
|
```
|
||||||
|
|
||||||
|
### ポルトガル語の地域別対応
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ブラジル・ポルトガル語とヨーロッパ・ポルトガル語
|
||||||
|
"Portuguese Brazilian": "pt-BR",
|
||||||
|
"Portuguese European": "pt-PT",
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中国語の文字体系対応
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 簡体字・繁体字の区別
|
||||||
|
"Chinese Simplified": "zh", # 簡体字(中国本土)
|
||||||
|
"Chinese Traditional": "zh", # 繁体字(台湾・香港)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 拡張性
|
||||||
|
|
||||||
|
### 新エンジンの追加
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 新しい翻訳エンジンの追加例
|
||||||
|
translation_lang["NewEngine"] = {
|
||||||
|
"source": {
|
||||||
|
"Japanese": "jp",
|
||||||
|
"English": "en",
|
||||||
|
"Korean": "kr"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"Japanese": "jp",
|
||||||
|
"English": "en",
|
||||||
|
"Korean": "kr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新言語の追加
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 既存エンジンへの新言語追加
|
||||||
|
translation_lang["DeepL"]["source"]["Hindi"] = "hi"
|
||||||
|
translation_lang["DeepL"]["target"]["Hindi"] = "hi"
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 安全な言語コード取得
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_get_language_code(engine, language, direction="target", fallback="en"):
|
||||||
|
"""フォールバック機能付き言語コード取得"""
|
||||||
|
try:
|
||||||
|
return translation_lang[engine][direction][language]
|
||||||
|
except KeyError:
|
||||||
|
# フォールバック言語を返す
|
||||||
|
try:
|
||||||
|
return translation_lang[engine][direction].get("English", fallback)
|
||||||
|
except KeyError:
|
||||||
|
return fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
### 言語サポート検証
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_translation_pair(engine, source_lang, target_lang):
|
||||||
|
"""翻訳ペアの有効性検証"""
|
||||||
|
try:
|
||||||
|
engine_data = translation_lang[engine]
|
||||||
|
source_supported = source_lang in engine_data["source"]
|
||||||
|
target_supported = target_lang in engine_data["target"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": source_supported and target_supported,
|
||||||
|
"source_supported": source_supported,
|
||||||
|
"target_supported": target_supported
|
||||||
|
}
|
||||||
|
except KeyError:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"source_supported": False,
|
||||||
|
"target_supported": False,
|
||||||
|
"error": f"Unknown engine: {engine}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- エンジンによって言語コード形式が異なる
|
||||||
|
- 地域バリエーションはエンジンにより対応状況が異なる
|
||||||
|
- 新しい言語追加時は全エンジンでの対応状況を確認
|
||||||
|
- API制限や課金体系はエンジンごとに異なる
|
||||||
|
- 一部の言語ペアは翻訳精度に差がある場合がある
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `translation_translator.py`: 翻訳エンジン本体
|
||||||
|
- `translation_utils.py`: 翻訳ユーティリティ
|
||||||
|
- `transcription_languages.py`: 音声認識言語マッピング
|
||||||
|
- `config.py`: 翻訳言語設定管理
|
||||||
|
- `controller.py`: 言語選択UI制御
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
### CTranslate2 言語構造変更
|
||||||
|
|
||||||
|
従来: 重みタイプがトップレベルキー (`translation_lang["m2m100_418M-ct2-int8"]`).
|
||||||
|
|
||||||
|
現在: `translation_lang["CTranslate2"][weight_type]["source"|"target"]` のネスト構造。`model.findTranslationEngines` / `translation_translator` で `engine == "CTranslate2"` の場合は `CTRANSLATE2_WEIGHT_TYPE` を用いて内部辞書へアクセス。
|
||||||
|
|
||||||
|
### 外部 YAML 言語マッピング導入
|
||||||
|
|
||||||
|
`models/translation/languages/languages.yml` を追加し、`config.init_config()` 内で `loadTranslationLanguages(path=config.PATH_LOCAL)` を呼び出し、既存 `translation_lang` にマージ/上書き。読込失敗時は空辞書を返しフォールバック。(PyYAML 追加)
|
||||||
|
|
||||||
|
### LMStudio / Ollama 翻訳モデル対応準備
|
||||||
|
|
||||||
|
新規ローカル LLM 接続用として LMStudio / Ollama 追加。現段階ではモデルリスト・選択用のエンドポイントとプロパティ (`SELECTABLE_LMSTUDIO_MODEL_LIST`, `SELECTED_LMSTUDIO_MODEL`, `SELECTABLE_OLLAMA_MODEL_LIST`, `SELECTED_OLLAMA_MODEL`) を定義。言語マッピングは今後 YAML 拡張で統合予定(未実装部分は翻訳本体の `translate()` に未統合)。
|
||||||
|
|
||||||
|
### モデル選択プロパティ名称統一
|
||||||
|
|
||||||
|
Plamo / Gemini / OpenAI の選択モデルプロパティを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` へ統一。保存キーも `SELECTED_*` に更新。
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| CTranslate2 | ネスト化により言語参照コード修正が必要 |
|
||||||
|
| YAML | 動的言語追加がコード編集無しに可能 |
|
||||||
|
| LLM接続 | 今後言語マッピングを YAML で拡張予定(未実装) |
|
||||||
|
| プロパティ | SELECTED_* へ統一で UI/設定整合性向上 |
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# translation_lmstudio.py - LMStudio ローカル LLM 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
LMStudio 互換 OpenAI API を利用したローカル LLM 翻訳クライアントラッパー。モデル一覧取得・モデル選択・翻訳処理を統一インターフェースで提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-12-30)
|
||||||
|
|
||||||
|
- 接続失敗時のエラーハンドリング改善
|
||||||
|
- URL への疎通確認失敗時にモデルリストをクリア (`SELECTABLE_LMSTUDIO_MODEL_LIST = []`)
|
||||||
|
- 選択モデルをクリア (`SELECTED_LMSTUDIO_MODEL = None`)
|
||||||
|
- `SELECTABLE_TRANSLATION_ENGINE_STATUS["LMStudio"]` を False に設定
|
||||||
|
- フロントエンドに通知して UI を同期
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
- 新規追加: ローカル LLM (LMStudio) を翻訳エンジン群へ統合
|
||||||
|
- `getModelList()` により現在起動中インスタンスから利用可能モデルを取得
|
||||||
|
- `setModel()` 成功時に `updateClient()` を呼ぶことで LangChain `ChatOpenAI` を再構築
|
||||||
|
- YAML (`prompt/translation_lmstudio.yml`) からシステムプロンプト (`system_prompt`) と対応言語をロード
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 拡張性 | ネットワーク不要のローカル推論を利用可能 |
|
||||||
|
| 一貫性 | 他 API クライアント (OpenAI/Plamo/Gemini/Ollama) と同一メソッド構成 |
|
||||||
|
| 保守性 | 翻訳ロジックを共通フォーマットへ集約 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- LMStudio エンドポイントへの疎通確認 (認証代替)
|
||||||
|
- 利用可能モデル一覧の収集とソート
|
||||||
|
- 選択モデルの検証と内部保持
|
||||||
|
- LangChain ラッパーインスタンス生成
|
||||||
|
- システムプロンプトによる指示付き翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LMStudioClient:
|
||||||
|
def __init__(base_url: str | None = None, root_path: str = None)
|
||||||
|
def getBaseURL() -> str | None
|
||||||
|
def setBaseURL(base_url: str | None) -> bool
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setBaseURL`: 疎通確認 (_authentication_check) に成功した場合のみ内部更新
|
||||||
|
- `getModelList`: `OpenAI` クライアントで `/models` を列挙し id を抽出
|
||||||
|
- `setModel`: `getModelList` 内のモデル名のみ受理
|
||||||
|
- `updateClient`: `ChatOpenAI` インスタンスを最新モデルで再生成
|
||||||
|
- `translate`: システム / ユーザメッセージで LLM へ問い合わせし文字列レスポンスを正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = LMStudioClient(base_url="http://localhost:1234/v1")
|
||||||
|
models = client.getModelList()
|
||||||
|
if models:
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
translated = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(translated)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `openai.OpenAI`: LMStudio OpenAI 互換 API 呼び出し
|
||||||
|
- `langchain_openai.ChatOpenAI`: LangChain 抽象化
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- `api_key` は固定文字列 "lmstudio" (LMStudio 側で不要のため) を利用
|
||||||
|
- モデル一覧取得はエンドポイントの互換性に依存 (古いバージョン非対応の可能性)
|
||||||
|
- `updateClient()` 呼び出し前は `translate()` を利用できない
|
||||||
|
- **接続失敗時の自動処理:**
|
||||||
|
- URL への疎通確認(接続テスト)が失敗すると、自動的にモデルリストと選択モデルがクリアされる
|
||||||
|
- `SELECTABLE_TRANSLATION_ENGINE_STATUS["LMStudio"]` が False に設定され、エンジンが使用不可状態になる
|
||||||
|
- Controller が自動的にフロントエンドに状態変化を通知
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- ストリーミング未対応 (streaming=False)
|
||||||
|
- エラーハンドリングは包括的ではなく詳細原因は上位層で処理必要
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# translation_ollama.py - Ollama ローカル LLM 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
Ollama サーバー上で稼働するローカル LLM を翻訳エンジンとして扱うためのクライアントラッパー。モデル一覧取得・モデル選択・翻訳実行を統一パターンで提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-12-30)
|
||||||
|
|
||||||
|
- 接続失敗時のエラーハンドリング改善
|
||||||
|
- `/api/ping` への疎通確認失敗時にモデルリストをクリア (`SELECTABLE_OLLAMA_MODEL_LIST = []`)
|
||||||
|
- 選択モデルをクリア (`SELECTED_OLLAMA_MODEL = None`)
|
||||||
|
- `SELECTABLE_TRANSLATION_ENGINE_STATUS["Ollama"]` を False に設定
|
||||||
|
- フロントエンドに通知して UI を同期
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
- 新規追加: Ollama を翻訳エンジン群へ統合
|
||||||
|
- `/api/ping` を用いた疎通確認による簡易認証
|
||||||
|
- `/api/tags` から利用可能モデル一覧抽出・ソート
|
||||||
|
- YAML (`prompt/translation_ollama.yml`) からシステムプロンプト (`system_prompt`) と対応言語をロード
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 拡張性 | LAN 内ローカル推論を利用可能 |
|
||||||
|
| 可搬性 | GPU/CPU 任意構成の Ollama 環境に適応 |
|
||||||
|
| 一貫性 | 他翻訳クライアント (OpenAI/Gemini/Plamo/LMStudio) と統一 API |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- Ollama インスタンスへの接続確認
|
||||||
|
- モデル一覧取得 (タグ列挙) とソート
|
||||||
|
- 選択モデルの検証と内部保持
|
||||||
|
- LangChain `ChatOllama` インスタンス生成
|
||||||
|
- システムプロンプトとユーザ入力を組み立てて翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OllamaClient:
|
||||||
|
def __init__(root_path: str = None)
|
||||||
|
def authenticationCheck() -> bool
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `authenticationCheck`: `/api/ping` が 200 を返すかで利用可否判定
|
||||||
|
- `getModelList`: 認証成功時のみ `/api/tags` 結果から name 抽出
|
||||||
|
- `setModel`: 取得済みモデル一覧内に存在する場合のみ設定
|
||||||
|
- `updateClient`: `ChatOllama` を最新モデルで再生成
|
||||||
|
- `translate`: system / user メッセージを LLM へ送信し結合結果を正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = OllamaClient()
|
||||||
|
if client.authenticationCheck():
|
||||||
|
models = client.getModelList()
|
||||||
|
if models:
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
translated = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(translated)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `requests`: Ping/タグ API 呼び出し
|
||||||
|
- `langchain_ollama.ChatOllama`: LangChain LLM ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- サーバー既定 URL: `http://localhost:11434`
|
||||||
|
- モデル一覧取得は起動しているローカルサーバー状態に依存
|
||||||
|
- `updateClient()` 呼び出し前は `translate()` を利用不可
|
||||||
|
- **接続失敗時の自動処理:**
|
||||||
|
- `/api/ping` への疎通確認が失敗すると、自動的にモデルリストと選択モデルがクリアされる
|
||||||
|
- `SELECTABLE_TRANSLATION_ENGINE_STATUS["Ollama"]` が False に設定され、エンジンが使用不可状態になる
|
||||||
|
- Controller が自動的にフロントエンドに状態変化を通知
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- ストリーミング未対応 (streaming=False)
|
||||||
|
- エラー詳細は包括的に扱わない (上位層でフォールバック)
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# translation_openai.py - OpenAI 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
OpenAI API (公式または互換エンドポイント) を用いた汎用 LLM 翻訳クライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
- 除外キーワード (`whisper`, `embedding`, `image`, `tts`, `audio`, `search`, `transcribe`, `diarize`, `vision`) を用いて翻訳非適合モデルをフィルタ
|
||||||
|
- Fine-tune モデル (`ft:`) は root が `gpt-` で始まる場合採用
|
||||||
|
- YAML (`prompt/translation_openai.yml`) からシステムプロンプト (`system_prompt`) をロードする構成へ統合
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 正確性 | 不適合モデル除外で翻訳品質安定 |
|
||||||
|
| 保守性 | フィルタリングロジック明示化で再利用容易 |
|
||||||
|
| 一貫性 | 他翻訳クライアントと API 形状統一 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- OpenAI API Key を用いた認証確認
|
||||||
|
- 利用可能モデルのフィルタリングとソート
|
||||||
|
- 選択モデルの検証と内部保持
|
||||||
|
- LangChain `ChatOpenAI` インスタンス生成
|
||||||
|
- システムプロンプトによる翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OpenAIClient:
|
||||||
|
def __init__(base_url: str | None = None, root_path: str = None)
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getAuthKey() -> str | None
|
||||||
|
def setAuthKey(api_key: str) -> bool
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setAuthKey`: `_authentication_check` に成功した場合のみ内部保存
|
||||||
|
- `getModelList`: モデル列挙後フィルタリング適用しソート
|
||||||
|
- `setModel`: 取得済みリスト内のモデルのみ受理
|
||||||
|
- `updateClient`: `ChatOpenAI` を選択モデルで再生成
|
||||||
|
- `translate`: システム + ユーザメッセージ構築→LLM呼び出し→レスポンス正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = OpenAIClient()
|
||||||
|
if client.setAuthKey("OPENAI_API_KEY"):
|
||||||
|
models = client.getModelList()
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
result = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `openai.OpenAI`: モデル列挙 / 推論
|
||||||
|
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- `base_url` が None の場合公式エンドポイント
|
||||||
|
- ストリーミング無効 (streaming=False) 固定
|
||||||
|
- API Key 無設定時 `getModelList()` は空
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- エラーメッセージ詳細は包括的に扱わない (上位層でロギング)
|
||||||
|
- 翻訳結果の構造が複雑 (list/dict) 場合を単純文字列へ normalize するのみ
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# translation_openrouter.py - OpenRouter 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
OpenRouter API を用いた統合 LLM 翻訳クライアントラッパー。OpenAI 互換エンドポイント (`https://openrouter.ai/api/v1`) を利用し、複数の LLM プロバイダーへの統一アクセスを提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-12-29)
|
||||||
|
|
||||||
|
- OpenRouter API 認証チェック方法を変更
|
||||||
|
- **以前:** `client.models.list()` を呼び出して認証確認
|
||||||
|
- **現在:** `https://openrouter.ai/api/v1/auth/key` エンドポイントに GET リクエスト送信して確認
|
||||||
|
- **理由:** より信頼性の高い専用認証エンドポイントを使用し、高速かつ確実に API キー有効性を検証
|
||||||
|
- 認証失敗時の sensitive data 処理
|
||||||
|
- API キー検証失敗時はレスポンス `data` フィールドに `None` を設定(API キーを露出させない)
|
||||||
|
- エラーメッセージのみを返却し、具体的なキー情報は隠蔽
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 柔軟性 | 複数 LLM プロバイダーを単一インターフェースで利用 |
|
||||||
|
| 互換性 | OpenAI 互換 API で既存実装との一貫性維持 |
|
||||||
|
| 拡張性 | 新規モデル追加時も API キー再設定不要 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- OpenRouter API Key (20文字以上) を用いた認証確認
|
||||||
|
- `https://openrouter.ai/api/v1/auth/key` エンドポイントへの HTTP GET リクエストで検証(タイムアウト10秒)
|
||||||
|
- ステータスコード 200 で有効と判定
|
||||||
|
- 利用可能モデルのフィルタリングとソート
|
||||||
|
- 選択モデルの検証と内部保持
|
||||||
|
- LangChain `ChatOpenAI` インスタンス生成(base_url に OpenRouter エンドポイント指定)
|
||||||
|
- システムプロンプトによる翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OpenRouterClient:
|
||||||
|
def __init__(root_path: str = None)
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getAuthKey() -> str | None
|
||||||
|
def setAuthKey(api_key: str) -> bool
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setAuthKey`: `_authentication_check` に成功した場合のみ内部保存
|
||||||
|
- `getModelList`: モデル列挙後フィルタリング適用しソート
|
||||||
|
- `setModel`: 取得済みリスト内のモデルのみ受理
|
||||||
|
- `updateClient`: `ChatOpenAI` を選択モデル・OpenRouter base_url で再生成
|
||||||
|
- `translate`: システム + ユーザメッセージ構築→LLM呼び出し→レスポンス正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = OpenRouterClient()
|
||||||
|
if client.setAuthKey("sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"):
|
||||||
|
models = client.getModelList()
|
||||||
|
# OpenRouter は多数のモデルを提供
|
||||||
|
client.setModel("anthropic/claude-3-sonnet")
|
||||||
|
client.updateClient()
|
||||||
|
result = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `openai.OpenAI`: モデル列挙 / 推論(OpenRouter エンドポイント経由)
|
||||||
|
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- base_url は固定で `https://openrouter.ai/api/v1`
|
||||||
|
- ストリーミング無効 (streaming=False) 固定
|
||||||
|
- API Key 無設定時 `getModelList()` は空
|
||||||
|
- API Key は20文字以上であることを検証
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- エラーメッセージ詳細は包括的に扱わない (上位層でロギング)
|
||||||
|
- 翻訳結果の構造が複雑 (list/dict) 場合を単純文字列へ normalize するのみ
|
||||||
|
- OpenRouter の料金体系はモデル毎に異なる(利用前に確認が必要)
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
- `details/translation_openai.md` (類似実装)
|
||||||
|
- `details/translation_groq.md` (類似実装)
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# translation_plamo.py - Plamo 翻訳クライアント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
Preferred Networks 提供の Plamo API を利用した翻訳向け LLM クライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を統一インターフェースで提供する。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20)
|
||||||
|
|
||||||
|
- 新規追加: Plamo クライアント統合
|
||||||
|
- プロンプト設定を YAML (`prompt/translation_plamo.yml`) からロード(システムプロンプト `system_prompt`)
|
||||||
|
- モデル一覧取得後ソートして再現性を確保
|
||||||
|
|
||||||
|
### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 拡張性 | 日本発 API への対応により選択肢拡大 |
|
||||||
|
| 保守性 | 他 LLM クライアントと同一構造でメンテ容易 |
|
||||||
|
| 一貫性 | メソッド命名/責務の統一化 |
|
||||||
|
|
||||||
|
## 責務
|
||||||
|
|
||||||
|
- API Key 認証確認
|
||||||
|
- 利用可能モデルの列挙とソート
|
||||||
|
- モデル選択の検証
|
||||||
|
- LangChain `ChatOpenAI` インスタンス生成
|
||||||
|
- システムプロンプトによる翻訳実行
|
||||||
|
|
||||||
|
## 公開API (メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PlamoClient:
|
||||||
|
def __init__(root_path: str = None)
|
||||||
|
def getModelList() -> list[str]
|
||||||
|
def getAuthKey() -> str | None
|
||||||
|
def setAuthKey(api_key: str) -> bool
|
||||||
|
def getModel() -> str | None
|
||||||
|
def setModel(model: str) -> bool
|
||||||
|
def updateClient() -> None
|
||||||
|
def translate(text: str, input_lang: str, output_lang: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### メソッド詳細
|
||||||
|
|
||||||
|
- `setAuthKey`: `_authentication_check` 成功時のみ内部保存
|
||||||
|
- `getModelList`: 認証済み状態でモデル列挙→ソート
|
||||||
|
- `setModel`: 列挙済みリスト内モデルのみ受理
|
||||||
|
- `updateClient`: `ChatOpenAI` を再構築
|
||||||
|
- `translate`: システム + ユーザメッセージで推論し応答正規化
|
||||||
|
|
||||||
|
## 使用例
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = PlamoClient()
|
||||||
|
if client.setAuthKey("PLAMO_API_KEY"):
|
||||||
|
models = client.getModelList()
|
||||||
|
if models:
|
||||||
|
client.setModel(models[0])
|
||||||
|
client.updateClient()
|
||||||
|
result = client.translate("こんにちは世界", "Japanese", "English")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `openai.OpenAI`: Plamo 互換 API 呼び出し
|
||||||
|
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
|
||||||
|
- `translation_languages.translation_lang`: 対応言語集合
|
||||||
|
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- BASE_URL 固定: `https://api.platform.preferredai.jp/v1`
|
||||||
|
- API Key 未設定時はモデル一覧取得不可
|
||||||
|
- ストリーミング無効 (streaming=False)
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
- 詳細エラーは包括的に扱わず (上位層でログ/フォールバック)
|
||||||
|
- 翻訳結果の構造が複雑な場合単純文字列へ normalize のみ
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
|
||||||
|
- `details/translation_translator.md`
|
||||||
|
- `details/translation_languages.md`
|
||||||
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# 翻訳プロンプトへの履歴注入(Chat/Mic/Speaker)
|
||||||
|
|
||||||
|
LLM は直前までの会話文脈を理解して翻訳品質を向上させられます。そのため、システムプロンプトに最近の履歴(Chat/Mic/Speaker)を内包する機能を追加しました。大量のログでトークン消費が増えないよう、YAML 設定で取り込み範囲と上限を管理できます。
|
||||||
|
|
||||||
|
## アーキテクチャ
|
||||||
|
|
||||||
|
### 履歴管理(Model)
|
||||||
|
|
||||||
|
**`model.py`** でChat/Mic/Speakerのメッセージ履歴を一元管理:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 履歴バッファ(最大50件)
|
||||||
|
self.translation_history: list[dict] = []
|
||||||
|
self.translation_history_max_items = 50
|
||||||
|
|
||||||
|
# 履歴追加(オリジナルメッセージのみ、翻訳結果は保存しない)
|
||||||
|
model.addTranslationHistory("chat", "こんにちは")
|
||||||
|
model.addTranslationHistory("mic", "今日はいい天気")
|
||||||
|
model.addTranslationHistory("speaker", "Hello!")
|
||||||
|
|
||||||
|
# 履歴取得
|
||||||
|
history = model.getTranslationHistory(max_items=10)
|
||||||
|
|
||||||
|
# 履歴クリア
|
||||||
|
model.clearTranslationHistory()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自動注入(Model → Translator → 各LLMクライアント)
|
||||||
|
|
||||||
|
- **`model.getTranslate()`** で履歴を取得し、`translator.translate(..., context_history=history)` に渡す。
|
||||||
|
- **`Translator.translate()`** 側でエンジンごとの分岐直前に `setContextHistory()` を呼び、履歴をプロンプト組み立てに反映する。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# model.getTranslate()
|
||||||
|
history = self.getTranslationHistory()
|
||||||
|
translation = self.translator.translate(
|
||||||
|
translator_name=translator_name,
|
||||||
|
weight_type=config.CTRANSLATE2_WEIGHT_TYPE,
|
||||||
|
source_language=source_language,
|
||||||
|
target_language=target_language,
|
||||||
|
target_country=target_country,
|
||||||
|
message=message,
|
||||||
|
context_history=history,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Translator.translate() の一例(OpenAI)
|
||||||
|
case "OpenAI_API":
|
||||||
|
if self.openai_client is None:
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
if context_history:
|
||||||
|
self.openai_client.setContextHistory(context_history)
|
||||||
|
result = self.openai_client.translate(message, input_lang=source_language, output_lang=target_language)
|
||||||
|
```
|
||||||
|
|
||||||
|
### メッセージ処理(Controller)
|
||||||
|
|
||||||
|
**`controller.py`** で各メッセージ処理完了後に履歴へ追加(翻訳の成否に関係なく、オリジナル文だけ保存):
|
||||||
|
|
||||||
|
- **Chat**: `chatMessage()` の末尾で `model.addTranslationHistory("chat", ...)`
|
||||||
|
- **Mic**: `micMessage()` の末尾で `model.addTranslationHistory("mic", ...)`
|
||||||
|
- **Speaker**: `speakerMessage()` の末尾で `model.addTranslationHistory("speaker", ...)`
|
||||||
|
|
||||||
|
## 設定ファイル(例: OpenAI)
|
||||||
|
|
||||||
|
`src-python/models/translation/translation_settings/prompt/translation_openai.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
system_prompt: |
|
||||||
|
You are a helpful translation assistant.
|
||||||
|
Supported languages:
|
||||||
|
{supported_languages}
|
||||||
|
|
||||||
|
Translate the user provided text from {input_lang} to {output_lang}.
|
||||||
|
Return ONLY the translated text. Do not add quotes or extra commentary.
|
||||||
|
history:
|
||||||
|
use_history: true # 履歴をプロンプトへ注入するか
|
||||||
|
sources: [chat, mic, speaker] # 取り込み対象の履歴種別
|
||||||
|
max_messages: 10 # 注入する履歴件数の上限(新しい順)
|
||||||
|
max_chars: 4000 # 履歴整形後の最大文字数(超過時は先頭を切り捨て)
|
||||||
|
header_template: |
|
||||||
|
Conversation context (recent {max_messages} messages):
|
||||||
|
{history}
|
||||||
|
item_template: "[{timestamp}][{source}] {text}"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `system_prompt`: 従来どおり、`{supported_languages}`/`{input_lang}`/`{output_lang}` が利用可能。
|
||||||
|
- `history.use_history`: 履歴注入を有効化します。
|
||||||
|
- `history.sources`: 取り込み対象ソース。`chat`/`mic`/`speaker` から選択。
|
||||||
|
- `history.max_messages`: 新しい順に N 件を取り込みます。
|
||||||
|
- `history.max_chars`: 整形後の履歴文字列の最大長。上限を超えた場合は先頭側を切り捨て(新しい文脈を優先)。
|
||||||
|
- `history.header_template`: 履歴ヘッダの整形テンプレート。`{max_messages}`/`{history}` が利用可能。
|
||||||
|
- `history.item_template`: 各履歴アイテムの整形テンプレート。`{timestamp}`(HH:MM形式)/`{source}`/`{text}` が利用可能。
|
||||||
|
|
||||||
|
## 実装(OpenAI クライアント)
|
||||||
|
|
||||||
|
`src-python/models/translation/translation_openai.py`
|
||||||
|
|
||||||
|
- `OpenAIClient.setContextHistory(history_items: list[dict])` を追加。
|
||||||
|
- `history_items` は以下のキーを含む辞書の配列:
|
||||||
|
- `source`: `"chat" | "mic" | "speaker"`
|
||||||
|
- `text`: 文字列
|
||||||
|
- `timestamp`: ISO形式の日時文字列(HH:MM形式にフォーマットされてプロンプトに挿入)
|
||||||
|
- `translate()` 呼び出し時、YAML の `history` 設定に基づき、指定履歴をシステムプロンプト末尾へ整形して注入します。
|
||||||
|
- 文字数上限は簡易的に `max_chars` で制御(トークンカウントは行わず、過剰消費抑制用の安全策)。
|
||||||
|
|
||||||
|
## 使い方
|
||||||
|
|
||||||
|
### 基本的な流れ
|
||||||
|
|
||||||
|
1. **メッセージ発生時に履歴追加**(controller.py で自動実行)
|
||||||
|
```python
|
||||||
|
# Chat送信時(オリジナルメッセージのみ保存)
|
||||||
|
model.addTranslationHistory("chat", user_message)
|
||||||
|
|
||||||
|
# Mic入力時(音声認識結果のみ保存)
|
||||||
|
model.addTranslationHistory("mic", transcribed_text)
|
||||||
|
|
||||||
|
# Speaker受信時(受信したオリジナルメッセージのみ保存)
|
||||||
|
model.addTranslationHistory("speaker", received_text)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **翻訳時に自動注入**(model.py で自動実行)
|
||||||
|
```python
|
||||||
|
# getTranslate() 内で自動的に履歴が各LLMクライアントへ注入される
|
||||||
|
translation = model.getTranslate(translator_name, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **設定の調整**(YAML編集)
|
||||||
|
```yaml
|
||||||
|
history:
|
||||||
|
use_history: true # 有効/無効
|
||||||
|
sources: [chat, mic] # chatとmicのみ使う場合
|
||||||
|
max_messages: 5 # 最新5件のみ
|
||||||
|
max_chars: 2000 # 2000文字まで
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手動で履歴を操作(必要な場合のみ)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 履歴をクリア
|
||||||
|
model.clearTranslationHistory()
|
||||||
|
|
||||||
|
# 履歴を取得
|
||||||
|
recent_history = model.getTranslationHistory(max_items=10)
|
||||||
|
|
||||||
|
# 手動で追加
|
||||||
|
model.addTranslationHistory("chat", "カスタムメッセージ")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 連携方法(開発者向け)
|
||||||
|
|
||||||
|
既存のcontroller/model統合により、**自動で動作**します:
|
||||||
|
|
||||||
|
1. ユーザーがChat入力 → `controller.chatMessage()` → メッセージ処理完了後に `model.addTranslationHistory()` 呼び出し(翻訳の成功/失敗に関係なく)
|
||||||
|
2. マイク音声 → `controller.micMessage()` → メッセージ処理完了後に `model.addTranslationHistory()` 呼び出し(翻訳の成功/失敗に関係なく)
|
||||||
|
3. スピーカー受信 → `controller.speakerMessage()` → メッセージ処理完了後に `model.addTranslationHistory()` 呼び出し(翻訳の成功/失敗に関係なく)
|
||||||
|
4. 翻訳実行 → `model.getTranslate()` → LLMクライアントへ履歴を自動注入 → `client.translate()` で履歴付きプロンプト生成
|
||||||
|
|
||||||
|
**重要**: 履歴にはオリジナルメッセージのみが保存されます。翻訳結果は履歴に含まれません。これによりトークン消費を抑え、文脈として必要な情報のみを提供します。
|
||||||
|
|
||||||
|
**追加実装は不要です。** YAML設定を変更するだけで履歴注入の有効/無効や範囲を制御できます。
|
||||||
|
|
||||||
|
## 連携方法
|
||||||
|
|
||||||
|
## 対応状況
|
||||||
|
|
||||||
|
✅ **全LLMクライアントに展開済み**
|
||||||
|
|
||||||
|
以下のすべてのクライアントで履歴注入機能が利用可能です:
|
||||||
|
- OpenAI (`translation_openai.py` / `translation_openai.yml`)
|
||||||
|
- Gemini (`translation_gemini.py` / `translation_gemini.yml`)
|
||||||
|
- Groq (`translation_groq.py` / `translation_groq.yml`)
|
||||||
|
- OpenRouter (`translation_openrouter.py` / `translation_openrouter.yml`)
|
||||||
|
- LMStudio (`translation_lmstudio.py` / `translation_lmstudio.yml`)
|
||||||
|
- Ollama (`translation_ollama.py` / `translation_ollama.yml`)
|
||||||
|
- Plamo (`translation_plamo.py` / `translation_plamo.yml`)
|
||||||
|
|
||||||
|
各クライアントで同一の設定形式とAPIインターフェースを使用します:
|
||||||
|
- `setContextHistory(history_items: list[dict])` メソッド
|
||||||
|
- YAML の `history` セクション
|
||||||
|
|
||||||
|
## 今後の拡張案
|
||||||
|
|
||||||
|
- 実トークン見積りに基づく切り詰め(tiktoken 等)
|
||||||
|
- 要約モデルを使った古い履歴の縮約
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
# translation_translator.py - 翻訳エンジン統合クラス
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
複数の翻訳エンジンを統合管理する高レベル翻訳インターフェースです。DeepL、Google、Bing、Papago、CTranslate2などの多様な翻訳サービスを統一的に扱い、エラー時の自動フォールバック機能と認証管理を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 多エンジン統合
|
||||||
|
- DeepL(無料版・API版)
|
||||||
|
- Google Translate(Webスクレイピング)
|
||||||
|
- Microsoft Translator(Bing)
|
||||||
|
- Papago Translator
|
||||||
|
- CTranslate2(ローカル翻訳)
|
||||||
|
- Plamo API(プリファードネットワークス LLM)
|
||||||
|
- Gemini API(Google LLM)
|
||||||
|
- OpenAI API(ChatGPT系)
|
||||||
|
- Groq API(高速 LLM 推論)
|
||||||
|
- OpenRouter API(統合 LLM プロバイダー)
|
||||||
|
- LMStudio(ローカル LLM)
|
||||||
|
- Ollama(ローカル LLM)
|
||||||
|
|
||||||
|
### 統一インターフェース
|
||||||
|
- エンジン依存を隠蔽した単一の翻訳メソッド
|
||||||
|
- 自動エラーハンドリング・フォールバック
|
||||||
|
- 認証情報の統合管理
|
||||||
|
|
||||||
|
### オフライン翻訳対応
|
||||||
|
- CTranslate2による完全オフライン翻訳
|
||||||
|
- 複数モデルサイズ(small/large)対応
|
||||||
|
- CUDA高速化サポート
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Translator クラス
|
||||||
|
```python
|
||||||
|
class Translator:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.deepl_client: Optional[DeepLClient] = None
|
||||||
|
self.ctranslate2_translator: Any = None
|
||||||
|
self.ctranslate2_tokenizer: Any = None
|
||||||
|
self.is_loaded_ctranslate2_model: bool = False
|
||||||
|
self.is_changed_translator_parameters: bool = False
|
||||||
|
self.is_enable_translators: bool = ENABLE_TRANSLATORS
|
||||||
|
```
|
||||||
|
|
||||||
|
翻訳機能の中核クラス
|
||||||
|
|
||||||
|
#### 属性
|
||||||
|
- **deepl_client**: DeepL APIクライアント
|
||||||
|
- **ctranslate2_translator**: ローカル翻訳モデル
|
||||||
|
- **ctranslate2_tokenizer**: CTranslate2トークナイザー
|
||||||
|
- **is_loaded_ctranslate2_model**: ローカルモデル読み込み状態
|
||||||
|
- **is_enable_translators**: Web翻訳サービス利用可能フラグ
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 翻訳実行
|
||||||
|
|
||||||
|
```python
|
||||||
|
translate(translator_name: str, source_language: str, target_language: str,
|
||||||
|
target_country: str, message: str) -> Any
|
||||||
|
```
|
||||||
|
|
||||||
|
統一翻訳インターフェース
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **translator_name**: 翻訳エンジン名("DeepL", "Google", "CTranslate2"等)
|
||||||
|
- **source_language**: 送信元言語
|
||||||
|
- **target_language**: 送信先言語
|
||||||
|
- **target_country**: 送信先国・地域
|
||||||
|
- **message**: 翻訳対象テキスト
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **str**: 翻訳結果(成功時)
|
||||||
|
- **False**: 翻訳失敗時
|
||||||
|
|
||||||
|
### DeepL認証管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
authenticationDeepLAuthKey(authkey: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
DeepL APIキーの認証と設定
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **authkey**: DeepL APIキー
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: 認証成功可否
|
||||||
|
|
||||||
|
### CTranslate2管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
changeCTranslate2Model(path: str, model_type: str, device: str = "cpu",
|
||||||
|
device_index: int = 0, compute_type: str = "auto") -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
ローカル翻訳モデルの読み込み・変更
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **path**: モデルファイルのベースパス
|
||||||
|
- **model_type**: モデルサイズ("small"/"large")
|
||||||
|
- **device**: 計算デバイス("cpu"/"cuda")
|
||||||
|
- **device_index**: デバイスインデックス
|
||||||
|
- **compute_type**: 計算精度タイプ
|
||||||
|
|
||||||
|
### 状態管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
isLoadedCTranslate2Model() -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
CTranslate2モデルの読み込み状態確認
|
||||||
|
|
||||||
|
```python
|
||||||
|
isChangedTranslatorParameters() -> bool
|
||||||
|
setChangedTranslatorParameters(is_changed: bool) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
翻訳設定変更フラグの管理
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な翻訳
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.translation.translation_translator import Translator
|
||||||
|
|
||||||
|
# 翻訳器の初期化
|
||||||
|
translator = Translator()
|
||||||
|
|
||||||
|
# Google翻訳の使用
|
||||||
|
result = translator.translate(
|
||||||
|
translator_name="Google",
|
||||||
|
source_language="Japanese",
|
||||||
|
target_language="English",
|
||||||
|
target_country="United States",
|
||||||
|
message="こんにちは、世界!"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result != False:
|
||||||
|
print(f"翻訳結果: {result}") # "Hello, world!"
|
||||||
|
else:
|
||||||
|
print("翻訳に失敗しました")
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepL API使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# DeepL APIキーの設定
|
||||||
|
api_key = "your-deepl-api-key"
|
||||||
|
auth_success = translator.authenticationDeepLAuthKey(api_key)
|
||||||
|
|
||||||
|
if auth_success:
|
||||||
|
print("DeepL API認証成功")
|
||||||
|
|
||||||
|
# DeepL APIで翻訳
|
||||||
|
result = translator.translate(
|
||||||
|
translator_name="DeepL_API",
|
||||||
|
source_language="English",
|
||||||
|
target_language="Japanese",
|
||||||
|
target_country="Japan",
|
||||||
|
message="Hello, world!"
|
||||||
|
)
|
||||||
|
print(f"DeepL翻訳: {result}")
|
||||||
|
else:
|
||||||
|
print("DeepL API認証失敗")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ローカル翻訳(CTranslate2)の使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ローカルモデルの読み込み
|
||||||
|
translator.changeCTranslate2Model(
|
||||||
|
path=".", # アプリケーションルート
|
||||||
|
model_type="small", # smallモデル使用
|
||||||
|
device="cuda", # GPU使用
|
||||||
|
device_index=0,
|
||||||
|
compute_type="float16" # 半精度で高速化
|
||||||
|
)
|
||||||
|
|
||||||
|
# モデル読み込み確認
|
||||||
|
if translator.isLoadedCTranslate2Model():
|
||||||
|
print("CTranslate2モデル読み込み完了")
|
||||||
|
|
||||||
|
# ローカル翻訳実行
|
||||||
|
result = translator.translate(
|
||||||
|
translator_name="CTranslate2",
|
||||||
|
source_language="Japanese",
|
||||||
|
target_language="English",
|
||||||
|
target_country="United States",
|
||||||
|
message="機械翻訳のテストです"
|
||||||
|
)
|
||||||
|
print(f"ローカル翻訳: {result}")
|
||||||
|
else:
|
||||||
|
print("CTranslate2モデル読み込み失敗")
|
||||||
|
```
|
||||||
|
|
||||||
|
### エラーハンドリング付きの翻訳
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_translate(translator, message, source_lang="Japanese", target_lang="English"):
|
||||||
|
"""安全な翻訳処理"""
|
||||||
|
# 翻訳エンジンの優先順位
|
||||||
|
engines = ["DeepL_API", "DeepL", "Google", "CTranslate2"]
|
||||||
|
|
||||||
|
for engine in engines:
|
||||||
|
try:
|
||||||
|
result = translator.translate(
|
||||||
|
translator_name=engine,
|
||||||
|
source_language=source_lang,
|
||||||
|
target_language=target_lang,
|
||||||
|
target_country="United States",
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
|
||||||
|
if result != False:
|
||||||
|
print(f"{engine}で翻訳成功: {result}")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
print(f"{engine}翻訳失敗、次のエンジンを試行")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{engine}でエラー: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("全ての翻訳エンジンで失敗")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
result = safe_translate(translator, "こんにちは")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 翻訳設定の管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 設定変更フラグの確認
|
||||||
|
if translator.isChangedTranslatorParameters():
|
||||||
|
print("翻訳設定が変更されています")
|
||||||
|
|
||||||
|
# 設定変更の適用(例:モデル再読み込み)
|
||||||
|
translator.changeCTranslate2Model(".", "small", "cpu")
|
||||||
|
|
||||||
|
# フラグのリセット
|
||||||
|
translator.setChangedTranslatorParameters(False)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 翻訳エンジン比較
|
||||||
|
|
||||||
|
### DeepL API(有料)
|
||||||
|
- **精度**: 最高レベル
|
||||||
|
- **速度**: 高速
|
||||||
|
- **制限**: API使用料、月間制限
|
||||||
|
- **対応**: 26言語、地域別対応
|
||||||
|
|
||||||
|
### DeepL(無料)
|
||||||
|
- **精度**: 高品質
|
||||||
|
- **速度**: 中程度
|
||||||
|
- **制限**: 月間使用量制限、文字数制限
|
||||||
|
- **対応**: 26言語
|
||||||
|
|
||||||
|
### Google Translate
|
||||||
|
- **精度**: 良好
|
||||||
|
- **速度**: 高速
|
||||||
|
- **制限**: アクセス頻度制限
|
||||||
|
- **対応**: 100+言語
|
||||||
|
|
||||||
|
### CTranslate2(ローカル)
|
||||||
|
- **精度**: 中〜高(モデル依存)
|
||||||
|
- **速度**: 高速(GPU使用時)
|
||||||
|
- **制限**: なし(オフライン)
|
||||||
|
- **対応**: 主要言語ペア
|
||||||
|
|
||||||
|
### その他(Bing, Papago等)
|
||||||
|
- **精度**: 中程度
|
||||||
|
- **速度**: 中程度
|
||||||
|
- **制限**: サービス依存
|
||||||
|
- **対応**: サービス固有
|
||||||
|
|
||||||
|
## CTranslate2詳細
|
||||||
|
|
||||||
|
### 対応モデル
|
||||||
|
```python
|
||||||
|
ctranslate2_weights = {
|
||||||
|
"small": {
|
||||||
|
"url": "m2m100_418m.zip",
|
||||||
|
"directory_name": "m2m100_418m",
|
||||||
|
"tokenizer": "facebook/m2m100_418M"
|
||||||
|
},
|
||||||
|
"large": {
|
||||||
|
"url": "m2m100_12b.zip",
|
||||||
|
"directory_name": "m2m100_12b",
|
||||||
|
"tokenizer": "facebook/m2m100_1.2b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### パフォーマンス特性
|
||||||
|
|
||||||
|
#### small モデル
|
||||||
|
- **サイズ**: ~400MB
|
||||||
|
- **メモリ**: ~1GB RAM
|
||||||
|
- **VRAM**: ~500MB(CUDA使用時)
|
||||||
|
- **速度**: 高速
|
||||||
|
- **精度**: 良好
|
||||||
|
|
||||||
|
#### large モデル
|
||||||
|
- **サイズ**: ~4.8GB
|
||||||
|
- **メモリ**: ~6GB RAM
|
||||||
|
- **VRAM**: ~3GB(CUDA使用時)
|
||||||
|
- **速度**: 中程度
|
||||||
|
- **精度**: 高品質
|
||||||
|
|
||||||
|
### 計算タイプ設定
|
||||||
|
```python
|
||||||
|
# CPU使用時
|
||||||
|
compute_type = "int8" # 速度重視
|
||||||
|
|
||||||
|
# CUDA使用時
|
||||||
|
compute_type = "float16" # バランス重視
|
||||||
|
compute_type = "int8_float16" # メモリ効率重視
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### ネットワークエラー
|
||||||
|
- 接続タイムアウト
|
||||||
|
- API制限超過
|
||||||
|
- サービス一時停止
|
||||||
|
|
||||||
|
### 認証エラー
|
||||||
|
- 無効なAPIキー
|
||||||
|
- 期限切れアカウント
|
||||||
|
- 使用量上限到達
|
||||||
|
|
||||||
|
### モデルエラー
|
||||||
|
- ファイル破損
|
||||||
|
- VRAM不足
|
||||||
|
- 非対応言語ペア
|
||||||
|
|
||||||
|
### 対応策
|
||||||
|
```python
|
||||||
|
def robust_translation(translator, message, source_lang, target_lang):
|
||||||
|
"""堅牢な翻訳処理"""
|
||||||
|
# オンライン翻訳を先に試行
|
||||||
|
online_engines = ["DeepL_API", "DeepL", "Google"]
|
||||||
|
|
||||||
|
for engine in online_engines:
|
||||||
|
try:
|
||||||
|
result = translator.translate(engine, source_lang, target_lang, "", message)
|
||||||
|
if result != False:
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{engine}エラー: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# オンライン翻訳が全て失敗した場合、ローカル翻訳にフォールバック
|
||||||
|
try:
|
||||||
|
if not translator.isLoadedCTranslate2Model():
|
||||||
|
translator.changeCTranslate2Model(".", "small", "cpu")
|
||||||
|
|
||||||
|
result = translator.translate("CTranslate2", source_lang, target_lang, "", message)
|
||||||
|
if result != False:
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ローカル翻訳エラー: {e}")
|
||||||
|
|
||||||
|
return "翻訳に失敗しました"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `translation_languages`: 言語コード管理
|
||||||
|
- `translation_utils`: CTranslate2ユーティリティ
|
||||||
|
- `utils`: エラーログ、計算デバイス管理
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `deepl`: DeepL APIライブラリ
|
||||||
|
- `translators`: Web翻訳サービスライブラリ
|
||||||
|
- `ctranslate2`: ローカル翻訳エンジン
|
||||||
|
- `transformers`: トークナイザー
|
||||||
|
|
||||||
|
## 設定要件
|
||||||
|
|
||||||
|
### 環境変数
|
||||||
|
- `DEEPL_AUTH_KEY`: DeepL APIキー(オプション)
|
||||||
|
|
||||||
|
### ファイル配置
|
||||||
|
```
|
||||||
|
root/
|
||||||
|
└── weights/
|
||||||
|
└── ctranslate2/
|
||||||
|
├── m2m100_418m/ # smallモデル
|
||||||
|
└── m2m100_12b/ # largeモデル
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- Web翻訳サービスは利用制限に注意
|
||||||
|
- CTranslate2の初回読み込みは時間がかかる
|
||||||
|
- GPU使用時はVRAM消費量に注意
|
||||||
|
- API認証情報の適切な管理が必要
|
||||||
|
- 長文翻訳時は分割処理を推奨
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `translation_languages.py`: 言語コードマッピング
|
||||||
|
- `translation_utils.py`: CTranslate2ユーティリティ
|
||||||
|
- `config.py`: 翻訳設定管理
|
||||||
|
- `model.py`: 翻訳機能統合
|
||||||
|
- `controller.py`: 翻訳制御インターフェース
|
||||||
|
|
||||||
|
## 最近の更新
|
||||||
|
|
||||||
|
### 2025-12-10: Groq API および OpenRouter API サポート追加
|
||||||
|
|
||||||
|
#### Groq API 統合
|
||||||
|
- Groq API (`https://api.groq.com/openai/v1`) を翻訳エンジンとして追加
|
||||||
|
- OpenAI 互換エンドポイントで高速 LLM 推論を実現
|
||||||
|
- API キーバリデーション(`gsk` で始まり40文字以上)
|
||||||
|
- モデルリスト自動取得とフィルタリング(テキスト処理モデルのみ)
|
||||||
|
- 認証成功時に `SELECTABLE_GROQ_MODEL_LIST` を更新、未選択時は先頭モデルを自動選択
|
||||||
|
- API キー無効時にモデルリストと選択モデルをクリアしフロントエンドに通知
|
||||||
|
|
||||||
|
#### OpenRouter API 統合
|
||||||
|
- OpenRouter API (`https://openrouter.ai/api/v1`) を翻訳エンジンとして追加
|
||||||
|
- 単一 API キーで複数 LLM プロバイダーへアクセス可能
|
||||||
|
- API キーバリデーション(20文字以上)
|
||||||
|
- モデルリスト自動取得とフィルタリング(テキスト処理モデルのみ)
|
||||||
|
- 認証成功時に `SELECTABLE_OPENROUTER_MODEL_LIST` を更新、未選択時は先頭モデルを自動選択
|
||||||
|
- API キー無効時にモデルリストと選択モデルをクリアしフロントエンドに通知
|
||||||
|
|
||||||
|
#### updateTranslationEngineAndEngineList() の拡張
|
||||||
|
- `SELECTABLE_TRANSLATION_ENGINE_STATUS` で Groq_API と OpenRouter_API の状態を管理
|
||||||
|
- 各エンジンの認証キー更新時に自動的にモデルリストを再取得
|
||||||
|
- フロントエンドへの通知を `run_mapping` 経由で送信
|
||||||
|
|
||||||
|
#### 影響
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| Groq API | 高速 LLM 推論による翻訳速度向上 |
|
||||||
|
| OpenRouter API | 単一キーで複数 LLM プロバイダーへアクセス |
|
||||||
|
| モデル管理 | API キー検証失敗時の自動クリアで一貫性向上 |
|
||||||
|
| UX改善 | 認証後の自動モデル選択で初期設定簡略化 |
|
||||||
|
|
||||||
|
### 2025-10-20: LLM エンジン拡張と最適化
|
||||||
|
|
||||||
|
#### 新規ローカル LLM エンジン追加
|
||||||
|
|
||||||
|
LMStudio / Ollama を翻訳エンジンとして追加。接続確認後にモデルリスト (`SELECTABLE_LMSTUDIO_MODEL_LIST` / `SELECTABLE_OLLAMA_MODEL_LIST`) を取得し、未選択なら先頭モデルを自動選択 (`SELECTED_LMSTUDIO_MODEL` / `SELECTED_OLLAMA_MODEL`)。現時点では CTranslate2 と同様にローカル動作を想定し、翻訳関数側は将来の統合(温度等パラメータ)に備えて抽象化維持。
|
||||||
|
|
||||||
|
#### モデル選択プロパティ名称統一
|
||||||
|
|
||||||
|
Plamo / Gemini / OpenAI の選択モデルプロパティを `SELECTED_*` 形式へ変更。旧名称 (`PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL`) は利用停止。自動認証後のモデルリスト更新ロジックで未選択時に先頭補完を行う。
|
||||||
|
|
||||||
|
#### OpenAI / Gemini / Plamo 認証後のモデルリスト自動更新
|
||||||
|
|
||||||
|
Auth設定メソッド完了時に `SELECTABLE_*_MODEL_LIST` を再取得し不足時は UI へ push。OpenAI はキー設定直後に最新モデルリストを反映し高速化。Gemini / Plamo も同様に `updateTranslator*Client()` 呼び出しでクライアント再生成。
|
||||||
|
|
||||||
|
#### CTranslate2 言語ネスト化対応
|
||||||
|
|
||||||
|
`translation_lang["CTranslate2"][weight_type]["source"|"target"]` へ構造変更。`CTRANSLATE2_WEIGHT_TYPE` により重みタイプ別の言語集合を参照。Translator 内では `translator_name == "CTranslate2"` の分岐で weight_type を参照して言語判定を行う実装に変更。
|
||||||
|
|
||||||
|
#### YAML 言語マッピング導入
|
||||||
|
|
||||||
|
外部ファイル `languages.yml` を読み込んで翻訳エンジン別対応言語を動的拡張。新言語追加は YAML 編集のみで実現(コード再デプロイ不要)。読み込み失敗時は空辞書でフォールバックし既存ハードコードを保持。
|
||||||
|
|
||||||
|
#### VRAM エラー検知とフォールバック
|
||||||
|
|
||||||
|
DeepL / Plamo / Gemini / OpenAI 実行時の VRAM 不足検知で自動的に CTranslate2 へ切替し翻訳を停止 (`ENABLE_TRANSLATION=False`)。ユーザー通知後は再度有効化要求時に再初期化を試行。安定性向上のためログへ VRAM エラー詳細を記録。
|
||||||
|
|
||||||
|
#### トークナイザーパス修正
|
||||||
|
|
||||||
|
CTranslate2 トークナイザーのダウンロード処理で保存ディレクトリ作成とパス使用順序不整合を修正。これにより初回起動時の失敗率低下。
|
||||||
|
|
||||||
|
#### 全言語ペア包括テスト導入
|
||||||
|
|
||||||
|
`backend_test.py` にて `test_translate_all_language_pairs()` を追加。複数エンジン・全言語ペアを列挙実行し `translation_test_results.json` を生成。失敗ペアの早期検出と YAML 追加言語検証に活用。
|
||||||
|
|
||||||
|
#### 影響
|
||||||
|
|
||||||
|
| 項目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| ローカルLLM | オフライン翻訳候補拡充 (LMStudio/Ollama) |
|
||||||
|
| プロパティ統一 | SELECTED_* 命名で一貫性と保守性向上 |
|
||||||
|
| CTranslate2構造 | 重みタイプ毎に最適言語集合参照可能 |
|
||||||
|
| YAML外部化 | 言語追加/削除が設定ファイル編集のみで完結 |
|
||||||
|
| VRAM検知 | エラー時自動停止 + 軽量エンジン切替で安定性向上 |
|
||||||
|
| Tokenizer修正 | 初回セットアップ失敗減少 |
|
||||||
|
| 包括テスト | 言語組合せの網羅的品質担保 |
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# translation_utils.py - CTranslate2モデル管理ユーティリティ
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
CTranslate2によるローカル機械翻訳モデルの自動ダウンロード、展開、管理を行うユーティリティモジュールです。複数のモデルサイズ(small/large)とプラットフォーム(CPU/CUDA)に対応し、モデルファイルの完全性チェックと自動修復機能を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### モデル自動管理
|
||||||
|
- CTranslate2モデルの自動ダウンロード
|
||||||
|
- ZIP形式モデルの展開・配置
|
||||||
|
- モデルファイルの完全性検証
|
||||||
|
- 破損モデルの自動再取得
|
||||||
|
|
||||||
|
### マルチプラットフォーム対応
|
||||||
|
- CPU版・CUDA版の両対応
|
||||||
|
- 複数モデルサイズの管理
|
||||||
|
- プラットフォーム別最適化
|
||||||
|
|
||||||
|
## 定数・設定
|
||||||
|
|
||||||
|
### モデル定義
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CTranslate2重みファイル情報
|
||||||
|
ctranslate2_weights = {
|
||||||
|
"small": {
|
||||||
|
"url": "m2m100_418m.zip",
|
||||||
|
"directory_name": "m2m100_418m",
|
||||||
|
"tokenizer": "facebook/m2m100_418M"
|
||||||
|
},
|
||||||
|
"large": {
|
||||||
|
"url": "m2m100_12b.zip",
|
||||||
|
"directory_name": "m2m100_12b",
|
||||||
|
"tokenizer": "facebook/m2m100_1.2b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 設定パラメータ
|
||||||
|
- **BASE_WEIGHTS_URL**: モデル配布ベースURL
|
||||||
|
- **LOCAL_WEIGHTS_DIR**: ローカル保存ディレクトリ
|
||||||
|
- **CHUNK_SIZE**: ダウンロード時のチャンクサイズ
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### モデルダウンロード
|
||||||
|
|
||||||
|
```python
|
||||||
|
def downloadCTranslate2Model(model_type: str, device: str = "cpu") -> bool:
|
||||||
|
"""CTranslate2モデルの自動ダウンロード"""
|
||||||
|
```
|
||||||
|
|
||||||
|
指定されたモデルタイプとデバイス用のモデルをダウンロード
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **model_type**: モデルサイズ("small"/"large")
|
||||||
|
- **device**: 計算デバイス("cpu"/"cuda")
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: ダウンロード成功可否
|
||||||
|
|
||||||
|
### モデル存在確認
|
||||||
|
|
||||||
|
```python
|
||||||
|
def checkCTranslate2ModelExists(model_type: str, device: str = "cpu") -> bool:
|
||||||
|
"""モデルファイルの存在確認"""
|
||||||
|
```
|
||||||
|
|
||||||
|
指定されたモデルがローカルに存在するかチェック
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **model_type**: 確認対象モデルタイプ
|
||||||
|
- **device**: 対象デバイス
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: モデル存在可否
|
||||||
|
|
||||||
|
### モデル完全性検証
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validateCTranslate2Model(model_type: str, device: str = "cpu") -> bool:
|
||||||
|
"""モデルファイルの完全性検証"""
|
||||||
|
```
|
||||||
|
|
||||||
|
ダウンロード済みモデルの整合性を確認
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **model_type**: 検証対象モデル
|
||||||
|
- **device**: 対象デバイス
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: モデル正常性
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的なモデル管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.translation.translation_utils import *
|
||||||
|
|
||||||
|
# smallモデル(CPU版)のダウンロード確認
|
||||||
|
if not checkCTranslate2ModelExists("small", "cpu"):
|
||||||
|
print("smallモデルが見つかりません。ダウンロード中...")
|
||||||
|
success = downloadCTranslate2Model("small", "cpu")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("ダウンロード完了")
|
||||||
|
else:
|
||||||
|
print("ダウンロード失敗")
|
||||||
|
else:
|
||||||
|
print("smallモデルは既に存在します")
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPU用モデルの準備
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CUDA版largeモデルのセットアップ
|
||||||
|
model_type = "large"
|
||||||
|
device = "cuda"
|
||||||
|
|
||||||
|
# 既存モデルの確認
|
||||||
|
if checkCTranslate2ModelExists(model_type, device):
|
||||||
|
# モデルの完全性検証
|
||||||
|
if validateCTranslate2Model(model_type, device):
|
||||||
|
print(f"{model_type}モデル({device}版)準備完了")
|
||||||
|
else:
|
||||||
|
print("モデルが破損しています。再ダウンロード中...")
|
||||||
|
# 破損モデルの再取得
|
||||||
|
downloadCTranslate2Model(model_type, device)
|
||||||
|
else:
|
||||||
|
# 新規ダウンロード
|
||||||
|
print(f"{model_type}モデル({device}版)をダウンロード中...")
|
||||||
|
downloadCTranslate2Model(model_type, device)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自動モデル管理システム
|
||||||
|
|
||||||
|
```python
|
||||||
|
def ensureModelReady(model_type="small", device="cpu", max_retries=3):
|
||||||
|
"""モデルの準備を保証する関数"""
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
print(f"モデル準備 試行 {attempt + 1}/{max_retries}")
|
||||||
|
|
||||||
|
# モデル存在確認
|
||||||
|
if not checkCTranslate2ModelExists(model_type, device):
|
||||||
|
print("モデルが見つかりません。ダウンロード中...")
|
||||||
|
if not downloadCTranslate2Model(model_type, device):
|
||||||
|
print(f"ダウンロード失敗(試行 {attempt + 1})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# モデル完全性確認
|
||||||
|
if validateCTranslate2Model(model_type, device):
|
||||||
|
print("モデル準備完了")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("モデルが破損しています。再取得中...")
|
||||||
|
# 破損ファイルの削除(実装依存)
|
||||||
|
# remove_corrupted_model(model_type, device)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("モデル準備に失敗しました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
if ensureModelReady("small", "cpu"):
|
||||||
|
print("翻訳システム初期化可能")
|
||||||
|
else:
|
||||||
|
print("翻訳システム初期化失敗")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 複数モデルの一括管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setupAllModels():
|
||||||
|
"""全モデルの一括セットアップ"""
|
||||||
|
|
||||||
|
models = [
|
||||||
|
("small", "cpu"),
|
||||||
|
("small", "cuda"),
|
||||||
|
("large", "cpu"),
|
||||||
|
("large", "cuda")
|
||||||
|
]
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for model_type, device in models:
|
||||||
|
print(f"\n=== {model_type}モデル({device}版)セットアップ ===")
|
||||||
|
|
||||||
|
# デバイス利用可能性チェック(CUDA版の場合)
|
||||||
|
if device == "cuda" and not torch.cuda.is_available():
|
||||||
|
print("CUDA環境が利用できません。スキップします。")
|
||||||
|
results[(model_type, device)] = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# モデル準備
|
||||||
|
success = ensureModelReady(model_type, device)
|
||||||
|
results[(model_type, device)] = success
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✓ {model_type}({device}版)準備完了")
|
||||||
|
else:
|
||||||
|
print(f"✗ {model_type}({device}版)準備失敗")
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
print("\n=== セットアップ結果 ===")
|
||||||
|
for (model_type, device), success in results.items():
|
||||||
|
status = "成功" if success else "失敗"
|
||||||
|
print(f"{model_type}({device}版): {status}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 全モデルセットアップの実行
|
||||||
|
setupAllModels()
|
||||||
|
```
|
||||||
|
|
||||||
|
## モデル仕様
|
||||||
|
|
||||||
|
### smallモデル(m2m100_418m)
|
||||||
|
|
||||||
|
```python
|
||||||
|
model_info = {
|
||||||
|
"name": "m2m100_418m",
|
||||||
|
"size": "~400MB",
|
||||||
|
"parameters": "418M",
|
||||||
|
"languages": "100言語",
|
||||||
|
"tokenizer": "facebook/m2m100_418M",
|
||||||
|
"memory_requirements": {
|
||||||
|
"cpu": "~1GB RAM",
|
||||||
|
"cuda": "~500MB VRAM"
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"speed": "高速",
|
||||||
|
"quality": "良好"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 特徴
|
||||||
|
- 高速処理に適している
|
||||||
|
- メモリ使用量が少ない
|
||||||
|
- リアルタイム翻訳に最適
|
||||||
|
- 100言語ペア対応
|
||||||
|
|
||||||
|
### largeモデル(m2m100_12b)
|
||||||
|
|
||||||
|
```python
|
||||||
|
model_info = {
|
||||||
|
"name": "m2m100_12b",
|
||||||
|
"size": "~4.8GB",
|
||||||
|
"parameters": "1.2B",
|
||||||
|
"languages": "100言語",
|
||||||
|
"tokenizer": "facebook/m2m100_1.2b",
|
||||||
|
"memory_requirements": {
|
||||||
|
"cpu": "~6GB RAM",
|
||||||
|
"cuda": "~3GB VRAM"
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"speed": "中程度",
|
||||||
|
"quality": "高品質"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 特徴
|
||||||
|
- 高品質翻訳が可能
|
||||||
|
- 大容量メモリが必要
|
||||||
|
- バッチ処理に適している
|
||||||
|
- 複雑な文章に対応
|
||||||
|
|
||||||
|
## ファイル構造
|
||||||
|
|
||||||
|
### ディレクトリレイアウト
|
||||||
|
```
|
||||||
|
weights/
|
||||||
|
└── ctranslate2/
|
||||||
|
├── m2m100_418m/ # smallモデル(CPU版)
|
||||||
|
│ ├── model.bin
|
||||||
|
│ ├── vocabulary.txt
|
||||||
|
│ ├── config.json
|
||||||
|
│ └── shared_vocabulary.txt
|
||||||
|
├── m2m100_418m_cuda/ # smallモデル(CUDA版)
|
||||||
|
│ └── [同様のファイル構成]
|
||||||
|
├── m2m100_12b/ # largeモデル(CPU版)
|
||||||
|
│ └── [同様のファイル構成]
|
||||||
|
└── m2m100_12b_cuda/ # largeモデル(CUDA版)
|
||||||
|
└── [同様のファイル構成]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 必須ファイル
|
||||||
|
- `model.bin`: 変換済みモデルウェイト
|
||||||
|
- `vocabulary.txt`: 語彙ファイル
|
||||||
|
- `config.json`: モデル設定ファイル
|
||||||
|
- `shared_vocabulary.txt`: 共有語彙ファイル
|
||||||
|
|
||||||
|
## ダウンロード処理
|
||||||
|
|
||||||
|
### ネットワーク処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def downloadWithProgress(url: str, destination: str) -> bool:
|
||||||
|
"""進捗表示付きダウンロード"""
|
||||||
|
try:
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
|
|
||||||
|
with open(destination, 'wb') as file:
|
||||||
|
downloaded = 0
|
||||||
|
for chunk in response.iter_content(chunk_size=CHUNK_SIZE):
|
||||||
|
if chunk:
|
||||||
|
file.write(chunk)
|
||||||
|
downloaded += len(chunk)
|
||||||
|
|
||||||
|
# 進捗表示
|
||||||
|
if total_size > 0:
|
||||||
|
progress = (downloaded / total_size) * 100
|
||||||
|
print(f"\rダウンロード進捗: {progress:.1f}%", end="")
|
||||||
|
|
||||||
|
print(f"\nダウンロード完了: {destination}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nダウンロードエラー: {e}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### 展開処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def extractZipModel(zip_path: str, extract_to: str) -> bool:
|
||||||
|
"""ZIPファイルの展開"""
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
# 展開先ディレクトリの作成
|
||||||
|
os.makedirs(extract_to, exist_ok=True)
|
||||||
|
|
||||||
|
# ファイル展開
|
||||||
|
zip_ref.extractall(extract_to)
|
||||||
|
|
||||||
|
print(f"展開完了: {extract_to}")
|
||||||
|
|
||||||
|
# 元のZIPファイルを削除(オプション)
|
||||||
|
os.remove(zip_path)
|
||||||
|
print(f"一時ファイル削除: {zip_path}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"展開エラー: {e}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### ネットワークエラー
|
||||||
|
- 接続タイムアウト
|
||||||
|
- ダウンロード中断
|
||||||
|
- サーバーエラー
|
||||||
|
|
||||||
|
### ファイルシステムエラー
|
||||||
|
- 容量不足
|
||||||
|
- 権限エラー
|
||||||
|
- ファイル破損
|
||||||
|
|
||||||
|
### リトライ機構
|
||||||
|
|
||||||
|
```python
|
||||||
|
def downloadWithRetry(url: str, destination: str, max_retries: int = 3) -> bool:
|
||||||
|
"""リトライ付きダウンロード"""
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
print(f"ダウンロード試行 {attempt + 1}/{max_retries}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if downloadWithProgress(url, destination):
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"試行 {attempt + 1} 失敗: {e}")
|
||||||
|
|
||||||
|
# 一時ファイルの清理
|
||||||
|
if os.path.exists(destination):
|
||||||
|
os.remove(destination)
|
||||||
|
|
||||||
|
# 最後の試行でない場合は少し待機
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(2 ** attempt) # 指数バックオフ
|
||||||
|
|
||||||
|
print("全ての試行が失敗しました")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス最適化
|
||||||
|
|
||||||
|
### ダウンロード最適化
|
||||||
|
- チャンク単位での分割ダウンロード
|
||||||
|
- 進捗表示による体験向上
|
||||||
|
- 自動リトライによる信頼性確保
|
||||||
|
|
||||||
|
### ストレージ最適化
|
||||||
|
- 一時ファイルの自動削除
|
||||||
|
- 重複ファイルの検出・排除
|
||||||
|
- 容量効率的なファイル管理
|
||||||
|
|
||||||
|
### メモリ最適化
|
||||||
|
- ストリーミングダウンロード
|
||||||
|
- 大容量ファイル対応
|
||||||
|
- メモリ使用量の制御
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `requests`: HTTPダウンロード
|
||||||
|
- `zipfile`: アーカイブ展開
|
||||||
|
- `os`: ファイルシステム操作
|
||||||
|
- `pathlib`: パス操作
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `tqdm`: 進捗バー表示(実装による)
|
||||||
|
- `hashlib`: ファイル整合性検証(実装による)
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 初回ダウンロードは時間がかかる(モデルサイズ依存)
|
||||||
|
- 十分なストレージ容量を確保
|
||||||
|
- ネットワーク環境によってダウンロード速度が変動
|
||||||
|
- CUDA版は対応GPU環境が必要
|
||||||
|
- モデルファイルのバックアップ推奨
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `translation_translator.py`: モデル利用クラス
|
||||||
|
- `translation_languages.py`: 言語コード管理
|
||||||
|
- `config.py`: 設定管理
|
||||||
|
- `utils.py`: 共通ユーティリティ
|
||||||
|
- `device_manager.py`: デバイス管理
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
# transliteration_context_rules.py - 文脈的転写ルールエンジン
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
トークン化された結果に対して文脈依存の転写ルールを適用するコンパクトなルールエンジンです。隣接するトークンの情報に基づいて読み(かな)を動的に修正し、より自然で正確な転写を実現します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 文脈依存転写
|
||||||
|
- 隣接トークン情報を利用した読み修正
|
||||||
|
- 優先度ベースのルール適用順序
|
||||||
|
- 正規表現・完全一致の両方に対応
|
||||||
|
|
||||||
|
### ルールエンジン
|
||||||
|
- 埋め込み型ルール定義(外部JSONファイル不要)
|
||||||
|
- 前方・後方の隣接トークン検査対応
|
||||||
|
- インプレース変更による効率的処理
|
||||||
|
|
||||||
|
### 動的読み変更
|
||||||
|
- 文脈に応じたかな読みの書き換え
|
||||||
|
- ひらがな・ヘボン式の自動クリア
|
||||||
|
- 呼び出し元での再計算トリガー
|
||||||
|
|
||||||
|
## ルール定義構造
|
||||||
|
|
||||||
|
### DEFAULT_RULES
|
||||||
|
|
||||||
|
```python
|
||||||
|
DEFAULT_RULES = {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "nan_next_tdna", # ルール名
|
||||||
|
"target": "何", # 対象文字
|
||||||
|
"match_mode": "equals", # マッチモード
|
||||||
|
"direction": "next", # 検査方向
|
||||||
|
"kana_set": ["タ", "チ", "ツ"...], # 条件文字セット
|
||||||
|
"on_true": {"kana": "ナン"}, # 条件真時のアクション
|
||||||
|
"on_false": {"kana": "ナニ"} # 条件偽時のアクション
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ルール要素
|
||||||
|
|
||||||
|
#### 基本設定
|
||||||
|
- **name**: ルールの識別名
|
||||||
|
- **target**: 適用対象となる文字・文字列
|
||||||
|
- **priority**: 適用優先度(高い順に処理)
|
||||||
|
- **match_mode**: マッチングモード("equals"/"regex")
|
||||||
|
|
||||||
|
#### 条件設定
|
||||||
|
- **direction**: 隣接トークン検査方向("next"/"prev")
|
||||||
|
- **kana_set**: 条件判定用の文字セット
|
||||||
|
- **pattern**: 正規表現パターン(regex時)
|
||||||
|
|
||||||
|
#### アクション設定
|
||||||
|
- **on_true**: 条件成立時のアクション
|
||||||
|
- **on_false**: 条件不成立時のアクション
|
||||||
|
- **kana**: 設定する新しいかな読み
|
||||||
|
|
||||||
|
## 主要関数
|
||||||
|
|
||||||
|
### apply_context_rules
|
||||||
|
|
||||||
|
```python
|
||||||
|
def apply_context_rules(results: List[Dict[str, Any]], use_macron: bool = False) -> List[Dict[str, Any]]
|
||||||
|
```
|
||||||
|
|
||||||
|
文脈ルールをトークンリストに適用
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **results**: `Transliterator.split_kanji_okurigana`で生成されたトークン辞書のリスト
|
||||||
|
- **use_macron**: 互換性のためのパラメータ(ルール処理では未使用)
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **List[Dict[str, Any]]**: 修正されたトークンリスト(インプレース変更も実施)
|
||||||
|
|
||||||
|
#### 必須キー
|
||||||
|
各トークン辞書は以下のキーを含む必要があります:
|
||||||
|
- **orig**: 元の文字・文字列
|
||||||
|
- **kana**: かな読み
|
||||||
|
- **hira**: ひらがな表記
|
||||||
|
- **hepburn**: ヘボン式ローマ字
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な文脈ルール適用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transliteration.transliteration_context_rules import apply_context_rules
|
||||||
|
|
||||||
|
# トークン化された結果(例)
|
||||||
|
results = [
|
||||||
|
{"orig": "何", "kana": "ナニ", "hira": "なに", "hepburn": "nani"},
|
||||||
|
{"orig": "度", "kana": "ド", "hira": "ど", "hepburn": "do"},
|
||||||
|
{"orig": "も", "kana": "モ", "hira": "も", "hepburn": "mo"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 文脈ルールの適用
|
||||||
|
modified_results = apply_context_rules(results)
|
||||||
|
|
||||||
|
# 結果確認
|
||||||
|
for token in modified_results:
|
||||||
|
print(f"{token['orig']}: {token['kana']} -> {token['hira']} ({token['hepburn']})")
|
||||||
|
|
||||||
|
# 期待される出力(「何度」の場合):
|
||||||
|
# 何: ナン -> (再計算必要) (再計算必要)
|
||||||
|
# 度: ド -> ど (do)
|
||||||
|
# も: モ -> も (mo)
|
||||||
|
```
|
||||||
|
|
||||||
|
### カスタムルールでの処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 独自ルール定義の例
|
||||||
|
custom_rules = {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "custom_rule_example",
|
||||||
|
"target": "今",
|
||||||
|
"match_mode": "equals",
|
||||||
|
"direction": "next",
|
||||||
|
"kana_set": ["バ", "ビ", "ブ", "ベ", "ボ"],
|
||||||
|
"priority": 100,
|
||||||
|
"on_true": {"kana": "イマ"},
|
||||||
|
"on_false": {"kana": "コン"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注意:現在の実装では DEFAULT_RULES が固定使用されています
|
||||||
|
# カスタムルールを使用するには関数の拡張が必要です
|
||||||
|
```
|
||||||
|
|
||||||
|
### 正規表現マッチングの例
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 正規表現ルールの定義例
|
||||||
|
regex_rule = {
|
||||||
|
"name": "kanji_pattern_rule",
|
||||||
|
"match_mode": "regex",
|
||||||
|
"pattern": r"^[一-龯]$", # 任意の漢字1文字
|
||||||
|
"direction": "next",
|
||||||
|
"kana_set": ["ア", "イ", "ウ", "エ", "オ"],
|
||||||
|
"priority": 50,
|
||||||
|
"on_true": {"kana": "特殊読み"},
|
||||||
|
"on_false": {"kana": "通常読み"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 転写パイプラインでの統合
|
||||||
|
|
||||||
|
```python
|
||||||
|
def complete_transliteration_pipeline(text):
|
||||||
|
"""完全な転写パイプライン"""
|
||||||
|
|
||||||
|
# 1. 初期分割・転写
|
||||||
|
transliterator = Transliterator()
|
||||||
|
tokens = transliterator.split_kanji_okurigana(text)
|
||||||
|
|
||||||
|
# 2. 文脈ルール適用
|
||||||
|
tokens = apply_context_rules(tokens)
|
||||||
|
|
||||||
|
# 3. 修正されたトークンの再計算
|
||||||
|
for token in tokens:
|
||||||
|
if token.get("kana") and not token.get("hira"):
|
||||||
|
# ひらがな・ヘボン式の再計算
|
||||||
|
token["hira"] = katakana_to_hiragana(token["kana"])
|
||||||
|
token["hepburn"] = hiragana_to_hepburn(token["hira"])
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
text = "何度でも挑戦する"
|
||||||
|
result = complete_transliteration_pipeline(text)
|
||||||
|
|
||||||
|
for token in result:
|
||||||
|
print(f"{token['orig']} -> {token['kana']} -> {token['hira']} -> {token['hepburn']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## ルール処理ロジック
|
||||||
|
|
||||||
|
### 処理フロー
|
||||||
|
|
||||||
|
1. **ルール準備**
|
||||||
|
- 優先度の降順でソート
|
||||||
|
- 正規表現の事前コンパイル
|
||||||
|
|
||||||
|
2. **トークン走査**
|
||||||
|
- 各トークンに対してルールを順次適用
|
||||||
|
- 空の`orig`を持つトークンはスキップ
|
||||||
|
|
||||||
|
3. **マッチング判定**
|
||||||
|
- `equals`: 完全一致判定
|
||||||
|
- `regex`: 正規表現マッチ判定
|
||||||
|
|
||||||
|
4. **隣接トークン検査**
|
||||||
|
- `direction`に基づく隣接トークン特定
|
||||||
|
- 空のトークンをスキップして有効トークンを検索
|
||||||
|
|
||||||
|
5. **条件評価**
|
||||||
|
- 隣接トークンの`kana`の先頭文字チェック
|
||||||
|
- `kana_set`との一致判定
|
||||||
|
|
||||||
|
6. **アクション実行**
|
||||||
|
- 条件に応じて`on_true`/`on_false`を選択
|
||||||
|
- `kana`の書き換えと`hira`/`hepburn`のクリア
|
||||||
|
|
||||||
|
### アルゴリズム詳細
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_token_with_rules(token_index, tokens, rules):
|
||||||
|
"""単一トークンのルール処理アルゴリズム"""
|
||||||
|
|
||||||
|
token = tokens[token_index]
|
||||||
|
orig = token.get("orig", "")
|
||||||
|
|
||||||
|
# 空トークンはスキップ
|
||||||
|
if not orig:
|
||||||
|
return
|
||||||
|
|
||||||
|
for rule in rules: # 優先度順
|
||||||
|
# マッチング判定
|
||||||
|
if not matches_rule(orig, rule):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 隣接トークン検索
|
||||||
|
neighbor = find_neighbor_token(token_index, tokens, rule["direction"])
|
||||||
|
|
||||||
|
if neighbor:
|
||||||
|
# 条件評価
|
||||||
|
condition = evaluate_condition(neighbor, rule["kana_set"])
|
||||||
|
|
||||||
|
# アクション実行
|
||||||
|
action = rule["on_true"] if condition else rule["on_false"]
|
||||||
|
apply_action(token, action)
|
||||||
|
|
||||||
|
# 最初にマッチしたルールで処理終了
|
||||||
|
break
|
||||||
|
|
||||||
|
def find_neighbor_token(current_index, tokens, direction):
|
||||||
|
"""隣接する有効トークンを検索"""
|
||||||
|
|
||||||
|
if direction == "next":
|
||||||
|
for i in range(current_index + 1, len(tokens)):
|
||||||
|
if tokens[i].get("orig"):
|
||||||
|
return tokens[i]
|
||||||
|
elif direction == "prev":
|
||||||
|
for i in range(current_index - 1, -1, -1):
|
||||||
|
if tokens[i].get("orig"):
|
||||||
|
return tokens[i]
|
||||||
|
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
## 具体的なルール例
|
||||||
|
|
||||||
|
### 「何」の読み分けルール
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"name": "nan_next_tdna",
|
||||||
|
"target": "何",
|
||||||
|
"match_mode": "equals",
|
||||||
|
"direction": "next",
|
||||||
|
"kana_set": ["タ", "チ", "ツ", "テ", "ト", "ダ", "ヂ", "ヅ", "デ", "ド", "ナ", "ニ", "ヌ", "ネ", "ノ"],
|
||||||
|
"on_true": {"kana": "ナン"},
|
||||||
|
"on_false": {"kana": "ナニ"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 動作例
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 「何度」の場合
|
||||||
|
tokens = [
|
||||||
|
{"orig": "何", "kana": "ナニ"}, # 初期状態
|
||||||
|
{"orig": "度", "kana": "ド"} # 次のトークン
|
||||||
|
]
|
||||||
|
|
||||||
|
# ルール適用後
|
||||||
|
tokens = [
|
||||||
|
{"orig": "何", "kana": "ナン"}, # 「ド」が kana_set に含まれるため "ナン" に変更
|
||||||
|
{"orig": "度", "kana": "ド"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 「何回」の場合
|
||||||
|
tokens = [
|
||||||
|
{"orig": "何", "kana": "ナニ"}, # 初期状態
|
||||||
|
{"orig": "回", "kana": "カイ"} # 次のトークン
|
||||||
|
]
|
||||||
|
|
||||||
|
# ルール適用後
|
||||||
|
tokens = [
|
||||||
|
{"orig": "何", "kana": "ナニ"}, # 「カイ」が kana_set に含まれないため "ナニ" のまま
|
||||||
|
{"orig": "回", "kana": "カイ"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 正規表現コンパイルエラー
|
||||||
|
```python
|
||||||
|
# 不正な正規表現の安全な処理
|
||||||
|
for rule in rules:
|
||||||
|
if rule.get("match_mode") == "regex" and rule.get("pattern"):
|
||||||
|
try:
|
||||||
|
rule["_re"] = re.compile(rule["pattern"])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"正規表現コンパイルエラー: {rule['pattern']} - {e}")
|
||||||
|
rule["_re"] = None # 無効化
|
||||||
|
```
|
||||||
|
|
||||||
|
### 不正なトークン構造
|
||||||
|
```python
|
||||||
|
# 必須キーの存在確認
|
||||||
|
def validate_token(token):
|
||||||
|
"""トークンの妥当性検証"""
|
||||||
|
required_keys = ["orig", "kana", "hira", "hepburn"]
|
||||||
|
|
||||||
|
for key in required_keys:
|
||||||
|
if key not in token:
|
||||||
|
print(f"警告: トークンに必須キー '{key}' が不足")
|
||||||
|
token[key] = "" # デフォルト値を設定
|
||||||
|
|
||||||
|
return token
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
### 効率的な処理
|
||||||
|
- インプレース変更によるメモリ効率
|
||||||
|
- 優先度ソートによる早期終了
|
||||||
|
- 正規表現の事前コンパイル
|
||||||
|
|
||||||
|
### スケーラビリティ
|
||||||
|
- 大量トークンでの線形処理時間
|
||||||
|
- ルール数の増加に対する適切な対応
|
||||||
|
- キャッシュ機能の追加可能性
|
||||||
|
|
||||||
|
## 拡張可能性
|
||||||
|
|
||||||
|
### ルール形式の拡張
|
||||||
|
```python
|
||||||
|
# より複雑なルール例(将来的な拡張)
|
||||||
|
complex_rule = {
|
||||||
|
"name": "multi_condition_rule",
|
||||||
|
"target": "言",
|
||||||
|
"conditions": [
|
||||||
|
{"direction": "prev", "kana_set": ["オ", "コ"]},
|
||||||
|
{"direction": "next", "kana_set": ["ハ", "バ"]}
|
||||||
|
],
|
||||||
|
"operator": "AND", # or "OR"
|
||||||
|
"actions": {
|
||||||
|
"all_true": {"kana": "ゴン"},
|
||||||
|
"any_true": {"kana": "ゲン"},
|
||||||
|
"all_false": {"kana": "イ"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 動的ルール追加
|
||||||
|
```python
|
||||||
|
def add_runtime_rule(new_rule):
|
||||||
|
"""実行時ルール追加(拡張版)"""
|
||||||
|
# ルールの検証
|
||||||
|
if validate_rule_format(new_rule):
|
||||||
|
DEFAULT_RULES["rules"].append(new_rule)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `typing`: 型ヒント
|
||||||
|
- `re`: 正規表現処理
|
||||||
|
|
||||||
|
### 関連モジュール
|
||||||
|
- `transliteration_transliterator.py`: メイン転写クラス
|
||||||
|
- `transliteration_kana_to_hepburn.py`: かな→ヘボン式変換
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- ルール適用後は`hira`と`hepburn`が空文字列になるため、呼び出し元での再計算が必要
|
||||||
|
- 現在のルールは日本語に特化している
|
||||||
|
- ルール適用順序は優先度に依存するため、適切な設定が重要
|
||||||
|
- 正規表現ルールはパフォーマンスに影響する可能性がある
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- 外部ルールファイルの読み込み対応
|
||||||
|
- より複雑な条件式のサポート
|
||||||
|
- ルール適用ログ・デバッグ機能
|
||||||
|
- 言語別ルールセットの対応
|
||||||
|
- パフォーマンス最適化とキャッシュ機能
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
# transliteration_kana_to_hepburn.py - カタカナ→ヘボン式変換
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
カタカナ文字列を標準的なヘボン式ローマ字に変換するモジュールです。マクロン(長音記号)対応、外来語音の変換、促音・撥音処理など、日本語のローマ字表記に必要な機能を包括的に提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 標準的なヘボン式変換
|
||||||
|
- カタカナ文字の基本ローマ字変換
|
||||||
|
- マクロン(ā ī ū ē ō)による長音表現
|
||||||
|
- 連続母音表記の選択的対応
|
||||||
|
|
||||||
|
### 特殊音処理
|
||||||
|
- 促音(ッ)の適切な子音重複処理
|
||||||
|
- 撥音(ン)のm/n使い分け
|
||||||
|
- 長音符(ー)の前母音延長処理
|
||||||
|
|
||||||
|
### 外来語対応
|
||||||
|
- シェ(she)、チェ(che)等の組み合わせ
|
||||||
|
- ヴ音(vu, va, vi, ve, vo)の変換
|
||||||
|
- ファ行(fa, fi, fe, fo)の処理
|
||||||
|
|
||||||
|
## 主要関数
|
||||||
|
|
||||||
|
### katakana_to_hepburn
|
||||||
|
|
||||||
|
```python
|
||||||
|
def katakana_to_hepburn(kata: str, use_macron: bool = True) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
カタカナ文字列をヘボン式ローマ字に変換
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **kata**: 変換対象のカタカナ文字列
|
||||||
|
- **use_macron**: マクロン使用フラグ(True=ā ī ū ē ō、False=aa ii uu ee oo)
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **str**: ヘボン式ローマ字文字列(小文字)
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な変換
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transliteration.transliteration_kana_to_hepburn import katakana_to_hepburn
|
||||||
|
|
||||||
|
# 基本的なカタカナ変換
|
||||||
|
result1 = katakana_to_hepburn("カタカナ")
|
||||||
|
print(result1) # "katakana"
|
||||||
|
|
||||||
|
# 長音のマクロン表記
|
||||||
|
result2 = katakana_to_hepburn("コンピューター", use_macron=True)
|
||||||
|
print(result2) # "konpyūtā"
|
||||||
|
|
||||||
|
# 長音の連続母音表記
|
||||||
|
result3 = katakana_to_hepburn("コンピューター", use_macron=False)
|
||||||
|
print(result3) # "konpyuutaa"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特殊音の処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 促音(ッ)の処理
|
||||||
|
result1 = katakana_to_hepburn("キャッチ")
|
||||||
|
print(result1) # "kyatchi"
|
||||||
|
|
||||||
|
result2 = katakana_to_hepburn("マッチャ")
|
||||||
|
print(result2) # "matcha"
|
||||||
|
|
||||||
|
# 撥音(ン)の処理
|
||||||
|
result3 = katakana_to_hepburn("ホンバン") # ホン+バン
|
||||||
|
print(result3) # "homban" (n→m変換)
|
||||||
|
|
||||||
|
result4 = katakana_to_hepburn("ホンテン") # ホン+テン
|
||||||
|
print(result4) # "honten" (nのまま)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 外来語音の変換
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 外来語特殊音
|
||||||
|
result1 = katakana_to_hepburn("シェア")
|
||||||
|
print(result1) # "shea"
|
||||||
|
|
||||||
|
result2 = katakana_to_hepburn("チェック")
|
||||||
|
print(result2) # "chekku"
|
||||||
|
|
||||||
|
result3 = katakana_to_hepburn("ジェット")
|
||||||
|
print(result3) # "jetto"
|
||||||
|
|
||||||
|
# ヴ音の処理
|
||||||
|
result4 = katakana_to_hepburn("ヴァイオリン")
|
||||||
|
print(result4) # "vaiorin"
|
||||||
|
|
||||||
|
result5 = katakana_to_hepburn("ヴィーナス")
|
||||||
|
print(result5) # "vīnasu"
|
||||||
|
|
||||||
|
# ファ行の処理
|
||||||
|
result6 = katakana_to_hepburn("ファイル")
|
||||||
|
print(result6) # "fairu"
|
||||||
|
|
||||||
|
result7 = katakana_to_hepburn("フィルム")
|
||||||
|
print(result7) # "firumu"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 長音処理の詳細
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 長音符(ー)の処理
|
||||||
|
result1 = katakana_to_hepburn("スーパー", use_macron=True)
|
||||||
|
print(result1) # "sūpā"
|
||||||
|
|
||||||
|
result2 = katakana_to_hepburn("パーティー", use_macron=True)
|
||||||
|
print(result2) # "pātī"
|
||||||
|
|
||||||
|
# ou → ō の変換(東京型)
|
||||||
|
result3 = katakana_to_hepburn("トウキョウ", use_macron=True)
|
||||||
|
print(result3) # "tōkyō"
|
||||||
|
|
||||||
|
# 連続母音表記との比較
|
||||||
|
result4 = katakana_to_hepburn("トウキョウ", use_macron=False)
|
||||||
|
print(result4) # "toukyou"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 複雑な組み合わせ
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 拗音(ゃゅょ)の組み合わせ
|
||||||
|
test_cases = [
|
||||||
|
("キャンプ", "kyanpu"),
|
||||||
|
("シュート", "shūto"),
|
||||||
|
("チョコレート", "chokorēto"),
|
||||||
|
("ギュウニュウ", "gyūnyū"),
|
||||||
|
("リュックサック", "ryukkusakku"),
|
||||||
|
("ピョンピョン", "pyonpyon")
|
||||||
|
]
|
||||||
|
|
||||||
|
for kata, expected in test_cases:
|
||||||
|
result = katakana_to_hepburn(kata)
|
||||||
|
print(f"{kata} -> {result}")
|
||||||
|
# キャンプ -> kyanpu
|
||||||
|
# シュート -> shūto
|
||||||
|
# チョコレート -> chokorēto
|
||||||
|
# ギュウニュウ -> gyūnyū
|
||||||
|
# リュックサック -> ryukkusakku
|
||||||
|
# ピョンピョン -> pyonpyon
|
||||||
|
```
|
||||||
|
|
||||||
|
## 変換ルール詳細
|
||||||
|
|
||||||
|
### 基本音対応表
|
||||||
|
|
||||||
|
```python
|
||||||
|
base_mapping = {
|
||||||
|
# 清音
|
||||||
|
'ア':'a', 'イ':'i', 'ウ':'u', 'エ':'e', 'オ':'o',
|
||||||
|
'カ':'ka', 'キ':'ki', 'ク':'ku', 'ケ':'ke', 'コ':'ko',
|
||||||
|
'サ':'sa', 'シ':'shi', 'ス':'su', 'セ':'se', 'ソ':'so',
|
||||||
|
'タ':'ta', 'チ':'chi', 'ツ':'tsu', 'テ':'te', 'ト':'to',
|
||||||
|
'ナ':'na', 'ニ':'ni', 'ヌ':'nu', 'ネ':'ne', 'ノ':'no',
|
||||||
|
'ハ':'ha', 'ヒ':'hi', 'フ':'fu', 'ヘ':'he', 'ホ':'ho',
|
||||||
|
'マ':'ma', 'ミ':'mi', 'ム':'mu', 'メ':'me', 'モ':'mo',
|
||||||
|
'ヤ':'ya', 'ユ':'yu', 'ヨ':'yo',
|
||||||
|
'ラ':'ra', 'リ':'ri', 'ル':'ru', 'レ':'re', 'ロ':'ro',
|
||||||
|
'ワ':'wa', 'ヲ':'wo', 'ン':'n',
|
||||||
|
|
||||||
|
# 濁音・半濁音
|
||||||
|
'ガ':'ga', 'ギ':'gi', 'グ':'gu', 'ゲ':'ge', 'ゴ':'go',
|
||||||
|
'ザ':'za', 'ジ':'ji', 'ズ':'zu', 'ゼ':'ze', 'ゾ':'zo',
|
||||||
|
'ダ':'da', 'ヂ':'ji', 'ヅ':'zu', 'デ':'de', 'ド':'do',
|
||||||
|
'バ':'ba', 'ビ':'bi', 'ブ':'bu', 'ベ':'be', 'ボ':'bo',
|
||||||
|
'パ':'pa', 'ピ':'pi', 'プ':'pu', 'ペ':'pe', 'ポ':'po',
|
||||||
|
|
||||||
|
# 特殊音
|
||||||
|
'ヴ':'vu'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 拗音組み合わせ
|
||||||
|
|
||||||
|
```python
|
||||||
|
digraphs_mapping = {
|
||||||
|
# キャ行
|
||||||
|
('キ','ャ'):'kya', ('キ','ュ'):'kyu', ('キ','ョ'):'kyo',
|
||||||
|
('ギ','ャ'):'gya', ('ギ','ュ'):'gyu', ('ギ','ョ'):'gyo',
|
||||||
|
|
||||||
|
# シャ行
|
||||||
|
('シ','ャ'):'sha', ('シ','ュ'):'shu', ('シ','ョ'):'sho',
|
||||||
|
('ジ','ャ'):'ja', ('ジ','ュ'):'ju', ('ジ','ョ'):'jo',
|
||||||
|
|
||||||
|
# チャ行
|
||||||
|
('チ','ャ'):'cha', ('チ','ュ'):'chu', ('チ','ョ'):'cho',
|
||||||
|
|
||||||
|
# その他の拗音
|
||||||
|
('ニ','ャ'):'nya', ('ヒ','ャ'):'hya', ('ビ','ャ'):'bya',
|
||||||
|
('ピ','ャ'):'pya', ('ミ','ャ'):'mya', ('リ','ャ'):'rya',
|
||||||
|
|
||||||
|
# 外来語音(ファ行等)
|
||||||
|
('フ','ァ'):'fa', ('フ','ィ'):'fi', ('フ','ェ'):'fe', ('フ','ォ'):'fo',
|
||||||
|
('シ','ェ'):'she', ('チ','ェ'):'che', ('テ','ィ'):'ti',
|
||||||
|
('ツ','ァ'):'tsa', ('ツ','ィ'):'tsi', ('ツ','ェ'):'tse', ('ツ','ォ'):'tso',
|
||||||
|
|
||||||
|
# ヴ音組み合わせ
|
||||||
|
('ヴ','ァ'):'va', ('ヴ','ィ'):'vi', ('ヴ','ェ'):'ve',
|
||||||
|
('ヴ','ォ'):'vo', ('ヴ','ュ'):'vyu'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### マクロン変換規則
|
||||||
|
|
||||||
|
```python
|
||||||
|
macron_rules = {
|
||||||
|
'aa': 'ā', # カア → kā
|
||||||
|
'ii': 'ī', # キイ → kī
|
||||||
|
'uu': 'ū', # クウ → kū
|
||||||
|
'ee': 'ē', # ケエ → kē
|
||||||
|
'oo': 'ō', # コオ → kō
|
||||||
|
'ou': 'ō' # コウ → kō(東京型長音)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 特殊処理アルゴリズム
|
||||||
|
|
||||||
|
### 促音(ッ)処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_sokuon(current_pos, kata_string, result_list):
|
||||||
|
"""促音の処理アルゴリズム"""
|
||||||
|
|
||||||
|
# 次の音を確認
|
||||||
|
if current_pos + 1 < len(kata_string):
|
||||||
|
next_kana = kata_string[current_pos + 1]
|
||||||
|
|
||||||
|
# 次の音のローマ字を取得
|
||||||
|
next_roman = get_next_roman(next_kana, kata_string[current_pos + 1:])
|
||||||
|
|
||||||
|
# 子音部分を抽出して重複
|
||||||
|
consonant = extract_initial_consonant(next_roman)
|
||||||
|
if consonant:
|
||||||
|
result_list.append(consonant[0]) # 先頭子音を重複
|
||||||
|
|
||||||
|
# 促音自体は消費
|
||||||
|
return current_pos + 1
|
||||||
|
|
||||||
|
# 例:
|
||||||
|
# マッチャ -> ma + tcha (ッ -> t重複) -> matcha
|
||||||
|
# キャッチ -> kya + tchi (ッ -> t重複) -> kyatchi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 撥音(ン)処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_hatsuon(roman_string):
|
||||||
|
"""撥音のm/n使い分け処理"""
|
||||||
|
|
||||||
|
# n の後に b/p/m が続く場合は m に変換
|
||||||
|
import re
|
||||||
|
|
||||||
|
# パターン: n + [bmp] -> m + [bmp]
|
||||||
|
result = re.sub(r'n(?=[bmp])', 'm', roman_string)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 例:
|
||||||
|
# ホンバン -> honban -> homban
|
||||||
|
# サンポ -> sanpo -> sampo
|
||||||
|
# コンマ -> konma -> komma
|
||||||
|
# but: ホンテン -> honten (変更なし)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 長音符(ー)処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_choonpu(roman_list):
|
||||||
|
"""長音符の前母音延長処理"""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(roman_list):
|
||||||
|
if roman_list[i] == '-': # 長音符マーカー
|
||||||
|
if i > 0:
|
||||||
|
prev_char = result[-1] # 直前の文字
|
||||||
|
if prev_char in 'aiueo':
|
||||||
|
# 前が母音なら重複(後でマクロン処理)
|
||||||
|
result.append(prev_char)
|
||||||
|
# else: 子音の場合は無視
|
||||||
|
else:
|
||||||
|
result.append(roman_list[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 例:
|
||||||
|
# スー -> su + - -> suu -> sū (マクロン処理後)
|
||||||
|
# パーティー -> pa + - + ti + - -> paatii -> pātī
|
||||||
|
```
|
||||||
|
|
||||||
|
## 実装例・テストケース
|
||||||
|
|
||||||
|
### 基本テストセット
|
||||||
|
|
||||||
|
```python
|
||||||
|
def run_basic_tests():
|
||||||
|
"""基本変換テストセット"""
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# 基本音
|
||||||
|
("アイウエオ", "aiueo"),
|
||||||
|
("カキクケコ", "kakikukeko"),
|
||||||
|
("サシスセソ", "sashisuseso"),
|
||||||
|
|
||||||
|
# 濁音・半濁音
|
||||||
|
("ガギグゲゴ", "gagigugego"),
|
||||||
|
("ザジズゼゾ", "zajizuzezo"),
|
||||||
|
("バビブベボ", "babibubebo"),
|
||||||
|
("パピプペポ", "papipupepo"),
|
||||||
|
|
||||||
|
# 特殊音
|
||||||
|
("シャシュショ", "shashusho"),
|
||||||
|
("チャチュチョ", "chachucho"),
|
||||||
|
("ジャジュジョ", "jajujo"),
|
||||||
|
|
||||||
|
# 促音
|
||||||
|
("アッパ", "appa"),
|
||||||
|
("イッキ", "ikki"),
|
||||||
|
("エッサ", "essa"),
|
||||||
|
|
||||||
|
# 撥音
|
||||||
|
("アンパン", "ampan"),
|
||||||
|
("コンマ", "komma"),
|
||||||
|
("ホンテン", "honten")
|
||||||
|
]
|
||||||
|
|
||||||
|
for kata, expected in test_cases:
|
||||||
|
result = katakana_to_hepburn(kata)
|
||||||
|
assert result == expected, f"Failed: {kata} -> {result} (expected {expected})"
|
||||||
|
print(f"✓ {kata} -> {result}")
|
||||||
|
|
||||||
|
run_basic_tests()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 外来語テストセット
|
||||||
|
|
||||||
|
```python
|
||||||
|
def run_foreign_word_tests():
|
||||||
|
"""外来語変換テストセット"""
|
||||||
|
|
||||||
|
foreign_tests = [
|
||||||
|
# ファ行
|
||||||
|
("ファイル", "fairu"),
|
||||||
|
("フィルム", "firumu"),
|
||||||
|
("フェイス", "feisu"),
|
||||||
|
("フォント", "fonto"),
|
||||||
|
|
||||||
|
# シェ・チェ
|
||||||
|
("シェア", "shea"),
|
||||||
|
("シェル", "sheru"),
|
||||||
|
("チェック", "chekku"),
|
||||||
|
("チェイン", "chein"),
|
||||||
|
|
||||||
|
# ヴ音
|
||||||
|
("ヴァイオリン", "vaiorin"),
|
||||||
|
("ヴィーナス", "vīnasu"),
|
||||||
|
("ヴェール", "vēru"),
|
||||||
|
("ヴォーカル", "vōkaru"),
|
||||||
|
|
||||||
|
# ティ・トゥ・ドゥ
|
||||||
|
("ティー", "tī"),
|
||||||
|
("パーティー", "pātī"),
|
||||||
|
("トゥー", "tū"),
|
||||||
|
("ドゥー", "dū")
|
||||||
|
]
|
||||||
|
|
||||||
|
for kata, expected in foreign_tests:
|
||||||
|
result = katakana_to_hepburn(kata, use_macron=True)
|
||||||
|
print(f"✓ {kata} -> {result}")
|
||||||
|
# 実際のexpectedとの比較は実装依存
|
||||||
|
|
||||||
|
run_foreign_word_tests()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 長音テストセット
|
||||||
|
|
||||||
|
```python
|
||||||
|
def run_long_vowel_tests():
|
||||||
|
"""長音処理テストセット"""
|
||||||
|
|
||||||
|
long_vowel_tests = [
|
||||||
|
# マクロンあり
|
||||||
|
("コーヒー", "kōhī", True),
|
||||||
|
("スーパー", "sūpā", True),
|
||||||
|
("パーティー", "pātī", True),
|
||||||
|
("トウキョウ", "tōkyō", True), # ou -> ō
|
||||||
|
|
||||||
|
# マクロンなし
|
||||||
|
("コーヒー", "koohii", False),
|
||||||
|
("スーパー", "suupaa", False),
|
||||||
|
("パーティー", "paatii", False),
|
||||||
|
("トウキョウ", "toukyou", False)
|
||||||
|
]
|
||||||
|
|
||||||
|
for kata, expected, use_macron in long_vowel_tests:
|
||||||
|
result = katakana_to_hepburn(kata, use_macron=use_macron)
|
||||||
|
print(f"✓ {kata} -> {result} (macron={use_macron})")
|
||||||
|
|
||||||
|
run_long_vowel_tests()
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
### 効率的な処理
|
||||||
|
- 単一パス処理による高速変換
|
||||||
|
- 正規表現の最小限使用
|
||||||
|
- 辞書ルックアップの最適化
|
||||||
|
|
||||||
|
### メモリ効率
|
||||||
|
- 文字列連結の最適化
|
||||||
|
- 不要な中間オブジェクトの削減
|
||||||
|
- 大量テキスト処理への対応
|
||||||
|
|
||||||
|
## 制限事項・注意点
|
||||||
|
|
||||||
|
### 変換精度の制限
|
||||||
|
- 文脈に依存する読み分けは非対応
|
||||||
|
- 固有名詞の特殊読みは非対応
|
||||||
|
- 方言・古語の特殊音は非対応
|
||||||
|
|
||||||
|
### ヘボン式の範囲
|
||||||
|
- 標準的なヘボン式に準拠
|
||||||
|
- 一部の外来語音は近似変換
|
||||||
|
- 撥音の文脈依存ルールは簡略化
|
||||||
|
|
||||||
|
### 入力制限
|
||||||
|
```python
|
||||||
|
# 適切な入力例
|
||||||
|
good_inputs = ["カタカナ", "シャープ", "コンピューター"]
|
||||||
|
|
||||||
|
# 問題のある入力例
|
||||||
|
problematic_inputs = [
|
||||||
|
"ひらがな", # ひらがな混在(処理されるがそのまま)
|
||||||
|
"English", # 英字混在(処理されるがそのまま)
|
||||||
|
"123数字", # 数字混在(処理されるがそのまま)
|
||||||
|
"", # 空文字列(空文字列を返す)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 混在入力の処理例
|
||||||
|
mixed_result = katakana_to_hepburn("カタカナと英語English")
|
||||||
|
print(mixed_result) # "katakanaと英語english"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transliteration_transliterator.py`: メイン転写クラス
|
||||||
|
- `transliteration_context_rules.py`: 文脈依存ルール
|
||||||
|
- 外部のひらがな↔カタカナ変換モジュール(必要に応じて)
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- 更なる外来語音への対応
|
||||||
|
- 文脈依存の読み分け機能
|
||||||
|
- パフォーマンス最適化
|
||||||
|
- より詳細なヘボン式バリエーション対応
|
||||||
|
- 音韻変化ルールの追加
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
# transliteration_transliterator.py - 総合音写・転写システム
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
SudachiPyを利用した日本語のローマ字転写システムのメインクラスです。形態素解析、漢字・送り仮名の分離、文脈依存ルールの適用、ヘボン式変換を統合し、高精度な日本語ローマ字化を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 統合転写システム
|
||||||
|
- SudachiPyによる高精度形態素解析
|
||||||
|
- 漢字・送り仮名の自動分離処理
|
||||||
|
- 文脈依存読み変更ルールの適用
|
||||||
|
|
||||||
|
### 多層変換処理
|
||||||
|
- カタカナ読み取得・分配
|
||||||
|
- ひらがな自動変換
|
||||||
|
- ヘボン式ローマ字生成
|
||||||
|
|
||||||
|
### 並行処理対応
|
||||||
|
- スレッドセーフなトークナイザー利用
|
||||||
|
- ロック機構による安全な並行実行
|
||||||
|
- 高負荷環境での安定動作
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Transliterator クラス
|
||||||
|
```python
|
||||||
|
class Transliterator:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.tokenizer_obj: tokenizer.Tokenizer
|
||||||
|
self.mode: tokenizer.Tokenizer.SplitMode
|
||||||
|
self._tokenizer_lock: threading.Lock
|
||||||
|
```
|
||||||
|
|
||||||
|
日本語転写処理の中核クラス
|
||||||
|
|
||||||
|
#### 属性
|
||||||
|
- **tokenizer_obj**: SudachiPyトークナイザーインスタンス
|
||||||
|
- **mode**: 分割モード(SplitMode.C = 最長一致)
|
||||||
|
- **_tokenizer_lock**: 並行アクセス制御用ミューテックス
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### analyze
|
||||||
|
|
||||||
|
```python
|
||||||
|
def analyze(self, text: str, use_macron: bool = False) -> List[Dict[str, Any]]
|
||||||
|
```
|
||||||
|
|
||||||
|
テキストを解析して転写情報を生成
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **text**: 解析対象の日本語テキスト
|
||||||
|
- **use_macron**: マクロン使用フラグ(長音表記方式)
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **List[Dict[str, Any]]**: トークン転写情報のリスト
|
||||||
|
|
||||||
|
#### 出力辞書構造
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"orig": str, # 元の文字・文字列
|
||||||
|
"kana": str, # カタカナ読み
|
||||||
|
"hira": str, # ひらがな読み
|
||||||
|
"hepburn": str # ヘボン式ローマ字
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### split_kanji_okurigana (静的メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
def split_kanji_okurigana(surface: str, reading_kana: str, use_macron: bool = True) -> List[Dict[str, str]]
|
||||||
|
```
|
||||||
|
|
||||||
|
単語の表層形と読みを漢字・送り仮名ブロックに分割
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **surface**: 表層形(漢字+ひらがな混在可能)
|
||||||
|
- **reading_kana**: 全体のカタカナ読み
|
||||||
|
- **use_macron**: ヘボン式変換でのマクロン使用
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **List[Dict[str, str]]**: 分割された部分の転写情報
|
||||||
|
|
||||||
|
## 補助メソッド
|
||||||
|
|
||||||
|
### is_kanji (静的メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
def is_kanji(ch: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
文字が漢字かどうかを判定
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **ch**: 判定対象文字
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **bool**: 漢字判定結果
|
||||||
|
|
||||||
|
### kata_to_hira (静的メソッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
def kata_to_hira(text: str) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
カタカナをひらがなに変換
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **text**: 変換対象のカタカナテキスト
|
||||||
|
|
||||||
|
#### 戻り値
|
||||||
|
- **str**: ひらがな変換結果
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な転写処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.transliteration.transliteration_transliterator import Transliterator
|
||||||
|
|
||||||
|
# 転写システムの初期化
|
||||||
|
transliterator = Transliterator()
|
||||||
|
|
||||||
|
# 基本的な文章の転写
|
||||||
|
text = "向こうへ行く"
|
||||||
|
results = transliterator.analyze(text)
|
||||||
|
|
||||||
|
for token in results:
|
||||||
|
print(f"{token['orig']} -> {token['kana']} -> {token['hira']} -> {token['hepburn']}")
|
||||||
|
|
||||||
|
# 期待される出力例:
|
||||||
|
# 向こう -> ムコウ -> むこう -> mukou
|
||||||
|
# へ -> ヘ -> へ -> he
|
||||||
|
# 行く -> イク -> いく -> iku
|
||||||
|
```
|
||||||
|
|
||||||
|
### マクロン使用の長音処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# マクロンを使用した長音表記
|
||||||
|
text = "東京に行く"
|
||||||
|
results_macron = transliterator.analyze(text, use_macron=True)
|
||||||
|
results_normal = transliterator.analyze(text, use_macron=False)
|
||||||
|
|
||||||
|
print("=== マクロンあり ===")
|
||||||
|
for token in results_macron:
|
||||||
|
print(f"{token['orig']} -> {token['hepburn']}")
|
||||||
|
|
||||||
|
print("=== マクロンなし ===")
|
||||||
|
for token in results_normal:
|
||||||
|
print(f"{token['orig']} -> {token['hepburn']}")
|
||||||
|
|
||||||
|
# 期待される出力:
|
||||||
|
# === マクロンあり ===
|
||||||
|
# 東京 -> tōkyō
|
||||||
|
# に -> ni
|
||||||
|
# 行く -> iku
|
||||||
|
|
||||||
|
# === マクロンなし ===
|
||||||
|
# 東京 -> toukyou
|
||||||
|
# に -> ni
|
||||||
|
# 行く -> iku
|
||||||
|
```
|
||||||
|
|
||||||
|
### 複雑な文章の処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 漢字・ひらがな・カタカナ・英語混在文の処理
|
||||||
|
complex_text = "パーティーで美しい花を見る"
|
||||||
|
results = transliterator.analyze(complex_text, use_macron=True)
|
||||||
|
|
||||||
|
for token in results:
|
||||||
|
print(f"原文: '{token['orig']}'")
|
||||||
|
print(f" カナ: {token['kana']}")
|
||||||
|
print(f" ひら: {token['hira']}")
|
||||||
|
print(f" ローマ: {token['hepburn']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 期待される出力:
|
||||||
|
# 原文: 'パーティー'
|
||||||
|
# カナ: パーティー
|
||||||
|
# ひら: ぱーてぃー
|
||||||
|
# ローマ: pātī
|
||||||
|
#
|
||||||
|
# 原文: 'で'
|
||||||
|
# カナ: デ
|
||||||
|
# ひら: で
|
||||||
|
# ローマ: de
|
||||||
|
#
|
||||||
|
# 原文: '美しい'
|
||||||
|
# カナ: ウツクシイ
|
||||||
|
# ひら: うつくしい
|
||||||
|
# ローマ: utsukushii
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文脈依存ルールの効果確認
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 文脈に依存する読み変更の例(「何」の読み分け)
|
||||||
|
test_cases = [
|
||||||
|
"何が好き?", # 何 -> ナニ (後続が「ガ」)
|
||||||
|
"何度も挑戦", # 何 -> ナン (後続が「ド」)
|
||||||
|
"何色ありますか?" # 何 -> ナニ (後続が「イ」)
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in test_cases:
|
||||||
|
results = transliterator.analyze(text)
|
||||||
|
|
||||||
|
print(f"入力: {text}")
|
||||||
|
|
||||||
|
# 「何」トークンを探して読みを確認
|
||||||
|
for token in results:
|
||||||
|
if token['orig'] == '何':
|
||||||
|
print(f"「何」の読み: {token['kana']} -> {token['hepburn']}")
|
||||||
|
break
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 期待される出力:
|
||||||
|
# 入力: 何が好き?
|
||||||
|
# 「何」の読み: ナニ -> nani
|
||||||
|
#
|
||||||
|
# 入力: 何度も挑戦
|
||||||
|
# 「何」の読み: ナン -> nan
|
||||||
|
#
|
||||||
|
# 入力: 何色ありますか?
|
||||||
|
# 「何」の読み: ナニ -> nani
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特殊文字・記号の処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 記号・英数字混在テキストの処理
|
||||||
|
mixed_text = "ID:12345、URL:https://example.com"
|
||||||
|
results = transliterator.analyze(mixed_text)
|
||||||
|
|
||||||
|
for token in results:
|
||||||
|
print(f"'{token['orig']}' -> '{token['hepburn']}'")
|
||||||
|
|
||||||
|
# 期待される出力:
|
||||||
|
# 'ID' -> 'ID' # 英字はそのまま
|
||||||
|
# ':' -> ':' # 記号はそのまま
|
||||||
|
# '12345' -> '12345' # 数字はそのまま
|
||||||
|
# '、' -> '、' # 区切り記号はそのまま
|
||||||
|
# 'URL' -> 'URL' # 英字はそのまま
|
||||||
|
```
|
||||||
|
|
||||||
|
## 内部処理フロー
|
||||||
|
|
||||||
|
### 解析処理パイプライン
|
||||||
|
|
||||||
|
```python
|
||||||
|
def analyze_pipeline_explained(self, text):
|
||||||
|
"""転写処理パイプラインの詳細説明"""
|
||||||
|
|
||||||
|
# 1. SudachiPy形態素解析
|
||||||
|
with self._tokenizer_lock:
|
||||||
|
tokens = self.tokenizer_obj.tokenize(text, self.mode)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 2. 各トークンの処理
|
||||||
|
for token in tokens:
|
||||||
|
surface = token.surface() # 表層形
|
||||||
|
reading = token.reading_form() # 読み(カタカナ)
|
||||||
|
pos = token.part_of_speech() # 品詞情報
|
||||||
|
|
||||||
|
# 3. 記号・空白の特別処理
|
||||||
|
if pos and pos[0] in ["記号", "補助記号", "空白"]:
|
||||||
|
reading = surface # 記号は表層形をそのまま使用
|
||||||
|
|
||||||
|
# 4. 表層形と読みが同じ場合(ひらがな・記号等)
|
||||||
|
if surface == reading:
|
||||||
|
results.append({
|
||||||
|
"orig": surface,
|
||||||
|
"kana": reading,
|
||||||
|
"hira": surface, # そのまま
|
||||||
|
"hepburn": surface # そのまま
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 5. 単一文字の処理
|
||||||
|
if len(surface) == 1:
|
||||||
|
results.append({
|
||||||
|
"orig": surface,
|
||||||
|
"kana": reading,
|
||||||
|
"hira": self.kata_to_hira(reading),
|
||||||
|
"hepburn": katakana_to_hepburn(reading, use_macron)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 6. 複数文字の漢字・送り仮名分離
|
||||||
|
parts = self.split_kanji_okurigana(surface, reading, use_macron)
|
||||||
|
results.extend(parts)
|
||||||
|
|
||||||
|
# 7. 文脈依存ルールの適用
|
||||||
|
try:
|
||||||
|
results = apply_context_rules(results, use_macron) or results
|
||||||
|
except Exception:
|
||||||
|
pass # ルール適用失敗時は元の結果を使用
|
||||||
|
|
||||||
|
# 8. ルール適用後の再計算
|
||||||
|
for entry in results:
|
||||||
|
kana = entry.get("kana", "")
|
||||||
|
if kana:
|
||||||
|
entry["hira"] = self.kata_to_hira(kana)
|
||||||
|
entry["hepburn"] = katakana_to_hepburn(kana, use_macron)
|
||||||
|
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
### 漢字・送り仮名分離アルゴリズム
|
||||||
|
|
||||||
|
```python
|
||||||
|
def split_algorithm_explained(surface, reading_kana):
|
||||||
|
"""分離アルゴリズムの詳細説明"""
|
||||||
|
|
||||||
|
# 1. 表層形のブロック分割
|
||||||
|
blocks = []
|
||||||
|
current_block = ""
|
||||||
|
prev_is_kanji = None
|
||||||
|
|
||||||
|
for char in surface:
|
||||||
|
is_kanji = Transliterator.is_kanji(char)
|
||||||
|
|
||||||
|
if prev_is_kanji is None or is_kanji == prev_is_kanji:
|
||||||
|
# 同じタイプの文字は同じブロックに
|
||||||
|
current_block += char
|
||||||
|
else:
|
||||||
|
# タイプが変わったら新しいブロック
|
||||||
|
blocks.append((prev_is_kanji, current_block))
|
||||||
|
current_block = char
|
||||||
|
|
||||||
|
prev_is_kanji = is_kanji
|
||||||
|
|
||||||
|
if current_block:
|
||||||
|
blocks.append((prev_is_kanji, current_block))
|
||||||
|
|
||||||
|
# 例: "向こう" -> [(True, "向"), (False, "こう")]
|
||||||
|
# "行く" -> [(True, "行"), (False, "く")]
|
||||||
|
|
||||||
|
# 2. 読みの分配
|
||||||
|
kana_len = len(reading_kana)
|
||||||
|
|
||||||
|
# 初期割当: 各ブロックの文字数に比例
|
||||||
|
allocations = [len(block_text) for _, block_text in blocks]
|
||||||
|
allocated_total = sum(allocations)
|
||||||
|
remaining = kana_len - allocated_total
|
||||||
|
|
||||||
|
# 3. 余った読みの分配(漢字ブロック優先)
|
||||||
|
if remaining > 0:
|
||||||
|
# まず漢字ブロックに分配
|
||||||
|
for i, (is_kanji, _) in enumerate(blocks):
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
if is_kanji:
|
||||||
|
allocations[i] += 1
|
||||||
|
remaining -= 1
|
||||||
|
|
||||||
|
# まだ余りがある場合は左から順に分配
|
||||||
|
i = 0
|
||||||
|
while remaining > 0 and len(blocks) > 0:
|
||||||
|
allocations[i] += 1
|
||||||
|
remaining -= 1
|
||||||
|
i = (i + 1) % len(blocks)
|
||||||
|
|
||||||
|
# 4. 読みが不足している場合は右から削減
|
||||||
|
if remaining < 0:
|
||||||
|
need_to_remove = -remaining
|
||||||
|
i = len(blocks) - 1
|
||||||
|
|
||||||
|
while need_to_remove > 0 and i >= 0:
|
||||||
|
can_remove = max(0, allocations[i] - 1)
|
||||||
|
remove_amount = min(can_remove, need_to_remove)
|
||||||
|
allocations[i] -= remove_amount
|
||||||
|
need_to_remove -= remove_amount
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
# 5. 最終的な読み分配
|
||||||
|
pos = 0
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for (is_kanji, block_text), allocation in zip(blocks, allocations):
|
||||||
|
block_reading = reading_kana[pos:pos + allocation]
|
||||||
|
pos += allocation
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"orig": block_text,
|
||||||
|
"kana": block_reading,
|
||||||
|
"hira": Transliterator.kata_to_hira(block_reading),
|
||||||
|
"hepburn": katakana_to_hepburn(block_reading, use_macron)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
## 並行処理・スレッドセーフティ
|
||||||
|
|
||||||
|
### ロック機構
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ThreadSafeUsage:
|
||||||
|
"""スレッドセーフな使用例"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.transliterator = Transliterator()
|
||||||
|
|
||||||
|
def process_texts_concurrently(self, texts):
|
||||||
|
"""複数テキストの並行処理"""
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
def process_single(text):
|
||||||
|
return self.transliterator.analyze(text)
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
||||||
|
# 内部のロック機構により安全に並行実行
|
||||||
|
futures = [executor.submit(process_single, text) for text in texts]
|
||||||
|
results = [f.result() for f in futures]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
processor = ThreadSafeUsage()
|
||||||
|
texts = ["東京に行く", "大阪で食事", "名古屋を観光", "福岡に宿泊"]
|
||||||
|
results = processor.process_texts_concurrently(texts)
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
print(f"テキスト{i+1}: {texts[i]}")
|
||||||
|
for token in result:
|
||||||
|
print(f" {token['orig']} -> {token['hepburn']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### パフォーマンス考慮事項
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 大量テキスト処理のベストプラクティス
|
||||||
|
def efficient_batch_processing(texts, batch_size=100):
|
||||||
|
"""効率的なバッチ処理"""
|
||||||
|
|
||||||
|
transliterator = Transliterator()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i in range(0, len(texts), batch_size):
|
||||||
|
batch = texts[i:i + batch_size]
|
||||||
|
|
||||||
|
batch_results = []
|
||||||
|
for text in batch:
|
||||||
|
# 各テキストを個別に処理(ロック制御あり)
|
||||||
|
result = transliterator.analyze(text)
|
||||||
|
batch_results.append(result)
|
||||||
|
|
||||||
|
results.extend(batch_results)
|
||||||
|
|
||||||
|
# バッチ間で少し休憩(メモリ管理)
|
||||||
|
if len(results) % 1000 == 0:
|
||||||
|
print(f"処理済み: {len(results)} テキスト")
|
||||||
|
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 例外処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_analyze(text):
|
||||||
|
"""安全な解析処理"""
|
||||||
|
|
||||||
|
transliterator = Transliterator()
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = transliterator.analyze(text)
|
||||||
|
return results, None
|
||||||
|
|
||||||
|
except RuntimeError as e:
|
||||||
|
if "Already borrowed" in str(e):
|
||||||
|
# SudachiPyの並行アクセスエラー
|
||||||
|
print("並行アクセスエラーが発生しました。リトライします。")
|
||||||
|
return None, "RETRY_NEEDED"
|
||||||
|
else:
|
||||||
|
print(f"実行時エラー: {e}")
|
||||||
|
return None, "RUNTIME_ERROR"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"予期しないエラー: {e}")
|
||||||
|
return None, "UNKNOWN_ERROR"
|
||||||
|
|
||||||
|
# 使用例(リトライ機構付き)
|
||||||
|
def analyze_with_retry(text, max_retries=3):
|
||||||
|
"""リトライ機構付き解析"""
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
results, error = safe_analyze(text)
|
||||||
|
|
||||||
|
if results is not None:
|
||||||
|
return results
|
||||||
|
|
||||||
|
if error == "RETRY_NEEDED":
|
||||||
|
print(f"リトライ {attempt + 1}/{max_retries}")
|
||||||
|
import time
|
||||||
|
time.sleep(0.1 * (attempt + 1)) # 指数バックオフ
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 全てのリトライが失敗した場合のフォールバック
|
||||||
|
print("解析に失敗しました。フォールバック処理を実行します。")
|
||||||
|
return [{"orig": text, "kana": text, "hira": text, "hepburn": text}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定・カスタマイズ
|
||||||
|
|
||||||
|
### SudachiPy設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
# カスタムSudachiPy設定での初期化
|
||||||
|
class CustomTransliterator(Transliterator):
|
||||||
|
def __init__(self, dict_type="full", split_mode="C"):
|
||||||
|
"""カスタム設定での初期化"""
|
||||||
|
|
||||||
|
# 辞書タイプの選択
|
||||||
|
dict_types = {
|
||||||
|
"small": dictionary.Dictionary.create(dict_type="small"),
|
||||||
|
"core": dictionary.Dictionary.create(dict_type="core"),
|
||||||
|
"full": dictionary.Dictionary.create(dict_type="full")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tokenizer_obj = dict_types.get(dict_type, dict_types["full"])
|
||||||
|
|
||||||
|
# 分割モードの選択
|
||||||
|
split_modes = {
|
||||||
|
"A": tokenizer.Tokenizer.SplitMode.A, # 短い単位
|
||||||
|
"B": tokenizer.Tokenizer.SplitMode.B, # 中間単位
|
||||||
|
"C": tokenizer.Tokenizer.SplitMode.C # 長い単位(デフォルト)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mode = split_modes.get(split_mode, split_modes["C"])
|
||||||
|
self._tokenizer_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
# 短い単位での分割を使用
|
||||||
|
small_unit_transliterator = CustomTransliterator(dict_type="core", split_mode="A")
|
||||||
|
|
||||||
|
text = "取り敢えず検索してみる"
|
||||||
|
results = small_unit_transliterator.analyze(text)
|
||||||
|
|
||||||
|
for token in results:
|
||||||
|
print(f"{token['orig']} -> {token['hepburn']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## テスト・デバッグ
|
||||||
|
|
||||||
|
### 包括的テストセット
|
||||||
|
|
||||||
|
```python
|
||||||
|
def run_comprehensive_tests():
|
||||||
|
"""包括的な機能テスト"""
|
||||||
|
|
||||||
|
transliterator = Transliterator()
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# 基本的な文章
|
||||||
|
("向こうへ行く", "向こう", "ムコウ"),
|
||||||
|
("美しい花", "美しい", "ウツクシイ"),
|
||||||
|
|
||||||
|
# 文脈依存
|
||||||
|
("何度も", "何", "ナン"),
|
||||||
|
("何が", "何", "ナニ"),
|
||||||
|
|
||||||
|
# 外来語
|
||||||
|
("パーティー", "パーティー", "パーティー"),
|
||||||
|
("コンピューター", "コンピューター", "コンピューター"),
|
||||||
|
|
||||||
|
# 漢字・送り仮名
|
||||||
|
("取り敢えず", "取り", "トリ"),
|
||||||
|
("見知らぬ", "見知ら", "ミシラ"),
|
||||||
|
|
||||||
|
# 記号・英数字
|
||||||
|
("ID:12345", "ID", "ID"),
|
||||||
|
("SessionIDを取得", "SessionID", "SessionID")
|
||||||
|
]
|
||||||
|
|
||||||
|
for text, target_orig, expected_kana in test_cases:
|
||||||
|
results = transliterator.analyze(text)
|
||||||
|
|
||||||
|
# 対象トークンを検索
|
||||||
|
target_token = None
|
||||||
|
for token in results:
|
||||||
|
if token['orig'] == target_orig:
|
||||||
|
target_token = token
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_token:
|
||||||
|
actual_kana = target_token['kana']
|
||||||
|
status = "✓" if actual_kana == expected_kana else "✗"
|
||||||
|
print(f"{status} {text}: {target_orig} -> {actual_kana} (期待値: {expected_kana})")
|
||||||
|
else:
|
||||||
|
print(f"✗ {text}: トークン '{target_orig}' が見つかりません")
|
||||||
|
|
||||||
|
run_comprehensive_tests()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `sudachipy`: 形態素解析エンジン
|
||||||
|
- `threading`: 並行制御
|
||||||
|
- `typing`: 型ヒント
|
||||||
|
|
||||||
|
### 内部モジュール依存
|
||||||
|
- `transliteration_kana_to_hepburn`: ヘボン式変換
|
||||||
|
- `transliteration_context_rules`: 文脈依存ルール
|
||||||
|
|
||||||
|
### システム要件
|
||||||
|
- Python 3.7以上
|
||||||
|
- SudachiPy辞書ファイル(自動ダウンロード)
|
||||||
|
- 十分なメモリ(辞書読み込み用)
|
||||||
|
|
||||||
|
## 注意事項・制限
|
||||||
|
|
||||||
|
### 処理精度の制限
|
||||||
|
- 形態素解析結果に依存
|
||||||
|
- 未知語・固有名詞は読み推定
|
||||||
|
- 文脈によっては不正確な分割
|
||||||
|
|
||||||
|
### パフォーマンス制限
|
||||||
|
- 初回実行時の辞書読み込み時間
|
||||||
|
- 大量テキスト処理時のメモリ使用量
|
||||||
|
- 並行アクセス時のロック待機
|
||||||
|
|
||||||
|
### 出力形式の制限
|
||||||
|
```python
|
||||||
|
# 現在サポートしていない機能
|
||||||
|
unsupported_features = [
|
||||||
|
"アクセント記号(音調)",
|
||||||
|
"方言・古語の特殊読み",
|
||||||
|
"人名・地名の特殊読み",
|
||||||
|
"外国語の音写(中国語・韓国語等)",
|
||||||
|
"カスタム読み辞書",
|
||||||
|
"品詞情報の出力"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `transliteration_kana_to_hepburn.py`: ヘボン式変換処理
|
||||||
|
- `transliteration_context_rules.py`: 文脈依存ルール適用
|
||||||
|
- `config.py`: システム設定管理
|
||||||
|
- `utils.py`: ユーティリティ関数
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- カスタム読み辞書対応
|
||||||
|
- より高精度な文脈解析
|
||||||
|
- 他言語音写システムとの統合
|
||||||
|
- リアルタイム処理最適化
|
||||||
|
- 分散処理対応
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# utils.py - ユーティリティ関数モジュール
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
VRCTアプリケーション全体で使用される汎用的なユーティリティ関数を提供するモジュールです。データ検証、ネットワーク接続確認、計算デバイス管理、ログ機能などの共通機能を集約しています。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### データ検証機能
|
||||||
|
|
||||||
|
- 辞書構造の型安全な検証
|
||||||
|
- IPアドレス形式の検証
|
||||||
|
- WebSocketサーバーの可用性確認
|
||||||
|
|
||||||
|
### システム情報取得
|
||||||
|
|
||||||
|
- 利用可能な計算デバイス(CPU/CUDA)の一覧取得
|
||||||
|
- 最適な計算タイプの自動選択
|
||||||
|
- デバイス固有の制約対応
|
||||||
|
|
||||||
|
### ログ機能
|
||||||
|
|
||||||
|
- 構造化ログの出力
|
||||||
|
- ローテーションログファイル管理
|
||||||
|
- エラーログとプロセスログの分離
|
||||||
|
|
||||||
|
### ネットワーク機能
|
||||||
|
|
||||||
|
- インターネット接続状態の確認
|
||||||
|
- Base64エンコード/デコード処理
|
||||||
|
|
||||||
|
## 主要関数
|
||||||
|
|
||||||
|
### データ検証
|
||||||
|
|
||||||
|
```python
|
||||||
|
validateDictStructure(data: dict, structure: dict) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- 辞書とその期待される構造が完全に一致するかを判別
|
||||||
|
- 入れ子構造にも対応
|
||||||
|
- 型安全性を保証
|
||||||
|
|
||||||
|
### ネットワーク関連
|
||||||
|
|
||||||
|
```python
|
||||||
|
isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- 指定URLへの接続可能性をチェック
|
||||||
|
- タイムアウト設定可能
|
||||||
|
|
||||||
|
```python
|
||||||
|
isAvailableWebSocketServer(host: str, port: int) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- WebSocketサーバーのバインド可能性を確認
|
||||||
|
|
||||||
|
```python
|
||||||
|
isValidIpAddress(ip_address: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- IPv4/IPv6アドレスの有効性を検証
|
||||||
|
|
||||||
|
### 計算デバイス管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
getComputeDeviceList() -> List[Dict[str, Any]]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 利用可能なCPU/CUDA計算デバイスの一覧を取得
|
||||||
|
- 各デバイスの計算タイプを含む詳細情報を提供
|
||||||
|
|
||||||
|
```python
|
||||||
|
getBestComputeType(device: str, device_index: int) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
- デバイスに最適な計算タイプを自動選択
|
||||||
|
- GPU固有の制約を考慮(GTX、RTX、Tesla、A100、Quadro等)
|
||||||
|
|
||||||
|
### ログ機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
setupLogger(name: str, log_file: str, level: int = logging.INFO) -> logging.Logger
|
||||||
|
```
|
||||||
|
|
||||||
|
- ローテーション機能付きログの設定
|
||||||
|
- 10MBサイズでのローテーション
|
||||||
|
- UTF-8エンコード対応
|
||||||
|
|
||||||
|
```python
|
||||||
|
printLog(log: str, data: Any = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 構造化プロセスログの出力
|
||||||
|
- JSON形式での標準出力
|
||||||
|
|
||||||
|
```python
|
||||||
|
printResponse(status: int, endpoint: str, result: Any = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- APIレスポンスの構造化出力
|
||||||
|
- シリアライゼーションエラーの安全な処理
|
||||||
|
|
||||||
|
```python
|
||||||
|
errorLogging() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 例外トレースバックのログ記録
|
||||||
|
- フォールバック機能付き
|
||||||
|
|
||||||
|
### その他のユーティリティ
|
||||||
|
|
||||||
|
```python
|
||||||
|
encodeBase64(data: str) -> Dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Base64エンコード済みJSON文字列のデコード
|
||||||
|
- エラー処理付き
|
||||||
|
|
||||||
|
```python
|
||||||
|
removeLog() -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
- プロセスログファイルの初期化
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import validateDictStructure, isConnectedNetwork, printLog
|
||||||
|
|
||||||
|
# 辞書構造の検証
|
||||||
|
expected_structure = {"name": str, "age": int}
|
||||||
|
data = {"name": "test", "age": 25}
|
||||||
|
is_valid = validateDictStructure(data, expected_structure)
|
||||||
|
|
||||||
|
# ネットワーク接続確認
|
||||||
|
is_connected = isConnectedNetwork()
|
||||||
|
|
||||||
|
# ログ出力
|
||||||
|
printLog("処理開始", {"user_id": 123})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 計算デバイス管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import getComputeDeviceList, getBestComputeType
|
||||||
|
|
||||||
|
# 利用可能デバイス一覧
|
||||||
|
devices = getComputeDeviceList()
|
||||||
|
|
||||||
|
# 最適な計算タイプ選択
|
||||||
|
compute_type = getBestComputeType("cuda", 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ログ設定
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import setupLogger, errorLogging
|
||||||
|
|
||||||
|
# カスタムログの設定
|
||||||
|
logger = setupLogger("my_module", "my_module.log")
|
||||||
|
logger.info("処理完了")
|
||||||
|
|
||||||
|
# エラーログ記録
|
||||||
|
try:
|
||||||
|
# 何らかの処理
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
|
||||||
|
- `json`: JSON処理
|
||||||
|
- `logging`: ログ機能
|
||||||
|
- `requests`: HTTP通信
|
||||||
|
- `ipaddress`: IPアドレス検証
|
||||||
|
- `socket`: ソケット通信
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
|
||||||
|
- `torch`: CUDA計算デバイス情報取得
|
||||||
|
- `ctranslate2`: 計算タイプ情報取得
|
||||||
|
|
||||||
|
## デバイス別計算タイプ制約
|
||||||
|
|
||||||
|
### GTXシリーズ
|
||||||
|
- サポート: `float32`のみ
|
||||||
|
- 理由: 古いアーキテクチャによる制約
|
||||||
|
|
||||||
|
### RTX/Tesla/A100/Quadroシリーズ
|
||||||
|
- サポート: フル機能
|
||||||
|
- 優先順位: `int8_bfloat16` > `int8_float16` > `int8` > `bfloat16` > `float16` > `int8_float32` > `float32`
|
||||||
|
|
||||||
|
### CPU
|
||||||
|
- サポート: 全計算タイプ(ハードウェア依存)
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- すべての関数は例外安全性を考慮
|
||||||
|
- オプション依存関係の欠如に対する適切なフォールバック
|
||||||
|
- ログ機能は多段階のフェールセーフ機構を持つ
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 計算デバイス情報取得は初回実行時にやや時間がかかる場合がある
|
||||||
|
- ログローテーションは10MBサイズで自動実行
|
||||||
|
- ネットワーク接続確認はデフォルト3秒のタイムアウト設定
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
# watchdog.py - 軽量監視システム
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
タイムアウトベースの軽量監視(ウォッチドッグ)システムです。定期的な"餌やり"(feed)により正常動作を確認し、指定時間内に餌やりがない場合にコールバック関数を実行する単純で効果的な監視機構を提供します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### タイムアウト監視
|
||||||
|
- 最後の餌やり時刻からの経過時間監視
|
||||||
|
- 設定可能なタイムアウト閾値
|
||||||
|
- タイムアウト時の自動コールバック実行
|
||||||
|
|
||||||
|
### 柔軟な実行モード
|
||||||
|
- 単発チェック(手動呼び出し)
|
||||||
|
- バックグラウンドスレッド実行
|
||||||
|
- カスタムチェック間隔設定
|
||||||
|
|
||||||
|
### 防御的設計
|
||||||
|
- コールバック例外の隔離処理
|
||||||
|
- スレッドセーフな制御機構
|
||||||
|
- 適切なリソース管理
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### Watchdog クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Watchdog:
|
||||||
|
def __init__(self, timeout: int = 60, interval: int = 20) -> None:
|
||||||
|
self.timeout: int # タイムアウト秒数
|
||||||
|
self.interval: int # チェック間隔秒数
|
||||||
|
self.last_feed_time: float # 最後の餌やり時刻
|
||||||
|
self.callback: Optional[Callable] # タイムアウト時コールバック
|
||||||
|
self._thread: Optional[Thread] # バックグラウンドスレッド
|
||||||
|
self._stop_event: Optional[Event] # 停止イベント
|
||||||
|
```
|
||||||
|
|
||||||
|
軽量ウォッチドッグの中核クラス
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **timeout**: 餌やりなしでタイムアウトするまでの秒数
|
||||||
|
- **interval**: 監視チェックの推奨間隔秒数
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### 基本制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
def feed(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
ウォッチドッグに餌やりを行い、タイマーをリセット
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setCallback(self, callback: Callable[[], None]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
タイムアウト時に実行するコールバック関数を設定
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **callback**: 引数なしの呼び出し可能オブジェクト
|
||||||
|
|
||||||
|
### 監視実行
|
||||||
|
|
||||||
|
```python
|
||||||
|
def start(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
単発のウォッチドッグチェックを実行し、間隔秒数だけスリープ
|
||||||
|
|
||||||
|
```python
|
||||||
|
def start_in_thread(self, daemon: bool = True) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
バックグラウンドスレッドでウォッチドッグを開始
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **daemon**: デーモンスレッドとして実行するかのフラグ
|
||||||
|
|
||||||
|
```python
|
||||||
|
def stop(self, timeout: Optional[float] = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
バックグラウンドスレッドを停止
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **timeout**: スレッド終了待機のタイムアウト秒数
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な監視システム
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.watchdog.watchdog import Watchdog
|
||||||
|
import time
|
||||||
|
|
||||||
|
def on_timeout():
|
||||||
|
"""タイムアウト時の処理"""
|
||||||
|
print("警告: システムの応答がありません!")
|
||||||
|
# ログ出力、アラート送信、復旧処理等
|
||||||
|
|
||||||
|
# ウォッチドッグの初期化
|
||||||
|
watchdog = Watchdog(timeout=30, interval=10) # 30秒でタイムアウト、10秒間隔
|
||||||
|
watchdog.setCallback(on_timeout)
|
||||||
|
|
||||||
|
# バックグラウンドで監視開始
|
||||||
|
watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
# メインプロセスのシミュレーション
|
||||||
|
for i in range(10):
|
||||||
|
print(f"処理中... {i}")
|
||||||
|
|
||||||
|
# 正常な処理では定期的に餌やり
|
||||||
|
if i % 3 == 0: # 3回に1回餌やり
|
||||||
|
watchdog.feed()
|
||||||
|
print("ウォッチドッグに餌やりしました")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 監視停止
|
||||||
|
watchdog.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手動チェックモード
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 手動でウォッチドッグをチェック
|
||||||
|
def manual_monitoring_example():
|
||||||
|
watchdog = Watchdog(timeout=60, interval=5)
|
||||||
|
|
||||||
|
def system_failure_handler():
|
||||||
|
print("システム障害を検出しました")
|
||||||
|
# 復旧処理、通知等
|
||||||
|
|
||||||
|
watchdog.setCallback(system_failure_handler)
|
||||||
|
|
||||||
|
# メインループ内で定期チェック
|
||||||
|
while True:
|
||||||
|
# 何らかの重要な処理
|
||||||
|
process_critical_work()
|
||||||
|
|
||||||
|
# 処理が正常なら餌やり
|
||||||
|
if is_system_healthy():
|
||||||
|
watchdog.feed()
|
||||||
|
|
||||||
|
# 監視チェック実行(5秒間隔でスリープ)
|
||||||
|
watchdog.start()
|
||||||
|
|
||||||
|
def process_critical_work():
|
||||||
|
"""重要な処理のシミュレーション"""
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def is_system_healthy():
|
||||||
|
"""システム正常性チェックのシミュレーション"""
|
||||||
|
import random
|
||||||
|
return random.random() > 0.2 # 80%の確率で正常
|
||||||
|
|
||||||
|
# manual_monitoring_example()
|
||||||
|
```
|
||||||
|
|
||||||
|
### プロセス監視システム
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ProcessMonitor:
|
||||||
|
"""外部プロセス監視システム"""
|
||||||
|
|
||||||
|
def __init__(self, process_name, check_interval=30):
|
||||||
|
self.process_name = process_name
|
||||||
|
self.watchdog = Watchdog(timeout=60, interval=check_interval)
|
||||||
|
self.watchdog.setCallback(self.on_process_timeout)
|
||||||
|
self.monitoring = False
|
||||||
|
|
||||||
|
def on_process_timeout(self):
|
||||||
|
"""プロセス応答タイムアウト時の処理"""
|
||||||
|
print(f"警告: プロセス {self.process_name} が応答しません")
|
||||||
|
|
||||||
|
# プロセス存在確認
|
||||||
|
if self.is_process_running():
|
||||||
|
print("プロセスは実行中ですが応答なし。再起動を試行します。")
|
||||||
|
self.restart_process()
|
||||||
|
else:
|
||||||
|
print("プロセスが停止しています。再起動します。")
|
||||||
|
self.start_process()
|
||||||
|
|
||||||
|
def is_process_running(self):
|
||||||
|
"""プロセス実行状態確認"""
|
||||||
|
import psutil
|
||||||
|
for proc in psutil.process_iter(['name']):
|
||||||
|
if proc.info['name'] == self.process_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_process(self):
|
||||||
|
"""プロセス起動"""
|
||||||
|
print(f"プロセス {self.process_name} を起動中...")
|
||||||
|
# 実際の起動処理
|
||||||
|
|
||||||
|
def restart_process(self):
|
||||||
|
"""プロセス再起動"""
|
||||||
|
print(f"プロセス {self.process_name} を再起動中...")
|
||||||
|
# 実際の再起動処理
|
||||||
|
|
||||||
|
def feed_watchdog(self):
|
||||||
|
"""外部から呼び出される餌やりメソッド"""
|
||||||
|
self.watchdog.feed()
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""監視開始"""
|
||||||
|
self.monitoring = True
|
||||||
|
self.watchdog.start_in_thread(daemon=True)
|
||||||
|
print(f"プロセス {self.process_name} の監視を開始しました")
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""監視停止"""
|
||||||
|
self.monitoring = False
|
||||||
|
self.watchdog.stop()
|
||||||
|
print(f"プロセス {self.process_name} の監視を停止しました")
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
vrchat_monitor = ProcessMonitor("VRChat.exe", check_interval=15)
|
||||||
|
vrchat_monitor.start_monitoring()
|
||||||
|
|
||||||
|
# VRChatプロセスが正常に動作している時の餌やり
|
||||||
|
# (実際にはVRChatからのOSC通信等で判定)
|
||||||
|
for _ in range(20):
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if vrchat_monitor.is_process_running():
|
||||||
|
vrchat_monitor.feed_watchdog()
|
||||||
|
print("VRChat正常動作確認")
|
||||||
|
|
||||||
|
vrchat_monitor.stop_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ネットワーク監視システム
|
||||||
|
|
||||||
|
```python
|
||||||
|
class NetworkWatchdog:
|
||||||
|
"""ネットワーク接続監視"""
|
||||||
|
|
||||||
|
def __init__(self, target_host="8.8.8.8", timeout=45):
|
||||||
|
self.target_host = target_host
|
||||||
|
self.watchdog = Watchdog(timeout=timeout, interval=15)
|
||||||
|
self.watchdog.setCallback(self.on_network_timeout)
|
||||||
|
self.last_ping_success = True
|
||||||
|
|
||||||
|
def on_network_timeout(self):
|
||||||
|
"""ネットワークタイムアウト処理"""
|
||||||
|
print("ネットワーク接続に問題があります")
|
||||||
|
|
||||||
|
# 複数ホストでの確認
|
||||||
|
test_hosts = ["8.8.8.8", "1.1.1.1", "google.com"]
|
||||||
|
|
||||||
|
for host in test_hosts:
|
||||||
|
if self.ping_host(host):
|
||||||
|
print(f"{host} への接続は正常です")
|
||||||
|
self.watchdog.feed() # 1つでも成功したら復旧とみなす
|
||||||
|
return
|
||||||
|
|
||||||
|
print("すべてのホストへの接続に失敗。ネットワーク設定を確認してください。")
|
||||||
|
self.handle_network_failure()
|
||||||
|
|
||||||
|
def ping_host(self, host):
|
||||||
|
"""ホストへのping確認"""
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
# プラットフォームに応じたpingコマンド
|
||||||
|
if platform.system().lower() == "windows":
|
||||||
|
cmd = ["ping", "-n", "1", "-w", "3000", host]
|
||||||
|
else:
|
||||||
|
cmd = ["ping", "-c", "1", "-W", "3", host]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, timeout=5)
|
||||||
|
return result.returncode == 0
|
||||||
|
except (subprocess.TimeoutExpired, Exception):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_network_failure(self):
|
||||||
|
"""ネットワーク障害時の処理"""
|
||||||
|
print("ネットワーク障害対応処理を実行中...")
|
||||||
|
# DNS設定リセット、ネットワークアダプター再起動等
|
||||||
|
|
||||||
|
def check_network_continuously(self):
|
||||||
|
"""継続的なネットワーク監視"""
|
||||||
|
self.watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if self.ping_host(self.target_host):
|
||||||
|
if not self.last_ping_success:
|
||||||
|
print("ネットワーク接続が復旧しました")
|
||||||
|
|
||||||
|
self.watchdog.feed()
|
||||||
|
self.last_ping_success = True
|
||||||
|
else:
|
||||||
|
if self.last_ping_success:
|
||||||
|
print("ネットワーク接続に問題が発生しました")
|
||||||
|
|
||||||
|
self.last_ping_success = False
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
network_monitor = NetworkWatchdog(target_host="google.com", timeout=60)
|
||||||
|
|
||||||
|
# バックグラウンドでネットワーク監視
|
||||||
|
import threading
|
||||||
|
monitor_thread = threading.Thread(target=network_monitor.check_network_continuously, daemon=True)
|
||||||
|
monitor_thread.start()
|
||||||
|
|
||||||
|
# メインプログラムの実行
|
||||||
|
print("ネットワーク監視システム開始...")
|
||||||
|
time.sleep(120) # 2分間監視
|
||||||
|
|
||||||
|
network_monitor.watchdog.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
### システムリソース監視
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SystemResourceWatchdog:
|
||||||
|
"""システムリソース監視"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.cpu_watchdog = Watchdog(timeout=300, interval=30) # CPU使用率監視
|
||||||
|
self.memory_watchdog = Watchdog(timeout=180, interval=20) # メモリ監視
|
||||||
|
|
||||||
|
self.cpu_watchdog.setCallback(self.on_cpu_overload)
|
||||||
|
self.memory_watchdog.setCallback(self.on_memory_pressure)
|
||||||
|
|
||||||
|
self.cpu_threshold = 80.0 # CPU使用率閾値(%)
|
||||||
|
self.memory_threshold = 85.0 # メモリ使用率閾値(%)
|
||||||
|
|
||||||
|
def on_cpu_overload(self):
|
||||||
|
"""CPU過負荷時の処理"""
|
||||||
|
print("警告: CPU使用率が長時間高い状態です")
|
||||||
|
self.optimize_cpu_usage()
|
||||||
|
|
||||||
|
def on_memory_pressure(self):
|
||||||
|
"""メモリ圧迫時の処理"""
|
||||||
|
print("警告: メモリ使用率が危険なレベルです")
|
||||||
|
self.free_memory()
|
||||||
|
|
||||||
|
def get_cpu_usage(self):
|
||||||
|
"""CPU使用率取得"""
|
||||||
|
import psutil
|
||||||
|
return psutil.cpu_percent(interval=1)
|
||||||
|
|
||||||
|
def get_memory_usage(self):
|
||||||
|
"""メモリ使用率取得"""
|
||||||
|
import psutil
|
||||||
|
return psutil.virtual_memory().percent
|
||||||
|
|
||||||
|
def optimize_cpu_usage(self):
|
||||||
|
"""CPU使用率最適化"""
|
||||||
|
print("CPU最適化処理を実行中...")
|
||||||
|
# 低優先度プロセスの特定・制限
|
||||||
|
# 不要なバックグラウンドタスクの停止等
|
||||||
|
|
||||||
|
def free_memory(self):
|
||||||
|
"""メモリ解放処理"""
|
||||||
|
print("メモリ解放処理を実行中...")
|
||||||
|
# ガベージコレクション実行
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
# キャッシュクリア等
|
||||||
|
|
||||||
|
def monitor_resources(self):
|
||||||
|
"""リソース監視メインループ"""
|
||||||
|
self.cpu_watchdog.start_in_thread(daemon=True)
|
||||||
|
self.memory_watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# CPU使用率チェック
|
||||||
|
cpu_usage = self.get_cpu_usage()
|
||||||
|
if cpu_usage < self.cpu_threshold:
|
||||||
|
self.cpu_watchdog.feed()
|
||||||
|
|
||||||
|
# メモリ使用率チェック
|
||||||
|
memory_usage = self.get_memory_usage()
|
||||||
|
if memory_usage < self.memory_threshold:
|
||||||
|
self.memory_watchdog.feed()
|
||||||
|
|
||||||
|
print(f"CPU: {cpu_usage:.1f}%, メモリ: {memory_usage:.1f}%")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""監視停止"""
|
||||||
|
self.cpu_watchdog.stop()
|
||||||
|
self.memory_watchdog.stop()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
resource_monitor = SystemResourceWatchdog()
|
||||||
|
|
||||||
|
# リソース監視開始
|
||||||
|
import threading
|
||||||
|
resource_thread = threading.Thread(target=resource_monitor.monitor_resources, daemon=True)
|
||||||
|
resource_thread.start()
|
||||||
|
|
||||||
|
# しばらく監視
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
resource_monitor.stop_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高度な使用パターン
|
||||||
|
|
||||||
|
### 多段階監視システム
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MultilevelWatchdog:
|
||||||
|
"""多段階警告レベル対応ウォッチドッグ"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 異なるタイムアウトレベル
|
||||||
|
self.warning_watchdog = Watchdog(timeout=30, interval=10) # 警告レベル
|
||||||
|
self.critical_watchdog = Watchdog(timeout=60, interval=15) # 危険レベル
|
||||||
|
self.emergency_watchdog = Watchdog(timeout=120, interval=20) # 緊急レベル
|
||||||
|
|
||||||
|
# 各レベルのコールバック設定
|
||||||
|
self.warning_watchdog.setCallback(self.on_warning)
|
||||||
|
self.critical_watchdog.setCallback(self.on_critical)
|
||||||
|
self.emergency_watchdog.setCallback(self.on_emergency)
|
||||||
|
|
||||||
|
self.alert_level = "normal"
|
||||||
|
|
||||||
|
def on_warning(self):
|
||||||
|
"""警告レベルのコールバック"""
|
||||||
|
self.alert_level = "warning"
|
||||||
|
print("⚠️ 警告: システムの応答が遅くなっています")
|
||||||
|
# 軽度の対応処理
|
||||||
|
|
||||||
|
def on_critical(self):
|
||||||
|
"""危険レベルのコールバック"""
|
||||||
|
self.alert_level = "critical"
|
||||||
|
print("🔴 危険: システムの重大な問題を検出")
|
||||||
|
# 中程度の復旧処理
|
||||||
|
|
||||||
|
def on_emergency(self):
|
||||||
|
"""緊急レベルのコールバック"""
|
||||||
|
self.alert_level = "emergency"
|
||||||
|
print("🚨 緊急: システムが完全に応答停止")
|
||||||
|
# 強制復旧・再起動処理
|
||||||
|
|
||||||
|
def feed_all(self):
|
||||||
|
"""すべてのウォッチドッグに餌やり"""
|
||||||
|
self.warning_watchdog.feed()
|
||||||
|
self.critical_watchdog.feed()
|
||||||
|
self.emergency_watchdog.feed()
|
||||||
|
|
||||||
|
if self.alert_level != "normal":
|
||||||
|
print("✅ システム復旧確認")
|
||||||
|
self.alert_level = "normal"
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""多段階監視開始"""
|
||||||
|
self.warning_watchdog.start_in_thread(daemon=True)
|
||||||
|
self.critical_watchdog.start_in_thread(daemon=True)
|
||||||
|
self.emergency_watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""監視停止"""
|
||||||
|
self.warning_watchdog.stop()
|
||||||
|
self.critical_watchdog.stop()
|
||||||
|
self.emergency_watchdog.stop()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
multilevel_monitor = MultilevelWatchdog()
|
||||||
|
multilevel_monitor.start_monitoring()
|
||||||
|
|
||||||
|
# 正常時は定期餌やり、異常時は餌やり停止で段階的警告
|
||||||
|
for i in range(30):
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 時々餌やりを忘れるシミュレーション
|
||||||
|
if i % 7 != 0: # 7回に1回は餌やりしない
|
||||||
|
multilevel_monitor.feed_all()
|
||||||
|
|
||||||
|
print(f"現在の警告レベル: {multilevel_monitor.alert_level}")
|
||||||
|
|
||||||
|
multilevel_monitor.stop_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング・防御機構
|
||||||
|
|
||||||
|
### 例外安全性
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_callback_example():
|
||||||
|
"""安全なコールバック実装例"""
|
||||||
|
|
||||||
|
def potentially_failing_callback():
|
||||||
|
"""例外を起こす可能性があるコールバック"""
|
||||||
|
print("重要な処理を実行中...")
|
||||||
|
|
||||||
|
# 何らかの理由で例外が発生
|
||||||
|
raise RuntimeError("処理中にエラーが発生しました")
|
||||||
|
|
||||||
|
# ウォッチドッグは例外を隔離するため、システム全体は継続動作
|
||||||
|
watchdog = Watchdog(timeout=10, interval=5)
|
||||||
|
watchdog.setCallback(potentially_failing_callback)
|
||||||
|
|
||||||
|
# エラーが発生してもウォッチドッグ自体は停止しない
|
||||||
|
watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
# 通常の処理継続
|
||||||
|
for i in range(5):
|
||||||
|
time.sleep(3)
|
||||||
|
watchdog.feed()
|
||||||
|
print(f"メイン処理継続中: {i}")
|
||||||
|
|
||||||
|
watchdog.stop()
|
||||||
|
print("ウォッチドッグは例外に関係なく正常に停止しました")
|
||||||
|
|
||||||
|
safe_callback_example()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 堅牢なスレッド制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RobustWatchdog:
|
||||||
|
"""より堅牢なウォッチドッグ実装"""
|
||||||
|
|
||||||
|
def __init__(self, timeout=60, interval=20):
|
||||||
|
self.base_watchdog = Watchdog(timeout, interval)
|
||||||
|
self.restart_count = 0
|
||||||
|
self.max_restarts = 3
|
||||||
|
|
||||||
|
def robust_callback(self):
|
||||||
|
"""自動復旧機能付きコールバック"""
|
||||||
|
try:
|
||||||
|
print(f"問題検出(再起動回数: {self.restart_count}/{self.max_restarts})")
|
||||||
|
|
||||||
|
if self.restart_count < self.max_restarts:
|
||||||
|
self.attempt_recovery()
|
||||||
|
self.restart_count += 1
|
||||||
|
else:
|
||||||
|
print("最大再起動回数に達しました。管理者に連絡してください。")
|
||||||
|
self.emergency_shutdown()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"復旧処理中にエラー: {e}")
|
||||||
|
|
||||||
|
def attempt_recovery(self):
|
||||||
|
"""復旧処理の試行"""
|
||||||
|
print("自動復旧処理を実行中...")
|
||||||
|
time.sleep(2) # 復旧処理のシミュレーション
|
||||||
|
|
||||||
|
# 復旧成功時はカウンターリセット
|
||||||
|
if self.check_system_health():
|
||||||
|
self.restart_count = 0
|
||||||
|
self.base_watchdog.feed()
|
||||||
|
print("復旧成功")
|
||||||
|
else:
|
||||||
|
print("復旧失敗")
|
||||||
|
|
||||||
|
def check_system_health(self):
|
||||||
|
"""システム正常性確認"""
|
||||||
|
# 実際のヘルスチェックロジック
|
||||||
|
import random
|
||||||
|
return random.random() > 0.3 # 70%成功率
|
||||||
|
|
||||||
|
def emergency_shutdown(self):
|
||||||
|
"""緊急停止処理"""
|
||||||
|
print("緊急停止処理を実行します")
|
||||||
|
# 安全な停止処理
|
||||||
|
|
||||||
|
def start_robust_monitoring(self):
|
||||||
|
"""堅牢監視開始"""
|
||||||
|
self.base_watchdog.setCallback(self.robust_callback)
|
||||||
|
self.base_watchdog.start_in_thread(daemon=True)
|
||||||
|
|
||||||
|
def feed(self):
|
||||||
|
"""餌やり(成功時はカウンターリセット)"""
|
||||||
|
self.base_watchdog.feed()
|
||||||
|
self.restart_count = 0
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""監視停止"""
|
||||||
|
self.base_watchdog.stop()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
robust_monitor = RobustWatchdog(timeout=20, interval=8)
|
||||||
|
robust_monitor.start_robust_monitoring()
|
||||||
|
|
||||||
|
# 不安定なシステムのシミュレーション
|
||||||
|
for i in range(15):
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# 時々システム異常をシミュレート
|
||||||
|
if random.random() > 0.7: # 30%の確率で異常
|
||||||
|
print("システム異常発生...")
|
||||||
|
# 餌やりしない
|
||||||
|
else:
|
||||||
|
robust_monitor.feed()
|
||||||
|
print("システム正常動作")
|
||||||
|
|
||||||
|
robust_monitor.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能・リソース考慮事項
|
||||||
|
|
||||||
|
### 軽量設計の特徴
|
||||||
|
- 最小限のメモリフットプリント
|
||||||
|
- 効率的なスレッド利用
|
||||||
|
- CPU使用量の最適化
|
||||||
|
|
||||||
|
### 推奨設定値
|
||||||
|
```python
|
||||||
|
# 用途別推奨設定
|
||||||
|
usage_patterns = {
|
||||||
|
"realtime_monitoring": {
|
||||||
|
"timeout": 10, # 10秒
|
||||||
|
"interval": 2 # 2秒間隔
|
||||||
|
},
|
||||||
|
"service_monitoring": {
|
||||||
|
"timeout": 60, # 1分
|
||||||
|
"interval": 15 # 15秒間隔
|
||||||
|
},
|
||||||
|
"batch_processing": {
|
||||||
|
"timeout": 300, # 5分
|
||||||
|
"interval": 60 # 1分間隔
|
||||||
|
},
|
||||||
|
"background_tasks": {
|
||||||
|
"timeout": 1800, # 30分
|
||||||
|
"interval": 300 # 5分間隔
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係・要件
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `threading`: スレッド制御
|
||||||
|
- `time`: 時刻管理
|
||||||
|
- 標準ライブラリのみ(外部依存なし)
|
||||||
|
|
||||||
|
### システム要件
|
||||||
|
- Python 3.7以上
|
||||||
|
- マルチスレッド対応OS
|
||||||
|
- 最小限のシステムリソース
|
||||||
|
|
||||||
|
## 注意事項・制限
|
||||||
|
|
||||||
|
### 設計上の制限
|
||||||
|
- 単純なタイムアウトベース監視のみ
|
||||||
|
- 複雑な条件判定は非対応
|
||||||
|
- ネットワーク監視等は上位層で実装
|
||||||
|
|
||||||
|
### 使用上の注意
|
||||||
|
- コールバック関数は軽量に保つ
|
||||||
|
- 長時間ブロックする処理は避ける
|
||||||
|
- 適切なタイムアウト値の設定が重要
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `threading`: スレッド管理
|
||||||
|
- `config.py`: 監視設定管理
|
||||||
|
- `utils.py`: エラーログ・ユーティリティ
|
||||||
|
- `controller.py`: 監視制御インターフェース
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- より複雑な監視条件のサポート
|
||||||
|
- 監視統計・メトリクス収集機能
|
||||||
|
- 設定可能な復旧戦略
|
||||||
|
- 分散監視システムとの連携
|
||||||
|
- Webインターフェースでの監視状態表示
|
||||||
@@ -0,0 +1,989 @@
|
|||||||
|
# websocket_server.py - WebSocket通信サーバー
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
非同期WebSocket通信を提供する包括的なサーバーシステムです。クライアント接続管理、メッセージ配信、外部スレッドからの安全な操作を統合し、VRCTアプリケーションとWebフロントエンド間のリアルタイム通信を実現します。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### 非同期WebSocket通信
|
||||||
|
- asyncio/websockets による高性能WebSocketサーバー
|
||||||
|
- 複数クライアント同時接続対応
|
||||||
|
- 自動接続・切断管理
|
||||||
|
|
||||||
|
### メッセージング機能
|
||||||
|
- リアルタイムメッセージ受信処理
|
||||||
|
- 全クライアントへのブロードキャスト配信
|
||||||
|
- カスタムメッセージハンドラー対応
|
||||||
|
|
||||||
|
### スレッド間通信
|
||||||
|
- GUI等の外部スレッドからの安全なメッセージ送信
|
||||||
|
- 非同期キューによる効率的な通信制御
|
||||||
|
- スレッドセーフな操作保証
|
||||||
|
|
||||||
|
## クラス構造
|
||||||
|
|
||||||
|
### WebSocketServer クラス
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WebSocketServer:
|
||||||
|
def __init__(self, host: str='127.0.0.1', port: int=8765):
|
||||||
|
self.host: str # サーバーホスト
|
||||||
|
self.port: int # サーバーポート
|
||||||
|
self.clients: Set[WebSocketServerProtocol] # 接続クライアント集合
|
||||||
|
self._message_handler: Optional[Callable] # メッセージハンドラー
|
||||||
|
self._loop: Optional[asyncio.AbstractEventLoop] # イベントループ
|
||||||
|
self._server: Optional[websockets.serve] # WebSocketサーバー
|
||||||
|
self._thread: Optional[threading.Thread] # サーバースレッド
|
||||||
|
self._send_queue: Optional[asyncio.Queue] # 送信キュー
|
||||||
|
self.is_running: bool # 動作状態フラグ
|
||||||
|
```
|
||||||
|
|
||||||
|
WebSocket通信の中核管理クラス
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### サーバー制御
|
||||||
|
|
||||||
|
```python
|
||||||
|
def start_server(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
WebSocketサーバーを開始(バックグラウンドスレッド)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def stop_server(self) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
WebSocketサーバーを停止・リソース解放
|
||||||
|
|
||||||
|
### メッセージハンドリング
|
||||||
|
|
||||||
|
```python
|
||||||
|
def set_message_handler(self, handler: Callable[['WebSocketServer', WebSocketServerProtocol, str], None]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
クライアントからのメッセージ受信時コールバック設定
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **handler**: メッセージハンドラー関数 `(server, websocket, message) -> None`
|
||||||
|
|
||||||
|
### メッセージ送信
|
||||||
|
|
||||||
|
```python
|
||||||
|
def send(self, message: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
外部スレッドから安全にメッセージを全クライアントに送信
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **message**: 送信するメッセージ文字列
|
||||||
|
|
||||||
|
```python
|
||||||
|
def broadcast(self, message: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
非同期的に全クライアントにメッセージをブロードキャスト
|
||||||
|
|
||||||
|
#### パラメータ
|
||||||
|
- **message**: ブロードキャストするメッセージ
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的なWebSocketサーバー
|
||||||
|
|
||||||
|
```python
|
||||||
|
from models.websocket.websocket_server import WebSocketServer
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
# メッセージハンドラーの定義
|
||||||
|
def on_message_received(server, websocket, message):
|
||||||
|
"""クライアントからのメッセージ処理"""
|
||||||
|
print(f"クライアントからメッセージ受信: {message}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# JSONメッセージの解析
|
||||||
|
data = json.loads(message)
|
||||||
|
|
||||||
|
if data.get('type') == 'translation_request':
|
||||||
|
# 翻訳要求の処理
|
||||||
|
handle_translation_request(server, data)
|
||||||
|
elif data.get('type') == 'config_update':
|
||||||
|
# 設定更新の処理
|
||||||
|
handle_config_update(server, data)
|
||||||
|
else:
|
||||||
|
# エコーバック
|
||||||
|
response = {
|
||||||
|
'type': 'echo',
|
||||||
|
'original_message': data,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# テキストメッセージの場合
|
||||||
|
response = f"受信しました: {message}"
|
||||||
|
server.broadcast(response)
|
||||||
|
|
||||||
|
def handle_translation_request(server, data):
|
||||||
|
"""翻訳要求の処理"""
|
||||||
|
text = data.get('text', '')
|
||||||
|
target_lang = data.get('target_language', 'English')
|
||||||
|
|
||||||
|
# 実際の翻訳処理(ここではモック)
|
||||||
|
translated_text = f"[{target_lang}] {text}"
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'translation_result',
|
||||||
|
'original': text,
|
||||||
|
'translated': translated_text,
|
||||||
|
'target_language': target_lang
|
||||||
|
}
|
||||||
|
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def handle_config_update(server, data):
|
||||||
|
"""設定更新の処理"""
|
||||||
|
config_key = data.get('key')
|
||||||
|
config_value = data.get('value')
|
||||||
|
|
||||||
|
print(f"設定更新: {config_key} = {config_value}")
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'config_updated',
|
||||||
|
'key': config_key,
|
||||||
|
'value': config_value,
|
||||||
|
'status': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
# WebSocketサーバーの起動
|
||||||
|
ws_server = WebSocketServer(host='127.0.0.1', port=8765)
|
||||||
|
ws_server.set_message_handler(on_message_received)
|
||||||
|
ws_server.start_server()
|
||||||
|
|
||||||
|
print("WebSocketサーバーが起動しました: ws://127.0.0.1:8765")
|
||||||
|
|
||||||
|
# 定期的なステータス送信
|
||||||
|
for i in range(10):
|
||||||
|
status_message = {
|
||||||
|
'type': 'status',
|
||||||
|
'server_time': time.time(),
|
||||||
|
'uptime': i * 5,
|
||||||
|
'connected_clients': len(ws_server.clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_server.send(json.dumps(status_message))
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# サーバー停止
|
||||||
|
ws_server.stop_server()
|
||||||
|
```
|
||||||
|
|
||||||
|
### VRCTアプリケーション統合
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VRCTWebSocketInterface:
|
||||||
|
"""VRCT用WebSocketインターフェース"""
|
||||||
|
|
||||||
|
def __init__(self, controller, port=8765):
|
||||||
|
self.controller = controller # VRCTコントローラー
|
||||||
|
self.ws_server = WebSocketServer(host='127.0.0.1', port=port)
|
||||||
|
self.ws_server.set_message_handler(self.handle_web_message)
|
||||||
|
|
||||||
|
def handle_web_message(self, server, websocket, message):
|
||||||
|
"""Webクライアントからのメッセージ処理"""
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
command = data.get('command')
|
||||||
|
|
||||||
|
if command == 'get_config':
|
||||||
|
self.send_config(server)
|
||||||
|
elif command == 'set_config':
|
||||||
|
self.update_config(server, data)
|
||||||
|
elif command == 'start_translation':
|
||||||
|
self.start_translation_service(server, data)
|
||||||
|
elif command == 'stop_translation':
|
||||||
|
self.stop_translation_service(server)
|
||||||
|
elif command == 'get_status':
|
||||||
|
self.send_status(server)
|
||||||
|
elif command == 'translate_text':
|
||||||
|
self.translate_text(server, data)
|
||||||
|
else:
|
||||||
|
self.send_error(server, f"未知のコマンド: {command}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(server, f"メッセージ処理エラー: {e}")
|
||||||
|
|
||||||
|
def send_config(self, server):
|
||||||
|
"""設定情報をWebクライアントに送信"""
|
||||||
|
config_data = {
|
||||||
|
'type': 'config',
|
||||||
|
'data': {
|
||||||
|
'source_language': self.controller.config.source_language,
|
||||||
|
'target_language': self.controller.config.target_language,
|
||||||
|
'translation_engine': self.controller.config.translation_engine,
|
||||||
|
'osc_enabled': self.controller.config.osc_enabled,
|
||||||
|
'overlay_enabled': self.controller.config.overlay_enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(config_data))
|
||||||
|
|
||||||
|
def update_config(self, server, data):
|
||||||
|
"""設定更新"""
|
||||||
|
config_updates = data.get('config', {})
|
||||||
|
|
||||||
|
for key, value in config_updates.items():
|
||||||
|
if hasattr(self.controller.config, key):
|
||||||
|
setattr(self.controller.config, key, value)
|
||||||
|
print(f"設定更新: {key} = {value}")
|
||||||
|
|
||||||
|
# 更新確認を送信
|
||||||
|
response = {
|
||||||
|
'type': 'config_updated',
|
||||||
|
'status': 'success',
|
||||||
|
'updated_keys': list(config_updates.keys())
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def start_translation_service(self, server, data):
|
||||||
|
"""翻訳サービス開始"""
|
||||||
|
try:
|
||||||
|
self.controller.start_translation()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'service_status',
|
||||||
|
'service': 'translation',
|
||||||
|
'status': 'started',
|
||||||
|
'message': '翻訳サービスが開始されました'
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(server, f"翻訳サービス開始エラー: {e}")
|
||||||
|
|
||||||
|
def stop_translation_service(self, server):
|
||||||
|
"""翻訳サービス停止"""
|
||||||
|
try:
|
||||||
|
self.controller.stop_translation()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'service_status',
|
||||||
|
'service': 'translation',
|
||||||
|
'status': 'stopped',
|
||||||
|
'message': '翻訳サービスが停止されました'
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(server, f"翻訳サービス停止エラー: {e}")
|
||||||
|
|
||||||
|
def send_status(self, server):
|
||||||
|
"""システム状態送信"""
|
||||||
|
status_data = {
|
||||||
|
'type': 'system_status',
|
||||||
|
'data': {
|
||||||
|
'translation_active': self.controller.is_translation_active(),
|
||||||
|
'osc_connected': self.controller.is_osc_connected(),
|
||||||
|
'overlay_active': self.controller.is_overlay_active(),
|
||||||
|
'connected_clients': len(server.clients),
|
||||||
|
'uptime': self.controller.get_uptime(),
|
||||||
|
'memory_usage': self.controller.get_memory_usage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(status_data))
|
||||||
|
|
||||||
|
def translate_text(self, server, data):
|
||||||
|
"""即座翻訳実行"""
|
||||||
|
text = data.get('text', '')
|
||||||
|
source_lang = data.get('source_language')
|
||||||
|
target_lang = data.get('target_language')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 翻訳実行
|
||||||
|
result = self.controller.translate_text(
|
||||||
|
text, source_lang, target_lang
|
||||||
|
)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'translation_result',
|
||||||
|
'original': text,
|
||||||
|
'translated': result,
|
||||||
|
'source_language': source_lang,
|
||||||
|
'target_language': target_lang,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(server, f"翻訳エラー: {e}")
|
||||||
|
|
||||||
|
def send_error(self, server, error_message):
|
||||||
|
"""エラーメッセージ送信"""
|
||||||
|
error_data = {
|
||||||
|
'type': 'error',
|
||||||
|
'message': error_message,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(error_data))
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""WebSocketインターフェース開始"""
|
||||||
|
self.ws_server.start_server()
|
||||||
|
print(f"VRCT WebSocketインターフェース開始: ws://127.0.0.1:{self.ws_server.port}")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""WebSocketインターフェース停止"""
|
||||||
|
self.ws_server.stop_server()
|
||||||
|
print("VRCT WebSocketインターフェース停止")
|
||||||
|
|
||||||
|
def notify_translation_result(self, original, translated, source_lang, target_lang):
|
||||||
|
"""翻訳結果の通知(VRCTコントローラーから呼び出し)"""
|
||||||
|
notification = {
|
||||||
|
'type': 'live_translation',
|
||||||
|
'original': original,
|
||||||
|
'translated': translated,
|
||||||
|
'source_language': source_lang,
|
||||||
|
'target_language': target_lang,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
self.ws_server.send(json.dumps(notification))
|
||||||
|
|
||||||
|
# 使用例(VRCTアプリケーション内)
|
||||||
|
# vrct_ws_interface = VRCTWebSocketInterface(controller)
|
||||||
|
# vrct_ws_interface.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
### リアルタイム監視ダッシュボード
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MonitoringDashboard:
|
||||||
|
"""リアルタイム監視ダッシュボード"""
|
||||||
|
|
||||||
|
def __init__(self, system_components, port=8766):
|
||||||
|
self.components = system_components
|
||||||
|
self.ws_server = WebSocketServer(host='127.0.0.1', port=port)
|
||||||
|
self.ws_server.set_message_handler(self.handle_dashboard_message)
|
||||||
|
self.monitoring_active = False
|
||||||
|
|
||||||
|
def handle_dashboard_message(self, server, websocket, message):
|
||||||
|
"""ダッシュボードからのメッセージ処理"""
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
action = data.get('action')
|
||||||
|
|
||||||
|
if action == 'start_monitoring':
|
||||||
|
self.start_monitoring(server)
|
||||||
|
elif action == 'stop_monitoring':
|
||||||
|
self.stop_monitoring(server)
|
||||||
|
elif action == 'get_metrics':
|
||||||
|
self.send_metrics(server)
|
||||||
|
elif action == 'get_logs':
|
||||||
|
self.send_logs(server, data.get('limit', 100))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send_dashboard_error(server, str(e))
|
||||||
|
|
||||||
|
def start_monitoring(self, server):
|
||||||
|
"""監視開始"""
|
||||||
|
if not self.monitoring_active:
|
||||||
|
self.monitoring_active = True
|
||||||
|
|
||||||
|
# 監視スレッド開始
|
||||||
|
import threading
|
||||||
|
monitor_thread = threading.Thread(
|
||||||
|
target=self.monitoring_loop,
|
||||||
|
args=(server,),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
monitor_thread.start()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'monitoring_status',
|
||||||
|
'status': 'started'
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def stop_monitoring(self, server):
|
||||||
|
"""監視停止"""
|
||||||
|
self.monitoring_active = False
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'monitoring_status',
|
||||||
|
'status': 'stopped'
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def monitoring_loop(self, server):
|
||||||
|
"""リアルタイム監視ループ"""
|
||||||
|
while self.monitoring_active:
|
||||||
|
try:
|
||||||
|
# システムメトリクス収集
|
||||||
|
metrics = self.collect_metrics()
|
||||||
|
|
||||||
|
# ダッシュボードに送信
|
||||||
|
dashboard_data = {
|
||||||
|
'type': 'live_metrics',
|
||||||
|
'metrics': metrics,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(dashboard_data))
|
||||||
|
|
||||||
|
time.sleep(2) # 2秒間隔で更新
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"監視ループエラー: {e}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def collect_metrics(self):
|
||||||
|
"""システムメトリクス収集"""
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
'system': {
|
||||||
|
'cpu_percent': psutil.cpu_percent(),
|
||||||
|
'memory_percent': psutil.virtual_memory().percent,
|
||||||
|
'disk_usage': psutil.disk_usage('/').percent
|
||||||
|
},
|
||||||
|
'network': {
|
||||||
|
'bytes_sent': psutil.net_io_counters().bytes_sent,
|
||||||
|
'bytes_recv': psutil.net_io_counters().bytes_recv
|
||||||
|
},
|
||||||
|
'vrct': {
|
||||||
|
'translation_count': self.components.get('translation_count', 0),
|
||||||
|
'osc_messages_sent': self.components.get('osc_count', 0),
|
||||||
|
'overlay_updates': self.components.get('overlay_count', 0),
|
||||||
|
'active_connections': len(self.ws_server.clients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def send_metrics(self, server):
|
||||||
|
"""メトリクス送信"""
|
||||||
|
metrics = self.collect_metrics()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'metrics_snapshot',
|
||||||
|
'metrics': metrics,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def send_logs(self, server, limit):
|
||||||
|
"""ログ送信"""
|
||||||
|
# ログファイルから最新のログを取得(実装例)
|
||||||
|
logs = self.get_recent_logs(limit)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'log_data',
|
||||||
|
'logs': logs,
|
||||||
|
'count': len(logs)
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
|
||||||
|
def get_recent_logs(self, limit):
|
||||||
|
"""最新ログ取得"""
|
||||||
|
# 実際のログファイル読み込み処理
|
||||||
|
mock_logs = [
|
||||||
|
{'level': 'INFO', 'message': 'システム開始', 'timestamp': time.time() - 60},
|
||||||
|
{'level': 'DEBUG', 'message': '翻訳処理完了', 'timestamp': time.time() - 30},
|
||||||
|
{'level': 'WARNING', 'message': 'メモリ使用量増加', 'timestamp': time.time() - 10}
|
||||||
|
]
|
||||||
|
return mock_logs[-limit:]
|
||||||
|
|
||||||
|
def send_dashboard_error(self, server, error_message):
|
||||||
|
"""ダッシュボードエラー送信"""
|
||||||
|
error_data = {
|
||||||
|
'type': 'dashboard_error',
|
||||||
|
'message': error_message,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
server.broadcast(json.dumps(error_data))
|
||||||
|
|
||||||
|
def start_dashboard(self):
|
||||||
|
"""ダッシュボード開始"""
|
||||||
|
self.ws_server.start_server()
|
||||||
|
print(f"監視ダッシュボード開始: ws://127.0.0.1:{self.ws_server.port}")
|
||||||
|
|
||||||
|
def stop_dashboard(self):
|
||||||
|
"""ダッシュボード停止"""
|
||||||
|
self.monitoring_active = False
|
||||||
|
self.ws_server.stop_server()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
system_components = {
|
||||||
|
'translation_count': 150,
|
||||||
|
'osc_count': 75,
|
||||||
|
'overlay_count': 200
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard = MonitoringDashboard(system_components)
|
||||||
|
dashboard.start_dashboard()
|
||||||
|
|
||||||
|
# しばらく実行
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
dashboard.stop_dashboard()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高度なメッセージルーティング
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WebSocketRouter:
|
||||||
|
"""WebSocketメッセージルーティングシステム"""
|
||||||
|
|
||||||
|
def __init__(self, port=8767):
|
||||||
|
self.ws_server = WebSocketServer(host='127.0.0.1', port=port)
|
||||||
|
self.ws_server.set_message_handler(self.route_message)
|
||||||
|
self.routes = {}
|
||||||
|
self.middleware = []
|
||||||
|
self.client_subscriptions = {}
|
||||||
|
|
||||||
|
def add_route(self, message_type, handler):
|
||||||
|
"""メッセージタイプに対するハンドラー登録"""
|
||||||
|
self.routes[message_type] = handler
|
||||||
|
|
||||||
|
def add_middleware(self, middleware_func):
|
||||||
|
"""ミドルウェア追加"""
|
||||||
|
self.middleware.append(middleware_func)
|
||||||
|
|
||||||
|
def route_message(self, server, websocket, message):
|
||||||
|
"""メッセージルーティング処理"""
|
||||||
|
try:
|
||||||
|
# JSON解析
|
||||||
|
data = json.loads(message)
|
||||||
|
message_type = data.get('type')
|
||||||
|
|
||||||
|
# ミドルウェア実行
|
||||||
|
for middleware in self.middleware:
|
||||||
|
data = middleware(data, websocket)
|
||||||
|
if data is None: # ミドルウェアがNoneを返した場合は処理中断
|
||||||
|
return
|
||||||
|
|
||||||
|
# ルーティング実行
|
||||||
|
if message_type in self.routes:
|
||||||
|
handler = self.routes[message_type]
|
||||||
|
response = handler(data, websocket, server)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
server.broadcast(json.dumps(response))
|
||||||
|
else:
|
||||||
|
# 未定義メッセージタイプ
|
||||||
|
error_response = {
|
||||||
|
'type': 'error',
|
||||||
|
'message': f'未対応メッセージタイプ: {message_type}',
|
||||||
|
'original_type': message_type
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(error_response))
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
error_response = {
|
||||||
|
'type': 'error',
|
||||||
|
'message': f'JSON解析エラー: {e}'
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(error_response))
|
||||||
|
except Exception as e:
|
||||||
|
error_response = {
|
||||||
|
'type': 'error',
|
||||||
|
'message': f'処理エラー: {e}'
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(error_response))
|
||||||
|
|
||||||
|
def subscription_middleware(self, data, websocket):
|
||||||
|
"""購読管理ミドルウェア"""
|
||||||
|
message_type = data.get('type')
|
||||||
|
|
||||||
|
if message_type == 'subscribe':
|
||||||
|
# 購読登録
|
||||||
|
topics = data.get('topics', [])
|
||||||
|
client_id = id(websocket)
|
||||||
|
self.client_subscriptions[client_id] = topics
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'subscription_confirmed',
|
||||||
|
'topics': topics
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(response))
|
||||||
|
return None # 処理終了
|
||||||
|
|
||||||
|
elif message_type == 'unsubscribe':
|
||||||
|
# 購読解除
|
||||||
|
client_id = id(websocket)
|
||||||
|
if client_id in self.client_subscriptions:
|
||||||
|
del self.client_subscriptions[client_id]
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'unsubscription_confirmed'
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(response))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return data # そのまま次の処理に進む
|
||||||
|
|
||||||
|
def authentication_middleware(self, data, websocket):
|
||||||
|
"""認証ミドルウェア"""
|
||||||
|
# 簡易認証例
|
||||||
|
api_key = data.get('api_key')
|
||||||
|
|
||||||
|
if api_key != 'valid_api_key_123':
|
||||||
|
error_response = {
|
||||||
|
'type': 'authentication_error',
|
||||||
|
'message': '無効なAPIキー'
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(error_response))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def logging_middleware(self, data, websocket):
|
||||||
|
"""ログ記録ミドルウェア"""
|
||||||
|
client_ip = websocket.remote_address[0] if websocket.remote_address else 'unknown'
|
||||||
|
message_type = data.get('type', 'unknown')
|
||||||
|
|
||||||
|
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {client_ip} -> {message_type}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def broadcast_to_subscribers(self, topic, message_data):
|
||||||
|
"""購読者へのトピック配信"""
|
||||||
|
message_data['topic'] = topic
|
||||||
|
message_json = json.dumps(message_data)
|
||||||
|
|
||||||
|
for client_id, topics in self.client_subscriptions.items():
|
||||||
|
if topic in topics:
|
||||||
|
# 該当クライアントを検索
|
||||||
|
for client in self.ws_server.clients:
|
||||||
|
if id(client) == client_id:
|
||||||
|
try:
|
||||||
|
client.send(message_json)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"配信エラー: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
def start_router(self):
|
||||||
|
"""ルーター開始"""
|
||||||
|
self.ws_server.start_server()
|
||||||
|
print(f"WebSocketルーター開始: ws://127.0.0.1:{self.ws_server.port}")
|
||||||
|
|
||||||
|
def stop_router(self):
|
||||||
|
"""ルーター停止"""
|
||||||
|
self.ws_server.stop_server()
|
||||||
|
|
||||||
|
# 使用例
|
||||||
|
def handle_chat_message(data, websocket, server):
|
||||||
|
"""チャットメッセージハンドラー"""
|
||||||
|
username = data.get('username', 'Anonymous')
|
||||||
|
message = data.get('message', '')
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'chat_broadcast',
|
||||||
|
'username': username,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def handle_translation_request(data, websocket, server):
|
||||||
|
"""翻訳要求ハンドラー"""
|
||||||
|
text = data.get('text', '')
|
||||||
|
# 翻訳処理(モック)
|
||||||
|
translated = f"[翻訳] {text}"
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'type': 'translation_response',
|
||||||
|
'original': text,
|
||||||
|
'translated': translated
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# ルーター設定
|
||||||
|
router = WebSocketRouter()
|
||||||
|
|
||||||
|
# ミドルウェア登録
|
||||||
|
router.add_middleware(router.logging_middleware)
|
||||||
|
router.add_middleware(router.subscription_middleware)
|
||||||
|
# router.add_middleware(router.authentication_middleware) # 認証が必要な場合
|
||||||
|
|
||||||
|
# ルート登録
|
||||||
|
router.add_route('chat_message', handle_chat_message)
|
||||||
|
router.add_route('translation_request', handle_translation_request)
|
||||||
|
|
||||||
|
router.start_router()
|
||||||
|
|
||||||
|
# トピック配信テスト
|
||||||
|
time.sleep(2)
|
||||||
|
router.broadcast_to_subscribers('system_updates', {
|
||||||
|
'type': 'system_notification',
|
||||||
|
'message': 'システム更新完了',
|
||||||
|
'severity': 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
router.stop_router()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高度な機能・パターン
|
||||||
|
|
||||||
|
### 接続プール管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ConnectionPoolManager:
|
||||||
|
"""WebSocket接続プール管理"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pools = {} # pool_name -> set of websockets
|
||||||
|
|
||||||
|
def assign_to_pool(self, websocket, pool_name):
|
||||||
|
"""クライアントをプールに割り当て"""
|
||||||
|
if pool_name not in self.pools:
|
||||||
|
self.pools[pool_name] = set()
|
||||||
|
|
||||||
|
self.pools[pool_name].add(websocket)
|
||||||
|
print(f"クライアントを {pool_name} プールに追加")
|
||||||
|
|
||||||
|
def remove_from_pools(self, websocket):
|
||||||
|
"""すべてのプールからクライアントを削除"""
|
||||||
|
for pool_name, pool in self.pools.items():
|
||||||
|
if websocket in pool:
|
||||||
|
pool.discard(websocket)
|
||||||
|
print(f"クライアントを {pool_name} プールから削除")
|
||||||
|
|
||||||
|
def broadcast_to_pool(self, pool_name, message):
|
||||||
|
"""特定プールに対してブロードキャスト"""
|
||||||
|
if pool_name in self.pools:
|
||||||
|
for websocket in self.pools[pool_name].copy():
|
||||||
|
try:
|
||||||
|
websocket.send(message)
|
||||||
|
except Exception:
|
||||||
|
# 切断されたクライアントを削除
|
||||||
|
self.pools[pool_name].discard(websocket)
|
||||||
|
|
||||||
|
def get_pool_stats(self):
|
||||||
|
"""プール統計情報"""
|
||||||
|
stats = {}
|
||||||
|
for pool_name, pool in self.pools.items():
|
||||||
|
stats[pool_name] = len(pool)
|
||||||
|
return stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### メッセージ永続化・再送機能
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PersistentMessageSystem:
|
||||||
|
"""メッセージ永続化・再送システム"""
|
||||||
|
|
||||||
|
def __init__(self, max_history=1000):
|
||||||
|
self.message_history = []
|
||||||
|
self.max_history = max_history
|
||||||
|
self.client_last_seen = {} # client_id -> last_message_id
|
||||||
|
|
||||||
|
def store_message(self, message_data):
|
||||||
|
"""メッセージを履歴に保存"""
|
||||||
|
message_id = len(self.message_history)
|
||||||
|
stored_message = {
|
||||||
|
'id': message_id,
|
||||||
|
'data': message_data,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.message_history.append(stored_message)
|
||||||
|
|
||||||
|
# 履歴サイズ制限
|
||||||
|
if len(self.message_history) > self.max_history:
|
||||||
|
self.message_history = self.message_history[-self.max_history:]
|
||||||
|
|
||||||
|
return message_id
|
||||||
|
|
||||||
|
def get_missed_messages(self, client_id, last_seen_id):
|
||||||
|
"""クライアントが見逃したメッセージを取得"""
|
||||||
|
missed_messages = []
|
||||||
|
|
||||||
|
for msg in self.message_history:
|
||||||
|
if msg['id'] > last_seen_id:
|
||||||
|
missed_messages.append(msg)
|
||||||
|
|
||||||
|
return missed_messages
|
||||||
|
|
||||||
|
def client_reconnected(self, websocket, client_id):
|
||||||
|
"""クライアント再接続時の処理"""
|
||||||
|
last_seen = self.client_last_seen.get(client_id, -1)
|
||||||
|
missed_messages = self.get_missed_messages(client_id, last_seen)
|
||||||
|
|
||||||
|
# 見逃したメッセージを再送
|
||||||
|
for msg in missed_messages:
|
||||||
|
try:
|
||||||
|
recovery_data = {
|
||||||
|
'type': 'message_recovery',
|
||||||
|
'original_message': msg['data'],
|
||||||
|
'message_id': msg['id'],
|
||||||
|
'original_timestamp': msg['timestamp']
|
||||||
|
}
|
||||||
|
websocket.send(json.dumps(recovery_data))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"メッセージ再送エラー: {e}")
|
||||||
|
|
||||||
|
print(f"クライアント {client_id} に {len(missed_messages)} 件のメッセージを再送")
|
||||||
|
|
||||||
|
def update_client_position(self, client_id, message_id):
|
||||||
|
"""クライアントの最新メッセージ位置更新"""
|
||||||
|
self.client_last_seen[client_id] = message_id
|
||||||
|
```
|
||||||
|
|
||||||
|
## パフォーマンス・スケーラビリティ
|
||||||
|
|
||||||
|
### 負荷分散・最適化
|
||||||
|
|
||||||
|
```python
|
||||||
|
class OptimizedWebSocketServer(WebSocketServer):
|
||||||
|
"""最適化されたWebSocketサーバー"""
|
||||||
|
|
||||||
|
def __init__(self, host='127.0.0.1', port=8765):
|
||||||
|
super().__init__(host, port)
|
||||||
|
self.message_stats = {
|
||||||
|
'total_messages': 0,
|
||||||
|
'messages_per_second': 0,
|
||||||
|
'last_reset_time': time.time()
|
||||||
|
}
|
||||||
|
self.compression_enabled = True
|
||||||
|
self.batch_size = 50
|
||||||
|
self.batch_timeout = 0.1
|
||||||
|
|
||||||
|
def enable_message_batching(self, batch_size=50, timeout=0.1):
|
||||||
|
"""メッセージバッチング有効化"""
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.batch_timeout = timeout
|
||||||
|
|
||||||
|
async def optimized_broadcast(self, message_batch):
|
||||||
|
"""最適化されたバッチブロードキャスト"""
|
||||||
|
if not self.clients:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 圧縮対応
|
||||||
|
if self.compression_enabled and len(message_batch) > 1:
|
||||||
|
# 複数メッセージをまとめて送信
|
||||||
|
combined_message = json.dumps({
|
||||||
|
'type': 'batch',
|
||||||
|
'messages': message_batch,
|
||||||
|
'count': len(message_batch)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
combined_message = json.dumps(message_batch[0])
|
||||||
|
|
||||||
|
# 並列送信(エラー処理付き)
|
||||||
|
send_tasks = []
|
||||||
|
for client in self.clients.copy():
|
||||||
|
send_tasks.append(self.safe_send(client, combined_message))
|
||||||
|
|
||||||
|
results = await asyncio.gather(*send_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# 失敗したクライアントを削除
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
failed_client = list(self.clients)[i]
|
||||||
|
self.clients.discard(failed_client)
|
||||||
|
print(f"クライアント削除(送信失敗): {result}")
|
||||||
|
|
||||||
|
# 統計更新
|
||||||
|
self.update_message_stats(len(message_batch))
|
||||||
|
|
||||||
|
async def safe_send(self, client, message):
|
||||||
|
"""安全なメッセージ送信"""
|
||||||
|
try:
|
||||||
|
await client.send(message)
|
||||||
|
except Exception as e:
|
||||||
|
raise e # gather で捕捉される
|
||||||
|
|
||||||
|
def update_message_stats(self, message_count):
|
||||||
|
"""メッセージ統計更新"""
|
||||||
|
self.message_stats['total_messages'] += message_count
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
time_diff = current_time - self.message_stats['last_reset_time']
|
||||||
|
|
||||||
|
if time_diff >= 1.0: # 1秒ごとに速度計算
|
||||||
|
self.message_stats['messages_per_second'] = message_count / time_diff
|
||||||
|
self.message_stats['last_reset_time'] = current_time
|
||||||
|
|
||||||
|
def get_performance_stats(self):
|
||||||
|
"""パフォーマンス統計取得"""
|
||||||
|
return {
|
||||||
|
'connected_clients': len(self.clients),
|
||||||
|
'total_messages': self.message_stats['total_messages'],
|
||||||
|
'messages_per_second': self.message_stats['messages_per_second'],
|
||||||
|
'compression_enabled': self.compression_enabled,
|
||||||
|
'batch_size': self.batch_size
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係・システム要件
|
||||||
|
|
||||||
|
### 必須依存関係
|
||||||
|
- `asyncio`: 非同期処理フレームワーク
|
||||||
|
- `websockets`: WebSocketライブラリ
|
||||||
|
- `threading`: マルチスレッド制御
|
||||||
|
- `json`: JSON形式データ処理
|
||||||
|
|
||||||
|
### システム要件
|
||||||
|
```python
|
||||||
|
system_requirements = {
|
||||||
|
"python_version": "3.7以上",
|
||||||
|
"asyncio_support": "非同期処理対応",
|
||||||
|
"network_stack": "TCP/WebSocket対応",
|
||||||
|
"memory": "同時接続数に応じた十分なメモリ"
|
||||||
|
}
|
||||||
|
|
||||||
|
performance_characteristics = {
|
||||||
|
"concurrent_connections": "数百~数千接続対応",
|
||||||
|
"message_throughput": "秒間数千メッセージ処理可能",
|
||||||
|
"latency": "低レイテンシー(ミリ秒オーダー)",
|
||||||
|
"memory_per_connection": "約1-5MB(接続当たり)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### オプション依存関係
|
||||||
|
- `ujson`: 高速JSON処理(パフォーマンス向上)
|
||||||
|
- `compression`: メッセージ圧縮(帯域節約)
|
||||||
|
|
||||||
|
## 注意事項・制限
|
||||||
|
|
||||||
|
### ネットワーク制限
|
||||||
|
- ファイアウォール設定の要確認
|
||||||
|
- プロキシ環境での制限可能性
|
||||||
|
- ブラウザーのWebSocket接続制限
|
||||||
|
|
||||||
|
### スケーラビリティ制限
|
||||||
|
- 単一プロセスでの同時接続数制限
|
||||||
|
- メモリ使用量の線形増加
|
||||||
|
- CPU集約的な処理での性能劣化
|
||||||
|
|
||||||
|
### セキュリティ考慮事項
|
||||||
|
```python
|
||||||
|
security_considerations = {
|
||||||
|
"authentication": "認証機構の実装推奨",
|
||||||
|
"authorization": "適切な認可制御",
|
||||||
|
"rate_limiting": "レート制限の実装",
|
||||||
|
"input_validation": "入力データの検証必須",
|
||||||
|
"cors_policy": "CORS設定の適切な構成"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連モジュール
|
||||||
|
|
||||||
|
- `config.py`: WebSocket設定管理
|
||||||
|
- `controller.py`: WebSocket制御インターフェース
|
||||||
|
- `utils.py`: エラーログ・ユーティリティ
|
||||||
|
- `model.py`: WebSocket機能統合
|
||||||
|
|
||||||
|
## 将来の改善点
|
||||||
|
|
||||||
|
- Redis等を用いたメッセージブローカー連携
|
||||||
|
- 負荷分散・クラスタリング対応
|
||||||
|
- より高度な認証・認可システム
|
||||||
|
- WebRTC等のより高速な通信プロトコル対応
|
||||||
|
- GraphQL over WebSocketサポート
|
||||||
|
- リアルタイム監視・分析機能の強化
|
||||||
@@ -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が元の値に戻せるように、元の値を渡すこと
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
# mainloop.py 設計書
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
`mainloop.py` は VRCT アプリケーションのバックエンドエントリーポイントであり、stdin/stdout を介したフロントエンド(Tauri/React UI)との通信を担当する。JSON ベースのリクエスト/レスポンスプロトコルを実装し、複数のワーカースレッドによる並列処理と排他制御を提供する。
|
||||||
|
|
||||||
|
## 主要コンポーネント
|
||||||
|
|
||||||
|
### 1. グローバル変数
|
||||||
|
|
||||||
|
#### `run_mapping` (dict)
|
||||||
|
|
||||||
|
フロントエンドへの通知用エンドポイントマッピング。Controllerが `run()` コールバックを通じてフロントエンドに状態変化を通知する際に使用。
|
||||||
|
|
||||||
|
**主要なエンドポイント:**
|
||||||
|
|
||||||
|
- `/run/enable_translation` - 翻訳機能の有効/無効状態
|
||||||
|
- `/run/transcription_mic_message` - マイク音声認識結果
|
||||||
|
- `/run/transcription_speaker_message` - スピーカー音声認識結果
|
||||||
|
- `/run/selectable_groq_model_list` - 利用可能な Groq モデルリスト通知
|
||||||
|
- `/run/selected_groq_model` - 選択中の Groq モデル通知
|
||||||
|
- `/run/selectable_openrouter_model_list` - 利用可能な OpenRouter モデルリスト通知
|
||||||
|
- `/run/selected_openrouter_model` - 選択中の OpenRouter モデル通知
|
||||||
|
- `/run/error_*` - 各種エラー通知
|
||||||
|
- `/run/initialization_complete` - 初期化完了通知
|
||||||
|
|
||||||
|
#### `mapping` (dict)
|
||||||
|
|
||||||
|
フロントエンドからのリクエストを処理する関数マッピング。各エンドポイントに対して:
|
||||||
|
|
||||||
|
- `status`: ロック状態(True: 処理可能, False: ロック中)
|
||||||
|
- `variable`: 実行する Controller メソッド
|
||||||
|
|
||||||
|
**エンドポイント分類:**
|
||||||
|
|
||||||
|
- `/get/data/*` - 設定値の取得(初期化時に使用)
|
||||||
|
- `/get/data/groq_auth_key` - Groq API キー取得
|
||||||
|
- `/get/data/selectable_groq_model_list` - 利用可能な Groq モデルリスト取得
|
||||||
|
- `/get/data/selected_groq_model` - 選択中の Groq モデル取得
|
||||||
|
- `/get/data/openrouter_auth_key` - OpenRouter API キー取得
|
||||||
|
- `/get/data/selectable_openrouter_model_list` - 利用可能な OpenRouter モデルリスト取得
|
||||||
|
- `/get/data/selected_openrouter_model` - 選択中の OpenRouter モデル取得
|
||||||
|
- `/set/data/*` - 設定値の更新
|
||||||
|
- `/set/data/groq_auth_key` - Groq API キー設定
|
||||||
|
- `/set/data/selected_groq_model` - Groq モデル選択
|
||||||
|
- `/set/data/openrouter_auth_key` - OpenRouter API キー設定
|
||||||
|
- `/set/data/selected_openrouter_model` - OpenRouter モデル選択
|
||||||
|
- `/delete/data/*` - 設定値の削除
|
||||||
|
- `/delete/data/groq_auth_key` - Groq API キー削除
|
||||||
|
- `/delete/data/openrouter_auth_key` - OpenRouter API キー削除
|
||||||
|
- `/set/enable/*` - 機能の有効化
|
||||||
|
- `/set/disable/*` - 機能の無効化
|
||||||
|
- `/run/*` - アクション実行(メッセージ送信、ダウンロード等)
|
||||||
|
|
||||||
|
#### `init_mapping` (dict)
|
||||||
|
|
||||||
|
初期化時に実行される `/get/data/*` エンドポイントのサブセット。アプリケーション起動時に全設定値をフロントエンドに送信するために使用。
|
||||||
|
|
||||||
|
### 2. Mainクラス
|
||||||
|
|
||||||
|
#### コンストラクタ `__init__(controller_instance, mapping_data, worker_count)`
|
||||||
|
|
||||||
|
**パラメータ:**
|
||||||
|
|
||||||
|
- `controller_instance`: Controller インスタンス
|
||||||
|
- `mapping_data`: エンドポイントマッピング辞書
|
||||||
|
- `worker_count`: ハンドラワーカースレッド数(デフォルト: 3)
|
||||||
|
|
||||||
|
**初期化処理:**
|
||||||
|
|
||||||
|
1. リクエストキュー (`Queue[Tuple[str, Any]]`) の作成
|
||||||
|
2. 停止イベント (`Event`) の作成
|
||||||
|
3. エンドポイント別 Lock の生成:
|
||||||
|
- `/set/enable/xxx` と `/set/disable/xxx` を `/lock/set/xxx` に正規化
|
||||||
|
- 同一機能の有効化/無効化リクエストが競合しないよう排他制御
|
||||||
|
|
||||||
|
**正規化ロジックの例:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"/set/enable/translation" → "/lock/set/translation"
|
||||||
|
"/set/disable/translation" → "/lock/set/translation"
|
||||||
|
# 両方が同じロックを共有 → 排他的に実行される
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `receiver()` メソッド
|
||||||
|
|
||||||
|
**責務:** stdin から JSON リクエストを読み取り、キューに投入
|
||||||
|
|
||||||
|
**処理フロー:**
|
||||||
|
|
||||||
|
1. `sys.stdin.readline()` でブロッキング読み取り
|
||||||
|
2. JSON パース (`json.loads()`)
|
||||||
|
3. エンドポイントとデータを抽出
|
||||||
|
4. データが存在する場合は Base64 デコード (`encodeBase64()`)
|
||||||
|
5. ログ出力 (`printLog()`)
|
||||||
|
6. キューに投入 `self.queue.put((endpoint, data))`
|
||||||
|
|
||||||
|
**エラー処理:**
|
||||||
|
|
||||||
|
- JSON パースエラー: ログ出力して継続
|
||||||
|
- EOF 到達: 0.1秒待機して再試行
|
||||||
|
- その他の例外: `errorLogging()` でトレースバック記録
|
||||||
|
|
||||||
|
**スレッド:** デーモンスレッド `main_receiver` として起動
|
||||||
|
|
||||||
|
#### `handler()` メソッド
|
||||||
|
|
||||||
|
**責務:** キューからリクエストを取り出し、適切なロックを取得して処理
|
||||||
|
|
||||||
|
**処理フロー:**
|
||||||
|
|
||||||
|
1. キューから `(endpoint, data)` を取得(0.5秒タイムアウト)
|
||||||
|
2. エンドポイントを正規化キーに変換
|
||||||
|
3. 対応する Lock を取得試行(非ブロッキング)
|
||||||
|
- 取得成功 → 処理実行 → ロック解放
|
||||||
|
- 取得失敗 → 0.05秒待機して再キュー
|
||||||
|
4. `_call_handler(endpoint, data)` を呼び出し
|
||||||
|
5. レスポンスを stdout に出力 (`printResponse()`)
|
||||||
|
|
||||||
|
**排他制御の意義:**
|
||||||
|
|
||||||
|
- 例: 翻訳機能の有効化中に無効化リクエストが来た場合、無効化は待機
|
||||||
|
- 異なる機能のリクエストは並列実行可能
|
||||||
|
|
||||||
|
**再キューロジック:**
|
||||||
|
|
||||||
|
- status == 423 (Locked): 0.1秒待機して再キュー
|
||||||
|
- これにより、初期化中の設定変更リクエストが適切にリトライされる
|
||||||
|
|
||||||
|
**ワーカー数:** `worker_count` 個のスレッド `main_handler_0`, `main_handler_1`, ... として起動
|
||||||
|
|
||||||
|
#### `_call_handler(endpoint, data)` メソッド
|
||||||
|
|
||||||
|
**責務:** 実際のビジネスロジック実行
|
||||||
|
|
||||||
|
**処理フロー:**
|
||||||
|
|
||||||
|
1. `mapping` から対応するハンドラを取得
|
||||||
|
2. エンドポイントが存在しない → status 404
|
||||||
|
3. ハンドラの `status` が False → status 423 (Locked)
|
||||||
|
4. ハンドラの `variable` 関数を実行 → `response = handler["variable"](data)`
|
||||||
|
5. 0.2秒待機(処理安定化のため)
|
||||||
|
6. status と result を抽出して返却
|
||||||
|
|
||||||
|
**エラー処理:**
|
||||||
|
|
||||||
|
- 例外発生時: `errorLogging()` でトレースバック記録、status 500 を返却
|
||||||
|
|
||||||
|
#### `start()` / `stop(wait)` メソッド(2025-10 仕様)
|
||||||
|
|
||||||
|
**start():**
|
||||||
|
コミット `5ce281e` によりシンプルな「メインループ維持」機能のみになりました。`startReceiver()` / `startHandler()` は `start()` 内では呼ばれません。呼び出し側(`__main__` ブロックや外部プロセスマネージャ)で必要なスレッドを起動した後、`start()` を呼び出して以下の挙動を提供します。
|
||||||
|
|
||||||
|
```python
|
||||||
|
def start(self) -> None:
|
||||||
|
try:
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
ポイント:
|
||||||
|
|
||||||
|
- メインスレッドを 1 秒スリープしつつ存続させ、Ctrl+C(KeyboardInterrupt)で安全に停止へ遷移。
|
||||||
|
- 既存の receiver / handler スレッド二重起動のリスクを除去。
|
||||||
|
- ライフサイクル明確化: スレッド起動責務を `start()` から分離しテスト・拡張容易性向上。
|
||||||
|
|
||||||
|
従来仕様ドキュメントの「`start()` がスレッドを直接起動する」記述は廃止済みであり、最新コードでは不要です。
|
||||||
|
|
||||||
|
**stop(wait):**
|
||||||
|
|
||||||
|
- `_stop_event.set()` - 全スレッドに停止シグナル送信
|
||||||
|
- 各スレッドを `join(timeout=remaining)` で待機(最大 `wait` 秒)
|
||||||
|
|
||||||
|
### 3. 初期化シーケンス
|
||||||
|
|
||||||
|
**`if __name__ == "__main__":` ブロック:**
|
||||||
|
|
||||||
|
1. `main_instance` 作成
|
||||||
|
2. `startReceiver()` - stdin リスニング開始
|
||||||
|
3. `startHandler()` - リクエスト処理開始
|
||||||
|
4. **Watchdog 設定:**
|
||||||
|
- `controller.setWatchdogCallback(main_instance.stop)`
|
||||||
|
- Watchdog がタイムアウトした場合にプロセス全体を停止
|
||||||
|
5. **Controller 初期化:**
|
||||||
|
- `controller.init()`
|
||||||
|
- Model の遅延初期化、デバイス列挙、ネットワーク接続チェック
|
||||||
|
- `init_mapping` のすべてのエンドポイントを実行して初期設定をフロントエンドに送信
|
||||||
|
6. **マッピングのアンロック:**
|
||||||
|
- すべての `mapping[key]["status"]` を True に設定
|
||||||
|
- これにより初期化中だった機能が利用可能になる
|
||||||
|
7. `main_instance.start()` - メインループ維持のみ(receiver / handler は事前に起動済み)
|
||||||
|
|
||||||
|
### Watchdog エンドポイント連携
|
||||||
|
|
||||||
|
バックエンドはクライアント側から `/run/feed_watchdog` が一定間隔(例: 30 秒)で送信される前提の設計が可能です。テストクライアント(`test_client.py`)は初期化完了後にバックグラウンドスレッドでこのエンドポイントを自動送信し、内部監視(`controller` 側の Watchdog コールバック)タイムアウト防止を行います。
|
||||||
|
|
||||||
|
ログ上は通常レスポンス非取得(fire & forget)となるため、負荷低減のためにハートビート頻度調整が可能です。
|
||||||
|
|
||||||
|
## 並列処理とスレッドセーフティ
|
||||||
|
|
||||||
|
### スレッド構成
|
||||||
|
|
||||||
|
| スレッド名 | 役割 | 生存期間 |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `main_receiver` | stdin からの JSON 読み取り | プロセス終了まで |
|
||||||
|
| `main_handler_0` ~ `main_handler_N` | リクエスト処理ワーカー | プロセス終了まで |
|
||||||
|
|
||||||
|
### 同期メカニズム
|
||||||
|
|
||||||
|
1. **キュー (`Queue`):**
|
||||||
|
- スレッドセーフな FIFO キュー
|
||||||
|
- receiver → handler への通信チャネル
|
||||||
|
|
||||||
|
2. **エンドポイント別 Lock (`dict[str, Lock]`):**
|
||||||
|
- 同一リソースへの競合アクセスを防止
|
||||||
|
- 正規化キーによる enable/disable ペアの統合
|
||||||
|
|
||||||
|
3. **停止イベント (`Event`):**
|
||||||
|
- グレースフルシャットダウン用のシグナル
|
||||||
|
|
||||||
|
### デッドロック回避
|
||||||
|
|
||||||
|
- **非ブロッキング Lock 取得:** `lock.acquire(blocking=False)`
|
||||||
|
- **失敗時の再キュー:** ロック取得失敗時は即座に諦めて再キュー
|
||||||
|
- **タイムアウト付きキュー取得:** `queue.get(timeout=0.5)` で無限待機を回避
|
||||||
|
|
||||||
|
## プロトコル仕様
|
||||||
|
|
||||||
|
### リクエストフォーマット (stdin)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "/set/data/transparency",
|
||||||
|
"data": "ODU=" // Base64 encoded: "85"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**フィールド:**
|
||||||
|
|
||||||
|
- `endpoint`: 実行するエンドポイント(必須)
|
||||||
|
- `data`: パラメータ(オプション、Base64 エンコード)
|
||||||
|
|
||||||
|
### レスポンスフォーマット (stdout)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"endpoint": "/set/data/transparency",
|
||||||
|
"result": 85
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**フィールド:**
|
||||||
|
|
||||||
|
- `status`: HTTP ステータスコード相当
|
||||||
|
- 200: 成功
|
||||||
|
- 400: バリデーションエラー
|
||||||
|
- 404: 無効なエンドポイント
|
||||||
|
- 423: ロック中(リトライされる)
|
||||||
|
- 500: 内部エラー
|
||||||
|
- `endpoint`: リクエストされたエンドポイント
|
||||||
|
- `result`: 処理結果(型はエンドポイントに依存)
|
||||||
|
|
||||||
|
### ログフォーマット (stdout)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 348, // 専用ステータスコード
|
||||||
|
"log": "setSelectedTabNo",
|
||||||
|
"data": "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 1. JSON パースエラー
|
||||||
|
|
||||||
|
- **発生箇所:** `receiver()` の `json.loads()`
|
||||||
|
- **処理:** `errorLogging()` でトレースバック記録、リクエストをスキップ
|
||||||
|
|
||||||
|
### 2. ハンドラ実行エラー
|
||||||
|
|
||||||
|
- **発生箇所:** `_call_handler()` の `handler["variable"](data)`
|
||||||
|
- **処理:**
|
||||||
|
- `errorLogging()` でトレースバック記録
|
||||||
|
- status 500 と "Internal error" を返却
|
||||||
|
- プロセスは継続
|
||||||
|
|
||||||
|
### 3. JSON シリアライズエラー
|
||||||
|
|
||||||
|
- **発生箇所:** `printResponse()` の `json.dumps()`
|
||||||
|
- **処理:**
|
||||||
|
- エラーログに詳細を記録
|
||||||
|
- フォールバック JSON を出力(status 500)
|
||||||
|
- プロセスは継続
|
||||||
|
|
||||||
|
### 4. EOF (stdin 終了)
|
||||||
|
|
||||||
|
- **発生箇所:** `receiver()` の `readline()`
|
||||||
|
- **処理:** 0.1秒待機して再試行(フロントエンドの再起動待ち)
|
||||||
|
|
||||||
|
## パフォーマンス最適化
|
||||||
|
|
||||||
|
### 1. 複数ワーカースレッド
|
||||||
|
|
||||||
|
- デフォルト3スレッドで並列処理
|
||||||
|
- CPU バウンドな処理(翻訳、文字起こし)を効率化
|
||||||
|
|
||||||
|
### 2. 非ブロッキングロック
|
||||||
|
|
||||||
|
- ロック競合時に即座に再キュー
|
||||||
|
- スレッドのブロッキング時間を最小化
|
||||||
|
|
||||||
|
### 3. 処理安定化待機
|
||||||
|
|
||||||
|
- 各ハンドラ実行後に 0.2秒待機
|
||||||
|
- 連続リクエストによる競合状態を回避
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
### 1. 初期化中の制限
|
||||||
|
|
||||||
|
- `mapping[key]["status"] = False` の間はリクエストが 423 でリトライされる
|
||||||
|
- 初期化完了まで最大数秒のレイテンシが発生
|
||||||
|
|
||||||
|
### 2. stdin の単方向性
|
||||||
|
|
||||||
|
- stdin → キュー → ハンドラの一方向フロー
|
||||||
|
- 複数のフロントエンドからの同時接続は非対応
|
||||||
|
|
||||||
|
### 3. シリアル実行の保証
|
||||||
|
|
||||||
|
- 同一エンドポイントのリクエストは排他的に実行されるが、
|
||||||
|
- 異なるエンドポイントは並列実行される可能性がある
|
||||||
|
- 依存関係のある操作は呼び出し側で順序制御が必要
|
||||||
|
|
||||||
|
## デバッグとトラブルシューティング
|
||||||
|
|
||||||
|
### ログファイル
|
||||||
|
|
||||||
|
| ファイル名 | 内容 |
|
||||||
|
|-----------|------|
|
||||||
|
| `process.log` | 全リクエスト/レスポンスの記録 |
|
||||||
|
| `error.log` | 例外トレースバック |
|
||||||
|
|
||||||
|
### デバッグ手法
|
||||||
|
|
||||||
|
1. **リクエストトレース:**
|
||||||
|
- `process.log` で endpoint と data を確認
|
||||||
|
- Base64 デコードは `base64.b64decode(data).decode('utf-8')` で手動実行
|
||||||
|
|
||||||
|
2. **ロック競合の検出:**
|
||||||
|
- 同一エンドポイントで status 423 が頻発する場合
|
||||||
|
- `_canonical_lock_key()` の正規化ロジックを確認
|
||||||
|
|
||||||
|
3. **パフォーマンス分析:**
|
||||||
|
- 各リクエストの処理時間は status 前後のタイムスタンプから算出
|
||||||
|
- worker_count を増やして並列度を調整
|
||||||
|
|
||||||
|
## 今後の拡張性
|
||||||
|
|
||||||
|
### 1. 双方向通信
|
||||||
|
|
||||||
|
- WebSocket への移行でリアルタイム通知を改善
|
||||||
|
- stdin/stdout は互換性のため維持
|
||||||
|
|
||||||
|
### 2. 動的ワーカー数調整
|
||||||
|
|
||||||
|
- キューの深さに応じてスレッド数を自動調整
|
||||||
|
- CPU 負荷に応じた適応的なスケーリング
|
||||||
|
|
||||||
|
### 3. 優先度キュー
|
||||||
|
|
||||||
|
- 重要なリクエスト(エラー通知等)を優先処理
|
||||||
|
- `queue.PriorityQueue` への移行
|
||||||
|
|
||||||
|
## 関連ファイル
|
||||||
|
|
||||||
|
- `controller.py` - ビジネスロジック実装
|
||||||
|
- `model.py` - 機能ファサード
|
||||||
|
- `utils.py` - ログとユーティリティ
|
||||||
|
- `config.py` - 設定管理
|
||||||
|
|
||||||
|
## コーディング規約
|
||||||
|
|
||||||
|
本ファイルは以下の規約に従う:
|
||||||
|
|
||||||
|
- PEP 8 スタイルガイド
|
||||||
|
- 型ヒント (`typing` モジュール)
|
||||||
|
- Docstring は Google スタイル
|
||||||
|
- エラーハンドリングは防御的に実装
|
||||||
|
|
||||||
|
## テストシナリオ
|
||||||
|
|
||||||
|
### 1. 基本動作テスト
|
||||||
|
|
||||||
|
```python
|
||||||
|
# stdin に JSON を送信
|
||||||
|
echo '{"endpoint": "/get/data/version", "data": null}' | python mainloop.py
|
||||||
|
# 期待される出力: {"status": 200, "endpoint": "/get/data/version", "result": "1.0.0"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 並列リクエストテスト
|
||||||
|
|
||||||
|
- 複数の設定変更リクエストを同時送信
|
||||||
|
- すべてが正常に処理されることを確認
|
||||||
|
|
||||||
|
### 3. ロック競合テスト
|
||||||
|
|
||||||
|
- 翻訳の有効化と無効化を連続送信
|
||||||
|
- 両方が排他的に実行されることを確認
|
||||||
|
|
||||||
|
### 4. エラー回復テスト
|
||||||
|
|
||||||
|
- 不正なJSON、無効なエンドポイント、不正なデータを送信
|
||||||
|
- プロセスがクラッシュせずエラーレスポンスを返すことを確認
|
||||||
|
|
||||||
|
## まとめ
|
||||||
|
|
||||||
|
`mainloop.py` は VRCT の中核となる通信レイヤーであり、stdin/stdout を介したフロントエンドとの JSON ベースプロトコルを実装する。複数のワーカースレッドと細粒度のロックにより、高い並列性と排他制御を両立させている。初期化シーケンスとエラーハンドリングは堅牢に設計されており、プロセスの安定稼働を保証する。
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 小型ログ ルビ表示機能 (Ruby Overlay for Small Log)
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
小型ログ (Small Log Overlay) に日本語原文が含まれる場合、ローマ字(hepburn) を上段、ひらがな(hira) を下段として原文メッセージの上に 2 段のルビを表示できます。翻訳行には現段階ではルビを付与しません。
|
||||||
|
|
||||||
|
## 有効化条件
|
||||||
|
- 原文 `message` が存在し空文字列でない。
|
||||||
|
- `model.createOverlayImageSmallLog` 内で自動的に `convertMessageToTransliteration(..., hiragana=True, romaji=True)` を呼び出しトークン生成。
|
||||||
|
- 生成されたトークンに `hepburn` または `hira` が含まれる。
|
||||||
|
|
||||||
|
## 大ログ (Large Log) への拡張
|
||||||
|
|
||||||
|
大ログについてもトークン単位のルビ描画をサポートしました。`createOverlayImageLargeLog` / `createTextboxLargeLog` 系の API に以下のような追加引数が入り、同等のルビ出力が可能です。
|
||||||
|
|
||||||
|
- transliteration_tokens: Optional[List[dict]] — 原文用トークン(orig/hira/hepburn)
|
||||||
|
- translation_transliteration_tokens: Optional[List[List[dict]] | List[dict]] — 翻訳ごとのトークン配列、もしくは先頭翻訳用の平坦な List[dict]
|
||||||
|
- ruby_font_scale: float — ルビのフォント倍率(原文フォントサイズに対するスケール)
|
||||||
|
- ruby_line_spacing: int — ルビ行間ピクセル
|
||||||
|
|
||||||
|
対応挙動:
|
||||||
|
|
||||||
|
- 原文のみが存在し、その原文にトークンがあれば原文にトークン単位ルビを振ります。
|
||||||
|
|
||||||
|
- 原文と翻訳の両方が存在する場合は、原則として原文のルビを抑止し、翻訳側にルビを振ります(翻訳側にトークンがある場合)。
|
||||||
|
|
||||||
|
- `translation_transliteration_tokens` は二通りの入力形式を受け付けます:
|
||||||
|
|
||||||
|
- List[List[dict]] — 各翻訳行ごとの tokens 配列(推奨)
|
||||||
|
|
||||||
|
- List[dict] — 平坦な tokens 配列(最初の翻訳行に適用されます)
|
||||||
|
|
||||||
|
フォールバック:
|
||||||
|
|
||||||
|
- トークン単位レイアウトで横幅がはみ出す or 改行がある場合は、既存のブロックルビ(romaji 上 / hira 下 を 1 行ブロックで表示)へ自動フォールバックします。
|
||||||
|
|
||||||
|
注意点:
|
||||||
|
|
||||||
|
- 既存の表示ロジックの互換性を保つため、引数は省略可能です(None/[])。
|
||||||
|
- フラットな `translation_transliteration_tokens` を渡す場合は最初の翻訳にのみ適用されます。複数翻訳に個別のルビを渡す場合は List[List[dict]] 形式で与えてください。
|
||||||
|
|
||||||
|
## 設定キー (`config.OVERLAY_SMALL_LOG_SETTINGS`)
|
||||||
|
| キー | 型 | 初期値 | 説明 |
|
||||||
|
| ---- | --- | ------ | ---- |
|
||||||
|
| ruby_font_scale | float | 0.5 | ルビ文字サイズ倍率 (原文フォントサイズ * 倍率)。安全範囲 0.05〜3.0 |
|
||||||
|
| ruby_line_spacing | int | 4 | ローマ字行とひらがな行の垂直スペース (px)。0〜200 |
|
||||||
|
|
||||||
|
## レイアウト仕様
|
||||||
|
|
||||||
|
1. ルビブロック (romaji 上 / hiragana 下) を中央揃えで描画。
|
||||||
|
|
||||||
|
2. その下に従来の本文テキストボックスを縦方向に連結。
|
||||||
|
|
||||||
|
3. フォントファミリは本文と同一 (言語に対応する NotoSans 系)。
|
||||||
|
|
||||||
|
4. ルビが存在しない場合は従来表示のみ。
|
||||||
|
|
||||||
|
## フォールバック
|
||||||
|
|
||||||
|
- ルビ生成中に例外が発生した場合はログを記録し、ルビ無しで本文のみ表示。
|
||||||
|
|
||||||
|
- トークンが空の場合(両方 False など)は従来表示。
|
||||||
|
|
||||||
|
## 例
|
||||||
|
|
||||||
|
以下は `createOverlayImageLargeLog` を使って、翻訳側にだけルビを渡す例(平坦な tokens を渡す場合と翻訳ごとの tokens を渡す場合):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 平坦な tokens を渡して最初の翻訳に適用
|
||||||
|
overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_tokens=[], translation_transliteration_tokens=[
|
||||||
|
{"orig": "こんにちは", "hira": "こんにちは", "hepburn": "konnichiha"},
|
||||||
|
{"orig": "世界", "hira": "せかい", "hepburn": "sekai"},
|
||||||
|
])
|
||||||
|
|
||||||
|
# 翻訳ごとに tokens を与える(推奨)
|
||||||
|
overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_tokens=[], translation_transliteration_tokens=[
|
||||||
|
[
|
||||||
|
{"orig": "Hello", "hira": "", "hepburn": "Hello"},
|
||||||
|
{"orig": "World", "hira": "", "hepburn": "World"},
|
||||||
|
]
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 今後の拡張候補
|
||||||
|
- 翻訳行へのルビ付与オプション。
|
||||||
|
- トークン単位での幅センタリングと折り返し。
|
||||||
|
- 高度な幅計測 (可変幅フォント対応改善)。
|
||||||
|
|
||||||
|
## 簡易テスト
|
||||||
|
`src-python/overlay_ruby_test.py` を実行すると `overlay_small_ruby_test.png` が生成され、縦順と配置を確認できます。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PowerShell (仮想環境有効化後)
|
||||||
|
python src-python/overlay_ruby_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
UI スケーリングは OpenVR 側の表示サイズのみ変更し、画像内部フォントサイズは直接変更しません。ルビの視認性が低い場合は `ruby_font_scale` を調整してください。
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# test_client.py ドキュメント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
`test_client.py` は stdin/stdout 経由でバックエンド (`mainloop.py`) と通信し、各種 API エンドポイントを自動 / 半自動でテストするためのクライアントユーティリティ。初期化完了待機、ログ (status=348) 展開表示、サイレントモード、結果エクスポート(JSON/CSV)、Watchdog ハートビート送信 (/run/feed_watchdog) などの補助機能を備える。
|
||||||
|
|
||||||
|
## 主な責務
|
||||||
|
- バックエンドプロセス起動と初期化完了待機 (`/run/initialization_complete`)
|
||||||
|
- エンドポイント単発送信/応答待機 (Base64 エンコード/デコード)
|
||||||
|
- status=348 ログエントリの全文展開表示
|
||||||
|
- タイムアウト / 例外発生時の復旧メッセージ出力
|
||||||
|
- Watchdog ハートビート送信スレッド管理
|
||||||
|
- 自動テスター (`AutomatedEndpointTester`) による包括的エンドポイント試験
|
||||||
|
- テスト結果の JSON / CSV エクスポート (インタラクティブ指定)
|
||||||
|
|
||||||
|
## クラス構成
|
||||||
|
### `Color`
|
||||||
|
ANSIカラー定数を定義し、可読性の高い出力を実現。
|
||||||
|
|
||||||
|
### `TestClient`
|
||||||
|
バックエンドとの 1 プロセス・1 チャンネル通信を管理。
|
||||||
|
|
||||||
|
| 属性 | 役割 |
|
||||||
|
|------|------|
|
||||||
|
| `process` | 起動した Python バックエンド subprocess |
|
||||||
|
| `_watchdog_stop_event` | Watchdog スレッド停止制御用 Event |
|
||||||
|
| `_watchdog_thread` | /run/feed_watchdog 送信スレッド |
|
||||||
|
|
||||||
|
#### 初期化フロー
|
||||||
|
1. `subprocess.Popen([sys.executable, 'mainloop.py'], ...)` でバックエンド起動
|
||||||
|
2. `_wait_for_initialization()` を呼び出し `/run/initialization_complete` 受信まで待機
|
||||||
|
- VRCT_INIT_TIMEOUT 環境変数があれば soft timeout として利用 (超過時 WARN のみ)
|
||||||
|
- 30 秒間隔で進捗ログ (最後に受信した endpoint)
|
||||||
|
- status=348 レコードは全文 JSON 展開
|
||||||
|
3. 初期化完了後 `_start_watchdog()` がバックグラウンドで /run/feed_watchdog を 30 秒間隔送信
|
||||||
|
|
||||||
|
#### 重要メソッド
|
||||||
|
- `send_request(endpoint, data=None, timeout=30.0, silent=False)`
|
||||||
|
- リクエスト JSON を構築し送信
|
||||||
|
- `data` は JSON シリアライズ → Base64 → `data` フィールド
|
||||||
|
- 指定 endpoint のレスポンス行まで逐次読み取り (他 endpoint のログ行は通過)
|
||||||
|
- status=348 の場合ログとして全文表示(silent=False のとき)
|
||||||
|
- タイムアウト時 504 レスポンスを合成
|
||||||
|
|
||||||
|
- `_wait_for_initialization(timeout=None)`
|
||||||
|
- 無期限または soft timeout 待機
|
||||||
|
- プロセス死亡検知で RuntimeError
|
||||||
|
|
||||||
|
- `_start_watchdog()` / `cleanup()`
|
||||||
|
- Watchdog スレッド開始と安全停止 (Event セット後 join)
|
||||||
|
|
||||||
|
### `AutomatedEndpointTester`
|
||||||
|
`backend_test.py` のロジック移植版(stdin/stdout プロトコル向け)。
|
||||||
|
|
||||||
|
| 属性 | 説明 |
|
||||||
|
|------|------|
|
||||||
|
| `silent` | True ならクライアント側詳細出力を抑制 |
|
||||||
|
| `export_path` | JSON エクスポート先 (None なら未出力) |
|
||||||
|
| `export_csv` | CSV 追加出力有無 |
|
||||||
|
| `results` | 収集したテストレコード一覧 |
|
||||||
|
|
||||||
|
#### エンドポイント分類 (ハードコード暫定)
|
||||||
|
- 有効/無効化: `/set/enable/*`, `/set/disable/*`
|
||||||
|
- 設定更新: `/set/data/*`
|
||||||
|
- 実行系: `/run/*`
|
||||||
|
- 削除系: `/delete/data/*`
|
||||||
|
|
||||||
|
#### 主メソッド
|
||||||
|
- `test_validity_single(endpoint)` 有効/無効化系単一試験
|
||||||
|
- `test_set_data_single(endpoint)` 設定更新系単一試験(事前に動的取得が必要な値は `/get/data/...` を呼び最新値をキャッシュ)
|
||||||
|
- `test_run_single(endpoint)` 実行系単一試験
|
||||||
|
- `test_delete_single(endpoint)` 削除系単一試験
|
||||||
|
- `run_all()` 全カテゴリ順次実行
|
||||||
|
- `run_random(count=1000)` 全エンドポイントプールからランダム選択
|
||||||
|
- `run_specific_random(category, count)` 指定カテゴリ内ランダム
|
||||||
|
- `summary()` 結果集計出力および必要な場合 JSON/CSV エクスポート
|
||||||
|
|
||||||
|
#### 結果レコード構造
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "/set/data/transparency",
|
||||||
|
"status": 200,
|
||||||
|
"result": 85,
|
||||||
|
"expected": "status==200"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV 例:
|
||||||
|
```
|
||||||
|
endpoint,status,expected,success
|
||||||
|
/set/data/transparency,200,status==200,True
|
||||||
|
```
|
||||||
|
|
||||||
|
## status=348 ログ取り扱い
|
||||||
|
- 初期化待機中: 展開してインデント付き表示
|
||||||
|
- リクエスト応答処理中: silent=False ならログエントリ全文を優先表示
|
||||||
|
- 通常 API 応答 (status != 348) との区別を明確化しデバッグ容易化
|
||||||
|
|
||||||
|
## Watchdog ハートビート
|
||||||
|
- 30 秒間隔で `/run/feed_watchdog` を送信 (fire-and-forget)
|
||||||
|
- 送信失敗時は警告を表示しループ終了
|
||||||
|
- クライアント終了時に停止イベントをセットしスレッド join でリーク防止
|
||||||
|
|
||||||
|
## エクスポート機能
|
||||||
|
### JSON エクスポート
|
||||||
|
- `export_path` が指定されていれば `results` を UTF-8 / ensure_ascii=False で整形出力
|
||||||
|
- フィールド: endpoint, status, result, expected, success
|
||||||
|
|
||||||
|
### CSV エクスポート
|
||||||
|
- JSON パスから拡張子置換(`.csv`)で派生 (内部 `_derive_csv_path` 相当ロジック)
|
||||||
|
- 成功判定: `status` と `expected` 文字列評価結果に依存 (単純比較)
|
||||||
|
|
||||||
|
## 例外 / エラー処理方針
|
||||||
|
| ケース | 対応 |
|
||||||
|
|--------|------|
|
||||||
|
| バックエンド終了検知 | 初期化待機中: RuntimeError を投げる / 通常通信: 500 レスポンス合成 |
|
||||||
|
| JSONDecodeError | ログ行扱いでスキップ (初期化中は進捗ログとして表示) |
|
||||||
|
| BrokenPipe / OSError | 通信切断とみなして 500 レスポンス返却 |
|
||||||
|
| タイムアウト | 504 レスポンス返却 (endpoint 同梱) |
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
- エンドポイント一覧は動的取得ではなくハードコード (将来的改善余地)
|
||||||
|
- レスポンスの並行受信は未対応(1 リクエスト同期待ち)
|
||||||
|
- status=200 以外の詳細な意味的検証は限定的 (expected = 単純条件)
|
||||||
|
- Watchdog レスポンスは読み取らないため送信失敗検知は例外経路のみ
|
||||||
|
|
||||||
|
## 今後の改善候補
|
||||||
|
1. CLI 引数サポート (`--mode random --count 500 --silent --export result.json`)
|
||||||
|
2. 動的エンドポイント列挙 API 追加後の自動反映
|
||||||
|
3. リトライポリシー (指数バックオフ) 導入
|
||||||
|
4. 応答時間測定とパフォーマンスレポート出力
|
||||||
|
5. 並列テスト実行 (複数 subprocess / async IO)
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
- `backend_test.py` : 元ロジック
|
||||||
|
- `utils.py` : status=348 ログ出力仕様
|
||||||
|
- `mainloop.md` : 通信プロトコル詳細
|
||||||
|
|
||||||
|
## まとめ
|
||||||
|
`test_client.py` は VRCT バックエンドに対する包括的なテストおよび運用補助を 1 ファイルで実現するツール。初期化待機の堅牢化(無期限 + soft timeout)、Watchdog ハートビート、ログ展開、静音化オプション、結果エクスポートにより長時間動作・回帰試験・CI への組み込みを容易にする基盤を提供する。
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# test_endpoints.py - APIエンドポイントテストモジュール
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
VRCTアプリケーションのAPIエンドポイントを包括的にテストするためのモジュールです。メインループの各種機能をランダムアクセスでテストし、システムの安定性と堅牢性を検証します。
|
||||||
|
|
||||||
|
### 2025-10 更新点(commit a54538e 反映)
|
||||||
|
|
||||||
|
動的値が必要な `/set/data/*` エンドポイントに対し、事前に対応する `/get/data/...` エンドポイントを呼び出して最新値を取得し、`self.config_dict` にキャッシュしてからランダム選択する方式に変更しました。これにより以下が改善されています:
|
||||||
|
|
||||||
|
- 翻訳/転写エンジン選択時の存在しないキー送信防止
|
||||||
|
- 計算デバイス / 重みタイプ選択の安定化(リスト更新後に選択)
|
||||||
|
- マイク/スピーカー関連タイムアウト値の正常範囲チェック(取得した最新値を基準にバリデーション)
|
||||||
|
- Whisper / CTranslate2 重みタイプ辞書のキー集合変化への追従
|
||||||
|
|
||||||
|
例: `"/set/data/selected_translation_engines"` 試験前に `"/get/data/selectable_translation_engines"` を呼び、取得したリストから `random.choice()`。従来の初期キャッシュ依存から、実行時取得へ移行。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
|
||||||
|
### Color クラス
|
||||||
|
|
||||||
|
- ANSIエスケープシーケンスを使用したコンソール出力色彩管理
|
||||||
|
- テスト結果の視覚的表示(成功・失敗・スキップ等)
|
||||||
|
|
||||||
|
### TestMainloop クラス
|
||||||
|
|
||||||
|
- APIエンドポイントの包括的テスト実行
|
||||||
|
- ランダムアクセステスト
|
||||||
|
- テスト結果の記録・分析
|
||||||
|
- VRCTメインループとの統合テスト
|
||||||
|
|
||||||
|
## 主要メソッド
|
||||||
|
|
||||||
|
### テスト実行メソッド
|
||||||
|
|
||||||
|
- `test_endpoints_on_off_all()`: ON/OFF系エンドポイントの全テスト
|
||||||
|
- `test_set_data_endpoints_all()`: データ設定系エンドポイントの全テスト
|
||||||
|
- `test_run_endpoints_all()`: 実行系エンドポイントの全テスト
|
||||||
|
- `test_endpoints_all_random()`: 全エンドポイントのランダムアクセステスト
|
||||||
|
|
||||||
|
### 特定機能テスト
|
||||||
|
|
||||||
|
- `test_translate_all_language_pairs()`: 全言語ペアでの翻訳テスト
|
||||||
|
- `test_endpoints_on_off_continuous()`: ON/OFF連続切り替えテスト
|
||||||
|
- `test_endpoints_specific_random()`: 特定エンドポイントのランダムテスト
|
||||||
|
|
||||||
|
### 結果分析
|
||||||
|
|
||||||
|
- `generate_summary()`: テスト結果のサマリー生成
|
||||||
|
- `record_test_result()`: テスト結果の記録
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本的な使い方
|
||||||
|
|
||||||
|
```python
|
||||||
|
# テストインスタンスを作成
|
||||||
|
test = TestMainloop()
|
||||||
|
|
||||||
|
# 各種テストを実行
|
||||||
|
test.test_endpoints_on_off_all()
|
||||||
|
test.test_set_data_endpoints_all()
|
||||||
|
test.test_run_endpoints_all()
|
||||||
|
|
||||||
|
# テスト結果のサマリー表示
|
||||||
|
test.generate_summary()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ランダムテストの実行
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 全エンドポイントのランダムアクセステスト
|
||||||
|
test.test_endpoints_all_random()
|
||||||
|
|
||||||
|
# 特定エンドポイントのランダムテスト
|
||||||
|
test.test_endpoints_specific_random()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
- `mainloop`: VRCTメインループモジュール
|
||||||
|
- `random`: ランダムテストデータ生成
|
||||||
|
- `time`: テスト間隔制御
|
||||||
|
|
||||||
|
## テスト対象エンドポイント
|
||||||
|
|
||||||
|
### 制御系
|
||||||
|
|
||||||
|
- `/set/enable/*`: 機能有効化
|
||||||
|
- `/set/disable/*`: 機能無効化
|
||||||
|
|
||||||
|
### データ設定系
|
||||||
|
|
||||||
|
- `/set/data/*`: 各種設定データの更新
|
||||||
|
|
||||||
|
動的取得対象(代表例):
|
||||||
|
|
||||||
|
- `selected_translation_engines` → `/get/data/selectable_translation_engines`
|
||||||
|
- `selected_transcription_engine` → `/get/data/selectable_transcription_engines`
|
||||||
|
- `selected_translation_compute_device` → `/get/data/selectable_translation_compute_device_list`
|
||||||
|
- `ctranslate2_weight_type` → `/get/data/selectable_ctranslate2_weight_type_dict`
|
||||||
|
- `whisper_weight_type` → `/get/data/selectable_whisper_weight_type_dict`
|
||||||
|
- `selected_mic_host` / `selected_mic_device` → `/get/data/selectable_mic_host_list` / `/get/data/selectable_mic_device_list`
|
||||||
|
- `selected_speaker_device` → `/get/data/selectable_speaker_device_list`
|
||||||
|
|
||||||
|
### 実行系
|
||||||
|
|
||||||
|
- `/run/*`: 各種機能の実行
|
||||||
|
|
||||||
|
### データ削除系
|
||||||
|
|
||||||
|
- `/delete/data/*`: データの削除
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- テスト実行前に`config.json`を削除して初期化
|
||||||
|
- 重いAIモデルを使用するテストは実行時間に注意
|
||||||
|
- ランダムテストは指定回数(デフォルト1000-10000回)実行される
|
||||||
|
- テスト終了時は自動的にすべての機能を無効化する
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
- 各テストは独立して実行され、一つの失敗が全体に影響しない
|
||||||
|
- 期待されるステータスコードと実際の結果を比較
|
||||||
|
- VRAM不足等のリソースエラーも適切にハンドリング
|
||||||
|
|
||||||
|
## テスト結果の分類
|
||||||
|
|
||||||
|
- **PASS**: 期待されるステータスコードと一致
|
||||||
|
- **ERROR**: 期待されるステータスコードと不一致
|
||||||
|
- **SKIP**: テスト実行不可(401ステータス)
|
||||||
|
- **Invalid**: 無効なエンドポイント(404ステータス)
|
||||||
@@ -0,0 +1,940 @@
|
|||||||
|
# utils.py ドキュメント
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
`utils.py` は VRCT アプリケーション全体で使用される汎用ユーティリティ関数とロギング機能を提供するモジュール。辞書構造の検証、ネットワーク接続確認、計算デバイス管理、Base64エンコーディング、構造化ログ出力など、複数のサブシステムで共有される基盤機能を集約している。
|
||||||
|
|
||||||
|
## 主要機能
|
||||||
|
- 辞書構造の厳密な検証
|
||||||
|
- ネットワーク接続状態の診断
|
||||||
|
- WebSocketサーバーのアドレス可用性チェック
|
||||||
|
- IPアドレスのバリデーション
|
||||||
|
- CUDA/CPU計算デバイスの検出と最適化
|
||||||
|
- 構造化ログ出力(process.log, error.log)
|
||||||
|
- Base64エンコード/デコード
|
||||||
|
- ログファイルのローテーション管理
|
||||||
|
|
||||||
|
## アーキテクチャ上の位置づけ
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ All Modules │ (controller, model, device_manager, etc.)
|
||||||
|
└────────┬────────┘
|
||||||
|
│ Import
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ utils.py │ ◄── このファイル
|
||||||
|
└─────────────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ External Deps │ (torch, ctranslate2, requests, ipaddress)
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
全てのモジュールから参照される共通基盤として機能し、循環参照を避けるため他の内部モジュールへの依存を持たない。
|
||||||
|
|
||||||
|
## 依存関係
|
||||||
|
|
||||||
|
### 標準ライブラリ
|
||||||
|
```python
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from typing import Any, List, Dict, Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
### サードパーティライブラリ(オプション依存)
|
||||||
|
```python
|
||||||
|
import torch # GPU検出用(インポート失敗時はNoneにフォールバック)
|
||||||
|
from ctranslate2 import get_supported_compute_types # 計算タイプ取得用
|
||||||
|
import requests # ネットワーク接続確認用
|
||||||
|
import ipaddress # IPアドレス検証用
|
||||||
|
import socket # WebSocketサーバー可用性チェック用
|
||||||
|
```
|
||||||
|
|
||||||
|
**セーフガードインポート:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except Exception:
|
||||||
|
torch = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ctranslate2 import get_supported_compute_types
|
||||||
|
except Exception:
|
||||||
|
def get_supported_compute_types(device: str, device_index: int) -> List[str]:
|
||||||
|
return []
|
||||||
|
```
|
||||||
|
|
||||||
|
オプション依存が満たされない環境でもモジュールは正常にロード可能。
|
||||||
|
|
||||||
|
## 関数リファレンス
|
||||||
|
|
||||||
|
### 1. 辞書構造検証
|
||||||
|
|
||||||
|
#### `validateDictStructure(data: dict, structure: dict) -> bool`
|
||||||
|
|
||||||
|
**責務:** 辞書の構造と型が期待される仕様と完全に一致するかを検証
|
||||||
|
|
||||||
|
**アルゴリズム:**
|
||||||
|
1. 両方が辞書型であることを確認
|
||||||
|
2. キーの数と名前が完全一致するかチェック
|
||||||
|
3. 各キーの値について:
|
||||||
|
- 期待値が辞書の場合: 再帰的に検証(多重入れ子対応)
|
||||||
|
- 期待値が型オブジェクトの場合: `isinstance()` で型チェック
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `data` (dict): 検証対象の辞書
|
||||||
|
- `structure` (dict): 期待される構造定義
|
||||||
|
- 値には型(str, int, bool等)または入れ子の辞書を指定
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- `True`: 構造が完全に一致
|
||||||
|
- `False`: 不一致(キー不足、余分なキー、型不一致等)
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
# 単純な構造検証
|
||||||
|
data = {"name": "Alice", "age": 30}
|
||||||
|
structure = {"name": str, "age": int}
|
||||||
|
assert validateDictStructure(data, structure) is True
|
||||||
|
|
||||||
|
# 入れ子構造の検証
|
||||||
|
data = {
|
||||||
|
"user": {
|
||||||
|
"id": 123,
|
||||||
|
"profile": {"name": "Bob", "active": True}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
structure = {
|
||||||
|
"user": {
|
||||||
|
"id": int,
|
||||||
|
"profile": {"name": str, "active": bool}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert validateDictStructure(data, structure) is True
|
||||||
|
|
||||||
|
# 不一致の検出
|
||||||
|
data = {"name": "Alice", "extra_key": "value"}
|
||||||
|
structure = {"name": str, "age": int}
|
||||||
|
assert validateDictStructure(data, structure) is False # キーが不一致
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用場面:**
|
||||||
|
- フロントエンドからのリクエストペイロード検証
|
||||||
|
- 設定ファイルのスキーマ検証
|
||||||
|
- API レスポンスの構造確認
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ネットワーク診断
|
||||||
|
|
||||||
|
#### `isConnectedNetwork(url: str = "http://www.google.com", timeout: int = 3) -> bool`
|
||||||
|
|
||||||
|
**責務:** インターネット接続の可用性を高速チェック
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
1. 指定URLに HTTP GET リクエストを送信
|
||||||
|
2. `timeout` 秒以内に 200 OK レスポンスを受信したら接続あり
|
||||||
|
3. タイムアウトまたは例外発生時は接続なし
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `url` (str): 接続確認先URL(デフォルト: Google)
|
||||||
|
- `timeout` (int): タイムアウト時間(秒)
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- `True`: ネットワーク接続あり
|
||||||
|
- `False`: ネットワーク接続なし
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
if isConnectedNetwork():
|
||||||
|
# モデルウェイトをダウンロード
|
||||||
|
downloadModelWeights()
|
||||||
|
else:
|
||||||
|
# オフラインモードで動作
|
||||||
|
useLocalModels()
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事項:**
|
||||||
|
- ファイアウォールやプロキシ環境では正しく動作しない場合がある
|
||||||
|
- 初期化時の1回のみチェックを推奨(頻繁な呼び出しは避ける)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `isAvailableWebSocketServer(host: str, port: int) -> bool`
|
||||||
|
|
||||||
|
**責務:** 指定したホスト/ポートでWebSocketサーバーが起動可能かを確認
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
1. TCP ソケットを作成
|
||||||
|
2. `SO_REUSEADDR` オプションを設定
|
||||||
|
3. `bind()` を試行
|
||||||
|
4. 成功 → アドレス利用可能、失敗 → アドレス使用中
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `host` (str): バインドするIPアドレス
|
||||||
|
- `port` (int): バインドするポート番号
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- `True`: アドレスが利用可能
|
||||||
|
- `False`: アドレスが使用中
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
if isAvailableWebSocketServer("127.0.0.1", 8080):
|
||||||
|
startWebSocketServer("127.0.0.1", 8080)
|
||||||
|
else:
|
||||||
|
print("Port 8080 is already in use")
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事項:**
|
||||||
|
- `SO_REUSEADDR` により、TIME_WAIT 状態のアドレスも利用可能と判定される
|
||||||
|
- 管理者権限が必要なポート(1024未満)では失敗する場合がある
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `isValidIpAddress(ip_address: str) -> bool`
|
||||||
|
|
||||||
|
**責務:** IPv4/IPv6アドレスの妥当性を検証
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
- `ipaddress.ip_address()` でパース
|
||||||
|
- 成功 → 有効なIPアドレス、失敗 → 無効
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `ip_address` (str): 検証対象のIPアドレス文字列
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- `True`: 有効なIPアドレス
|
||||||
|
- `False`: 無効なIPアドレス
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
assert isValidIpAddress("127.0.0.1") is True
|
||||||
|
assert isValidIpAddress("2001:db8::1") is True
|
||||||
|
assert isValidIpAddress("invalid") is False
|
||||||
|
```
|
||||||
|
|
||||||
|
**サポート形式:**
|
||||||
|
- IPv4: "192.168.1.1", "127.0.0.1"
|
||||||
|
- IPv6: "2001:db8::1", "fe80::1"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 計算デバイス管理
|
||||||
|
|
||||||
|
#### `getComputeDeviceList() -> List[Dict[str, Any]]`
|
||||||
|
|
||||||
|
**責務:** 利用可能な計算デバイス(CPU/GPU)とサポートされる計算タイプを列挙
|
||||||
|
|
||||||
|
**返り値構造:**
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"device": "cpu",
|
||||||
|
"device_index": 0,
|
||||||
|
"device_name": "cpu",
|
||||||
|
"compute_types": ["auto", "float32", "int8", ...]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device": "cuda",
|
||||||
|
"device_index": 0,
|
||||||
|
"device_name": "NVIDIA GeForce RTX 3090",
|
||||||
|
"compute_types": ["auto", "int8_bfloat16", "int8_float16", ...]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**処理フロー:**
|
||||||
|
1. CPU デバイスを常に追加(最低限の計算環境を保証)
|
||||||
|
2. PyTorch と CUDA が利用可能な場合:
|
||||||
|
- 全GPUデバイスを列挙
|
||||||
|
- 各GPUの計算タイプを `get_supported_compute_types()` で取得
|
||||||
|
- GPU アーキテクチャに応じて計算タイプを制限:
|
||||||
|
- **GTX シリーズ**: `int8_bfloat16`, `bfloat16`, `float16`, `int8` を除外
|
||||||
|
- **RTX, Tesla, A100, Quadro**: 全計算タイプをサポート
|
||||||
|
- **その他**: `float32` のみ
|
||||||
|
|
||||||
|
**GPU別の計算タイプ制限:**
|
||||||
|
```python
|
||||||
|
if "GTX" in gpu_device_name:
|
||||||
|
unsupported_types = {"int8_bfloat16", "bfloat16", "float16", "int8"}
|
||||||
|
gpu_compute_types = [t for t in gpu_compute_types if t not in unsupported_types]
|
||||||
|
elif not any(keyword in gpu_device_name for keyword in ["RTX", "Tesla", "A100", "Quadro"]):
|
||||||
|
gpu_compute_types = ["float32"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
devices = getComputeDeviceList()
|
||||||
|
for device in devices:
|
||||||
|
print(f"{device['device_name']}: {', '.join(device['compute_types'])}")
|
||||||
|
|
||||||
|
# 出力例:
|
||||||
|
# cpu: auto, float32, int8
|
||||||
|
# NVIDIA GeForce RTX 3090: auto, int8_bfloat16, int8_float16, int8, bfloat16, float16, int8_float32, float32
|
||||||
|
```
|
||||||
|
|
||||||
|
**エラーハンドリング:**
|
||||||
|
- GPU検出中の例外は `errorLogging()` でログ記録し、CPU デバイスのみ返却
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `getBestComputeType(device: str, device_index: int) -> str`
|
||||||
|
|
||||||
|
**責務:** デバイスアーキテクチャに最適な計算タイプを自動選択
|
||||||
|
|
||||||
|
**優先順位:**
|
||||||
|
```python
|
||||||
|
preferred_types = {
|
||||||
|
"default": [
|
||||||
|
"int8_bfloat16", # 最も効率的(対応GPUのみ)
|
||||||
|
"int8_float16", # 2番目に効率的
|
||||||
|
"int8", # 整数演算高速化
|
||||||
|
"bfloat16", # 混合精度
|
||||||
|
"float16", # 半精度浮動小数点
|
||||||
|
"int8_float32", # 互換性重視
|
||||||
|
"float32" # フォールバック
|
||||||
|
],
|
||||||
|
"GTX": ["float32"], # GTXシリーズは制限あり
|
||||||
|
"RTX": ["int8_bfloat16", "int8_float16", ...],
|
||||||
|
"Tesla": [...],
|
||||||
|
"A100": [...],
|
||||||
|
"Quadro": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**処理フロー:**
|
||||||
|
1. `get_supported_compute_types()` で利用可能な計算タイプを取得
|
||||||
|
2. デバイス名に基づいて優先リストを選択
|
||||||
|
3. 優先順に計算タイプをチェックし、最初に利用可能なものを返却
|
||||||
|
4. 全て利用不可の場合は `"float32"` を返却(安全なフォールバック)
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `device` (str): "cpu" または "cuda"
|
||||||
|
- `device_index` (int): GPUデバイスのインデックス(CPUの場合は0)
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- 最適な計算タイプ文字列(例: "int8_bfloat16", "float32")
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
best_type = getBestComputeType("cuda", 0)
|
||||||
|
model.load_model(compute_type=best_type)
|
||||||
|
```
|
||||||
|
|
||||||
|
**計算タイプの特性:**
|
||||||
|
|
||||||
|
| 計算タイプ | メモリ使用量 | 速度 | 精度 | 対応GPU |
|
||||||
|
|----------|------------|------|------|--------|
|
||||||
|
| int8_bfloat16 | 最小 | 最速 | 高 | RTX 30xx以降 |
|
||||||
|
| int8_float16 | 最小 | 最速 | 高 | RTX 20xx以降 |
|
||||||
|
| int8 | 小 | 高速 | 中 | 多くのGPU |
|
||||||
|
| bfloat16 | 中 | 高速 | 高 | RTX 30xx以降 |
|
||||||
|
| float16 | 中 | 高速 | 高 | RTX 20xx以降 |
|
||||||
|
| float32 | 大 | 標準 | 最高 | 全GPU/CPU |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. エンコーディング
|
||||||
|
|
||||||
|
#### `encodeBase64(data: str) -> Dict[str, Any]`
|
||||||
|
|
||||||
|
**責務:** Base64エンコードされたJSON文字列をデコードしてパース
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
1. Base64デコード
|
||||||
|
2. UTF-8文字列に変換
|
||||||
|
3. JSON パース
|
||||||
|
4. 失敗時は空の辞書を返却
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `data` (str): Base64エンコードされたJSON文字列
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- パース成功: JSON オブジェクト
|
||||||
|
- パース失敗: `{}`(空の辞書)
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
# エンコード例(参考)
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
payload = {"message": "Hello", "id": 123}
|
||||||
|
encoded = base64.b64encode(json.dumps(payload).encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
# デコード
|
||||||
|
decoded = encodeBase64(encoded)
|
||||||
|
assert decoded == {"message": "Hello", "id": 123}
|
||||||
|
```
|
||||||
|
|
||||||
|
**エラーハンドリング:**
|
||||||
|
- 不正なBase64文字列
|
||||||
|
- 不正なJSON形式
|
||||||
|
- 文字エンコーディングエラー
|
||||||
|
|
||||||
|
全て `errorLogging()` でログ記録し、空の辞書を返却。
|
||||||
|
|
||||||
|
**注意事項:**
|
||||||
|
- 関数名が `encodeBase64` だが、実際には**デコード**を行う(命名の歴史的経緯)
|
||||||
|
- セキュリティ: Base64は暗号化ではないため、機密情報の保護には使用しない
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. ロギング
|
||||||
|
|
||||||
|
#### `removeLog() -> None`
|
||||||
|
|
||||||
|
**責務:** プロセスログファイル(process.log)を初期化
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
- `process.log` を空の内容で上書き
|
||||||
|
- ファイルが存在しない場合は新規作成
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
# アプリケーション起動時にログをクリア
|
||||||
|
removeLog()
|
||||||
|
printLog("Application started")
|
||||||
|
```
|
||||||
|
|
||||||
|
**エラーハンドリング:**
|
||||||
|
- ファイル書き込み失敗時は `errorLogging()` でエラーログに記録
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `setupLogger(name: str, log_file: str, level: int = logging.INFO) -> logging.Logger`
|
||||||
|
|
||||||
|
**責務:** ローテーション機能付きロガーインスタンスを生成
|
||||||
|
|
||||||
|
**設定:**
|
||||||
|
- **最大ログサイズ**: 10MB
|
||||||
|
- **バックアップ数**: 1(最大2ファイル)
|
||||||
|
- **ローテーション動作**: 10MB到達時に `.1` バックアップを作成し、新規ログを開始
|
||||||
|
- **エンコーディング**: UTF-8
|
||||||
|
- **遅延書き込み**: `delay=True`(最初の書き込み時にファイルを開く)
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `name` (str): ロガー名(例: "process", "error")
|
||||||
|
- `log_file` (str): ログファイルパス
|
||||||
|
- `level` (int): ログレベル(デフォルト: `logging.INFO`)
|
||||||
|
|
||||||
|
**返り値:**
|
||||||
|
- 設定済み `logging.Logger` インスタンス
|
||||||
|
|
||||||
|
**ログフォーマット:**
|
||||||
|
```
|
||||||
|
%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||||
|
```
|
||||||
|
|
||||||
|
**出力例:**
|
||||||
|
```
|
||||||
|
2025-10-13 14:30:45,123 - process - INFO - Application started
|
||||||
|
2025-10-13 14:30:46,456 - error - ERROR - Connection failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**重複ハンドラー防止:**
|
||||||
|
```python
|
||||||
|
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == getattr(file_handler, 'baseFilename', None) for h in logger.handlers):
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
同じファイルへの重複ハンドラー追加を防止し、複数回呼び出されても安全。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `printLog(log: str, data: Any = None) -> None`
|
||||||
|
|
||||||
|
**責務:** 構造化プロセスログの出力
|
||||||
|
|
||||||
|
**出力先:**
|
||||||
|
1. `process.log` ファイル
|
||||||
|
2. 標準出力(JSON形式)
|
||||||
|
|
||||||
|
**出力形式:**
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"status": 348, # プロセスログ専用ステータス
|
||||||
|
"log": "User action performed",
|
||||||
|
"data": "additional context"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `log` (str): ログメッセージ
|
||||||
|
- `data` (Any): 追加のコンテキスト情報(オプション)
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
printLog("Model loading started", {"model_type": "whisper", "weight": "medium"})
|
||||||
|
# 出力(stdout):
|
||||||
|
# {"status": 348, "log": "Model loading started", "data": "{'model_type': 'whisper', 'weight': 'medium'}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**実装の詳細:**
|
||||||
|
```python
|
||||||
|
global process_logger
|
||||||
|
if process_logger is None:
|
||||||
|
process_logger = setupLogger("process", "process.log", logging.INFO)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"status": 348,
|
||||||
|
"log": log,
|
||||||
|
"data": str(data),
|
||||||
|
}
|
||||||
|
process_logger.info(response)
|
||||||
|
serialized = json.dumps(response)
|
||||||
|
print(serialized, flush=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事項:**
|
||||||
|
- `data` は `str()` で文字列化されるため、複雑なオブジェクトは読みにくくなる可能性がある
|
||||||
|
- `flush=True` により即座に出力(バッファリングを無効化)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `printResponse(status: int, endpoint: str, result: Any = None) -> None`
|
||||||
|
|
||||||
|
**責務:** 構造化APIレスポンスの出力
|
||||||
|
|
||||||
|
**出力先:**
|
||||||
|
1. `process.log` ファイル
|
||||||
|
2. 標準出力(JSON形式)
|
||||||
|
|
||||||
|
**出力形式:**
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"endpoint": "/get/config/version",
|
||||||
|
"result": {"version": "3.3.0"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**引数:**
|
||||||
|
- `status` (int): HTTPステータスコード風のステータス番号
|
||||||
|
- `endpoint` (str): エンドポイント識別子
|
||||||
|
- `result` (Any): レスポンスペイロード(オプション)
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
printResponse(200, "/set/config/language", {"language": "ja"})
|
||||||
|
printResponse(400, "/set/config/threshold", {"error": "Value out of range"})
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSONシリアライズエラーハンドリング:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
serialized_response = json.dumps(response)
|
||||||
|
except Exception as e:
|
||||||
|
errorLogging() # 完全なトレースバックをログ
|
||||||
|
process_logger.error(f"Problematic response object: {response}")
|
||||||
|
process_logger.error(f"Exception during json.dumps: {e}")
|
||||||
|
# フォールバックエラーペイロード
|
||||||
|
error_json = json.dumps({
|
||||||
|
"status": 500,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"result": {"error": "Failed to serialize response", "details": str(e)},
|
||||||
|
})
|
||||||
|
print(error_json, flush=True)
|
||||||
|
else:
|
||||||
|
print(serialized_response, flush=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**シリアライズ不可能なオブジェクトの例:**
|
||||||
|
- `datetime` オブジェクト
|
||||||
|
- カスタムクラスインスタンス
|
||||||
|
- 循環参照を持つ辞書
|
||||||
|
|
||||||
|
**対策:**
|
||||||
|
- `result` を構築する際に JSON シリアライズ可能な型のみ使用
|
||||||
|
- 必要に応じて `str()` や専用のシリアライザーで変換
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `errorLogging() -> None`
|
||||||
|
|
||||||
|
**責務:** 現在の例外トレースバックをエラーログに記録
|
||||||
|
|
||||||
|
**処理:**
|
||||||
|
1. `error.log` ファイルにトレースバックを出力
|
||||||
|
2. ロガー初期化失敗時は標準出力にフォールバック
|
||||||
|
|
||||||
|
**使用例:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
risky_operation()
|
||||||
|
except Exception:
|
||||||
|
errorLogging() # トレースバックをerror.logに記録
|
||||||
|
# 必要に応じて追加処理
|
||||||
|
```
|
||||||
|
|
||||||
|
**出力例(error.log):**
|
||||||
|
```
|
||||||
|
2025-10-13 14:35:12,789 - error - ERROR - Traceback (most recent call last):
|
||||||
|
File "model.py", line 123, in loadModel
|
||||||
|
model.load()
|
||||||
|
File "ctranslate2/model.py", line 456, in load
|
||||||
|
raise RuntimeError("CUDA out of memory")
|
||||||
|
RuntimeError: CUDA out of memory
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事項:**
|
||||||
|
- **例外コンテキスト内でのみ呼び出し可能**(`traceback.format_exc()` を使用)
|
||||||
|
- 例外をキャッチせずに呼び出すと空のトレースバックが記録される
|
||||||
|
|
||||||
|
**ベストプラクティス:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
dangerous_function()
|
||||||
|
except SpecificException as e:
|
||||||
|
errorLogging() # 詳細をログ
|
||||||
|
# ユーザーフレンドリーなエラー処理
|
||||||
|
printResponse(400, endpoint, {"error": "Operation failed"})
|
||||||
|
except Exception:
|
||||||
|
errorLogging() # 予期しないエラーもログ
|
||||||
|
raise # 上位へ伝播
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## グローバル変数
|
||||||
|
|
||||||
|
### `process_logger: Optional[logging.Logger] = None`
|
||||||
|
プロセスログ用のグローバルロガーインスタンス。初回 `printLog()` または `printResponse()` 呼び出し時に初期化される。
|
||||||
|
|
||||||
|
### `error_logger: Optional[logging.Logger] = None`
|
||||||
|
エラーログ用のグローバルロガーインスタンス。初回 `errorLogging()` 呼び出し時に初期化される。
|
||||||
|
|
||||||
|
**遅延初期化の理由:**
|
||||||
|
- モジュールインポート時のオーバーヘッド削減
|
||||||
|
- ファイルシステムへの不要なアクセスを回避
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## エラーハンドリング戦略
|
||||||
|
|
||||||
|
### 1. 防御的プログラミング
|
||||||
|
全てのユーティリティ関数は例外を内部で処理し、呼び出し元に例外を伝播しない:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool:
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=timeout)
|
||||||
|
return response.status_code == 200
|
||||||
|
except requests.RequestException:
|
||||||
|
return False # 例外をキャッチして安全な値を返却
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. フォールバック値
|
||||||
|
- `encodeBase64()`: パース失敗時は `{}`
|
||||||
|
- `getComputeDeviceList()`: GPU検出失敗時はCPUのみ
|
||||||
|
- `getBestComputeType()`: 全て失敗時は `"float32"`
|
||||||
|
|
||||||
|
### 3. ログ記録
|
||||||
|
全てのエラーは `errorLogging()` でトレースバックを記録し、デバッグを容易にする。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## パフォーマンス考慮事項
|
||||||
|
|
||||||
|
### 1. ネットワーク接続チェック
|
||||||
|
`isConnectedNetwork()` はブロッキング操作(最大3秒)のため、起動時の1回のみ実行を推奨:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 良い例
|
||||||
|
if isConnectedNetwork():
|
||||||
|
downloadModels()
|
||||||
|
|
||||||
|
# 悪い例(UI フリーズの原因)
|
||||||
|
while True:
|
||||||
|
if isConnectedNetwork(): # 毎回3秒待機
|
||||||
|
processData()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ログローテーション
|
||||||
|
10MB のログファイルローテーションにより、ディスク容量を制御(最大20MB)。
|
||||||
|
|
||||||
|
### 3. グローバルロガーの遅延初期化
|
||||||
|
ロガーは初回使用時に初期化されるため、インポート時のオーバーヘッドを最小化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用パターン
|
||||||
|
|
||||||
|
### パターン1: ネットワーク依存機能の初期化
|
||||||
|
```python
|
||||||
|
def initialize_online_features():
|
||||||
|
if not isConnectedNetwork():
|
||||||
|
printLog("Offline mode: skipping model download")
|
||||||
|
return
|
||||||
|
|
||||||
|
printLog("Online mode: downloading models")
|
||||||
|
downloadModels()
|
||||||
|
```
|
||||||
|
|
||||||
|
### パターン2: デバイス自動選択
|
||||||
|
```python
|
||||||
|
devices = getComputeDeviceList()
|
||||||
|
if len(devices) > 1:
|
||||||
|
# GPU利用可能
|
||||||
|
best_device = devices[1] # 最初のGPU
|
||||||
|
best_type = getBestComputeType(best_device["device"], best_device["device_index"])
|
||||||
|
printLog(f"Using GPU: {best_device['device_name']}", {"compute_type": best_type})
|
||||||
|
else:
|
||||||
|
# CPUのみ
|
||||||
|
printLog("No GPU detected, using CPU")
|
||||||
|
best_type = "float32"
|
||||||
|
```
|
||||||
|
|
||||||
|
### パターン3: 構造化リクエスト検証
|
||||||
|
```python
|
||||||
|
def handle_request(payload):
|
||||||
|
expected_structure = {
|
||||||
|
"action": str,
|
||||||
|
"data": {
|
||||||
|
"id": int,
|
||||||
|
"value": str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if not validateDictStructure(payload, expected_structure):
|
||||||
|
printResponse(400, "/handle_request", {"error": "Invalid request structure"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# 処理続行
|
||||||
|
printLog("Valid request received", payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
### パターン4: WebSocketサーバー起動
|
||||||
|
```python
|
||||||
|
def start_websocket(host, port):
|
||||||
|
if not isValidIpAddress(host):
|
||||||
|
printResponse(400, "/websocket/start", {"error": "Invalid IP address"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isAvailableWebSocketServer(host, port):
|
||||||
|
printResponse(400, "/websocket/start", {"error": f"Port {port} is in use"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# サーバー起動
|
||||||
|
printLog(f"Starting WebSocket server", {"host": host, "port": port})
|
||||||
|
startServer(host, port)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## テスト推奨事項
|
||||||
|
|
||||||
|
### 単体テスト例
|
||||||
|
|
||||||
|
**辞書構造検証:**
|
||||||
|
```python
|
||||||
|
def test_validate_dict_structure_simple():
|
||||||
|
data = {"name": "Alice", "age": 30}
|
||||||
|
structure = {"name": str, "age": int}
|
||||||
|
assert validateDictStructure(data, structure) is True
|
||||||
|
|
||||||
|
def test_validate_dict_structure_nested():
|
||||||
|
data = {"user": {"id": 1, "active": True}}
|
||||||
|
structure = {"user": {"id": int, "active": bool}}
|
||||||
|
assert validateDictStructure(data, structure) is True
|
||||||
|
|
||||||
|
def test_validate_dict_structure_invalid():
|
||||||
|
data = {"name": "Alice"}
|
||||||
|
structure = {"name": str, "age": int} # 'age'キーが不足
|
||||||
|
assert validateDictStructure(data, structure) is False
|
||||||
|
```
|
||||||
|
|
||||||
|
**ネットワーク診断:**
|
||||||
|
```python
|
||||||
|
def test_network_connection():
|
||||||
|
# 実際のネットワーク接続をテスト
|
||||||
|
result = isConnectedNetwork()
|
||||||
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
def test_network_timeout():
|
||||||
|
# タイムアウト動作を確認
|
||||||
|
result = isConnectedNetwork(url="http://192.0.2.1", timeout=1)
|
||||||
|
assert result is False
|
||||||
|
```
|
||||||
|
|
||||||
|
**計算デバイス:**
|
||||||
|
```python
|
||||||
|
def test_get_compute_device_list():
|
||||||
|
devices = getComputeDeviceList()
|
||||||
|
assert len(devices) >= 1 # 最低限CPUが含まれる
|
||||||
|
assert devices[0]["device"] == "cpu"
|
||||||
|
|
||||||
|
def test_get_best_compute_type():
|
||||||
|
compute_type = getBestComputeType("cpu", 0)
|
||||||
|
assert compute_type in ["float32", "int8"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**ロギング:**
|
||||||
|
```python
|
||||||
|
def test_print_log(capsys):
|
||||||
|
printLog("Test message", {"key": "value"})
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
output = json.loads(captured.out)
|
||||||
|
assert output["status"] == 348
|
||||||
|
assert output["log"] == "Test message"
|
||||||
|
|
||||||
|
def test_print_response(capsys):
|
||||||
|
printResponse(200, "/test", {"result": "success"})
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
output = json.loads(captured.out)
|
||||||
|
assert output["status"] == 200
|
||||||
|
assert output["endpoint"] == "/test"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## セキュリティ考慮事項
|
||||||
|
|
||||||
|
### 1. IPアドレス検証
|
||||||
|
`isValidIpAddress()` はフォーマット検証のみで、プライベートアドレス範囲のチェックは行わない:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# セキュリティを強化する場合
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
def is_public_ip(ip_str):
|
||||||
|
if not isValidIpAddress(ip_str):
|
||||||
|
return False
|
||||||
|
ip = ipaddress.ip_address(ip_str)
|
||||||
|
return not (ip.is_private or ip.is_loopback or ip.is_reserved)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Base64デコード
|
||||||
|
`encodeBase64()` は入力検証を行わないため、信頼できないソースからのデータには注意:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 安全な使用例
|
||||||
|
if source_is_trusted:
|
||||||
|
data = encodeBase64(base64_string)
|
||||||
|
else:
|
||||||
|
# 追加の検証を実施
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ログファイルへの機密情報記録
|
||||||
|
ログに機密情報(API キー、パスワード等)が含まれないよう注意:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 悪い例
|
||||||
|
printLog("API key loaded", api_key)
|
||||||
|
|
||||||
|
# 良い例
|
||||||
|
printLog("API key loaded", "***REDACTED***")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 制限事項
|
||||||
|
|
||||||
|
1. **プラットフォーム依存性:**
|
||||||
|
- GPU検出は CUDA 環境でのみ動作(ROCm/Metal非対応)
|
||||||
|
|
||||||
|
2. **ネットワークチェックの制限:**
|
||||||
|
- ファイアウォール、プロキシ環境で誤判定の可能性
|
||||||
|
- IPv6専用環境での動作は未検証
|
||||||
|
|
||||||
|
3. **ログファイルのスレッドセーフティ:**
|
||||||
|
- `RotatingFileHandler` は基本的にスレッドセーフだが、高負荷時のローテーション中にログ損失の可能性
|
||||||
|
|
||||||
|
4. **計算タイプの最適化:**
|
||||||
|
- `getBestComputeType()` の優先順位は一般的な推奨値であり、特定のモデルやタスクでは最適でない場合がある
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依存モジュールとの関係
|
||||||
|
|
||||||
|
### controller.py
|
||||||
|
- デバイス管理の設定変更時にデバイスリスト取得
|
||||||
|
- エラー時のログ記録
|
||||||
|
- ネットワーク接続確認
|
||||||
|
|
||||||
|
### model.py
|
||||||
|
- 計算デバイスとタイプの決定
|
||||||
|
- エラー時のトレースバック記録
|
||||||
|
|
||||||
|
### config.py
|
||||||
|
- 起動時のネットワーク接続確認
|
||||||
|
- 計算デバイスリストの提供
|
||||||
|
|
||||||
|
### mainloop.py
|
||||||
|
- リクエスト/レスポンスの構造化ログ出力
|
||||||
|
- エラー時のトレースバック記録
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 今後の拡張性
|
||||||
|
|
||||||
|
### 1. 非同期ネットワークチェック
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
async def isConnectedNetworkAsync(url="http://www.google.com", timeout=3) -> bool:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as response:
|
||||||
|
return response.status == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 構造化ログの拡張
|
||||||
|
```python
|
||||||
|
def printStructuredLog(level: str, message: str, context: dict = None):
|
||||||
|
"""
|
||||||
|
より詳細な構造化ログ出力
|
||||||
|
- timestamp
|
||||||
|
- level
|
||||||
|
- message
|
||||||
|
- context (key-value pairs)
|
||||||
|
- stack trace (error時)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. メトリクス収集
|
||||||
|
```python
|
||||||
|
def recordMetric(metric_name: str, value: float, tags: dict = None):
|
||||||
|
"""
|
||||||
|
パフォーマンスメトリクスの記録
|
||||||
|
- function execution time
|
||||||
|
- memory usage
|
||||||
|
- GPU utilization
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 関連ドキュメント
|
||||||
|
- `controller.md`: Controller での utils 関数使用例
|
||||||
|
- `config.md`: Config での計算デバイス管理
|
||||||
|
- `model.md`: Model でのエラーハンドリング
|
||||||
|
- `コーディングルール.md`: ロギングとエラーハンドリングの規約
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
プロジェクトのルートディレクトリの `LICENSE` ファイルを参照
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## まとめ
|
||||||
|
|
||||||
|
`utils.py` は VRCT プロジェクトの基盤インフラストラクチャとして、以下の重要な責務を担う:
|
||||||
|
|
||||||
|
1. **安全性**: 全ての関数が例外を内部処理し、安全なフォールバック値を提供
|
||||||
|
2. **可観測性**: 構造化ログとローテーション機能により、問題の診断を容易化
|
||||||
|
3. **互換性**: オプション依存のセーフガードにより、様々な環境で動作
|
||||||
|
4. **最適化**: GPU アーキテクチャに応じた計算タイプの自動選択
|
||||||
|
5. **検証**: 辞書構造、IPアドレス、ネットワーク接続の厳密なバリデーション
|
||||||
|
|
||||||
|
全てのサブシステムから依存される中核モジュールとして、高い信頼性と保守性を維持している。
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# VRCT backend — コーディングルール
|
||||||
|
|
||||||
|
目的:
|
||||||
|
- 可読性と保守性を保ちながら既存スタイルを尊重する。
|
||||||
|
- 漸進的に型注釈を導入し、mypy と ruff のチェックに合わせる。
|
||||||
|
- 自動化(CI / pre-commit)へ導出しやすくする。
|
||||||
|
|
||||||
|
注意: 既存の命名・構造(関数名・クラス名・変数名・run mapping のキー等)はコード上の互換性のためそのまま維持します。以下は新規実装やリファクタ時に従うべきルールです。
|
||||||
|
|
||||||
|
## 目次
|
||||||
|
- 命名規則
|
||||||
|
- モジュール・パッケージ構成
|
||||||
|
- インポート
|
||||||
|
- 型注釈と mypy 方針
|
||||||
|
- ドキュメンテーション / docstrings
|
||||||
|
- エラーハンドリングとロギング
|
||||||
|
- 非同期 / スレッド / キューの扱い
|
||||||
|
- テストと CI
|
||||||
|
- リファクタ・互換性の観点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命名規則
|
||||||
|
- モジュール名: 小文字、アンダースコアで区切る(例: `overlay_utils.py`)。既存ファイルに従う。
|
||||||
|
- パッケージ名: 小文字(`models`, `websocket` など)。
|
||||||
|
- クラス名: CapWords (PascalCase)。既存クラス(`Controller`, `Model`, `Overlay`)に従う。
|
||||||
|
- 関数・メソッド名: snake_case。
|
||||||
|
- 変数名: snake_case。短い一時変数は `i`, `j`, `buf` 等の伝統的な省略形を可とするが、意味ある名前を優先する。
|
||||||
|
- 定数: UPPER_SNAKE_CASE(`config.py` の定数に合わせる)。
|
||||||
|
- run_mapping のキー: 現在は短い key(例: `transcription_mic`)を内部で使い `run_mapping` に `/run/...` を置いている。この慣習は維持する。Controller 内で `self.run_mapping[...]` を直接参照する実装は許容される。
|
||||||
|
|
||||||
|
## モジュール・パッケージ構成
|
||||||
|
- 各サブ領域(ocr, overlay, transcription, translation, websocket 等)は `models/` 下に整理済みのため、同様の粒度で新機能は追加する。
|
||||||
|
- パッケージは必ず `__init__.py` を置く(static analysis / mypy のため)。空の `__init__.py` でも可。これにより相対インポートが安定する。
|
||||||
|
|
||||||
|
## インポート
|
||||||
|
- 標準ライブラリ、サードパーティ、ローカルの順でインポートをまとめる。
|
||||||
|
- ローカルモジュールを参照する場合は相対 import を使ってもよいが、プロジェクト全体を PYTHONPATH に入れてテスト/静的解析できるようにすること。
|
||||||
|
- 例:
|
||||||
|
```
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from . import overlay_utils
|
||||||
|
```
|
||||||
|
|
||||||
|
## 型注釈と mypy 方針
|
||||||
|
- 戦略: Relax + incremental annotations(漸進的型付け)。以下を守る。
|
||||||
|
- 新規コードは可能な限り型注釈を追加する(関数シグネチャ・返り値)。
|
||||||
|
- 既存の大きな関数は段階的に注釈する。まずモジュール境界(public API)のシグネチャに注釈を入れる。内部の細かい変数は後回し。
|
||||||
|
- CI では初期段階で mypy を `--ignore-missing-imports --allow-untyped-defs --allow-redefinition` のように緩めて実行する。段階的に `--check-untyped-defs` を有効化していく。
|
||||||
|
- 型の `Any` を多用しない。どうしても必要な場合は `# type: ignore[assignment]` を付けて理由をコメントに残す。
|
||||||
|
|
||||||
|
## docstrings / コメント
|
||||||
|
- 重要な public 関数・メソッドとクラスに短い docstring を追加する(目的・引数・返り値の要約)。Google/Numpy スタイルのどちらかに統一する必要はないが、プロジェクト内で混乱しないよう短く統一すること。
|
||||||
|
- 実装トリッキーな箇所には `# NOTE:` や `# FIXME:` コメントを残し、必要なら issue を紐付ける(例: `# NOTE: keep in sync with mainloop.run_mapping`)。
|
||||||
|
|
||||||
|
## エラーハンドリングとロギング
|
||||||
|
- 例外をキャッチするときは有用なコンテキストをログに残す(`errorLogging()` のようなユーティリティを使う)。
|
||||||
|
- broad except: を使う場合は最低限 `errorLogging()` を呼び、必要なら `raise` して上位へ伝播する。
|
||||||
|
|
||||||
|
## 非同期 / スレッド / キューの扱い
|
||||||
|
- スレッドは `threading.Thread` を使っている箇所があるため、スレッド間通信は `queue.Queue` ベースで実装すること。
|
||||||
|
- スレッドを生成する関数は `start_` プレフィックス(例: `start_transcription_thread`)のように命名すると分かりやすい。
|
||||||
|
|
||||||
|
## テスト・CI
|
||||||
|
- まずは軽量な CI ワークフローを入れる:
|
||||||
|
- ruff check
|
||||||
|
- mypy (relaxed)
|
||||||
|
- 自動テスト(将来的に pytest を追加)
|
||||||
|
- pre-commit フックの導入を推奨: ruff auto-fix と isort (import 整理) を採用できる。
|
||||||
|
|
||||||
|
## リファクタ・互換性
|
||||||
|
- 既存 public API(stdin/stdout の endpoint 仕様や `run_mapping` のキー、Controller のメソッド名)は後方互換を優先する。変更が必要な場合は CHANGELOG に明示する。
|
||||||
|
|
||||||
|
## 小さなコーディング規約チェックリスト(PR テンプレ)
|
||||||
|
- 新しい public メソッドには docstring を付けたか
|
||||||
|
- 既存命名規則に従っているか(snake_case / PascalCase / UPPER_SNAKE_CASE)
|
||||||
|
- 型アノテーションをシグネチャに追加したか(可能な限り)
|
||||||
|
- 直接 stdout に JSON を print する箇所は `printResponse` 等ユーティリティ経由か確認する
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
このドキュメントは現状のスタイルを尊重して最小限の規則を与えることを目的としています。次のステップを希望する場合:
|
||||||
|
- CI に ruff/mypy を組み込む PR を作成
|
||||||
|
- pre-commit 用の設定ファイル(`.pre-commit-config.yaml`)を追加して自動整形を導入
|
||||||
|
- 型注釈・テストのためのタスク分割(優先順位をつけた TODO)
|
||||||
|
|
||||||
|
要望があれば、これをベースに `.pre-commit-config.yaml`、`pyproject.toml` の ruff 設定、あるいは CI ワークフローの雛形(GitHub Actions)を作成します。
|
||||||
|
|
||||||
|
## Copilot と共同作業するための具体例とテンプレート
|
||||||
|
以下は Copilot に推奨プロンプトを投げやすく、また PR 作成時に便利なテンプレート類です。コピー&ペーストして使用してください。
|
||||||
|
|
||||||
|
### 関数テンプレート(型注釈 + docstring)
|
||||||
|
```python
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
def example_handler(endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
|
"""Handle an example endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: incoming endpoint string (e.g. '/get/data/version')
|
||||||
|
data: request payload (None for many GETs)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict suitable for printResponse(status, endpoint, result)
|
||||||
|
"""
|
||||||
|
# implementation...
|
||||||
|
result = {"status": 200, "endpoint": endpoint, "result": data}
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller の run 発行パターン(推奨)
|
||||||
|
Controller 内で run を呼ぶときは `self.run(self.run_mapping["key"], payload)` の形を維持してください。Copilot に尋ねるときは「この run key に対応する payload の形は?」と聞くとペイロード例を生成しやすいです。
|
||||||
|
|
||||||
|
### Docstring 例(Google スタイル)
|
||||||
|
```python
|
||||||
|
def set_selected_tab_no(tab_no: int) -> Dict[str, Any]:
|
||||||
|
"""Set the current tab.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab_no: index of tab to select
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A response dict with status and new tab number
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### PR チェックリスト(拡張版)
|
||||||
|
- コーディング規則に従っているか
|
||||||
|
- 新しい public API の docstring があるか
|
||||||
|
- 型注釈を最小限追加しているか(特に関数シグネチャ)
|
||||||
|
- ruff check が通るか
|
||||||
|
- mypy(relaxed)で重大な型エラーが出ていないか
|
||||||
|
- docs (必要箇所) を更新したか(API 変更があれば)
|
||||||
|
|
||||||
|
### 推奨 `.pre-commit-config.yaml`(例)
|
||||||
|
```yaml
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
|
rev: v0.14.0
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: ["--fix"]
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v1.18.2
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
args: ["--ignore-missing-imports", "--allow-untyped-defs", "--allow-redefinition"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 推奨 ruff 設定(pyproject.toml への最小設定例)
|
||||||
|
```toml
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 88
|
||||||
|
extend-ignore = ["E203"]
|
||||||
|
select = ["E", "F", "W", "C90"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
更新が必要なら私が `.pre-commit-config.yaml`、`pyproject.toml`、および CI ワークフロー (GitHub Actions) の雛形を作成してコミットまでできます。どれを優先しますか?
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 仕様書
|
||||||
|
|
||||||
|
概要
|
||||||
|
|
||||||
|
- プロジェクト名: VRCT (VR Chat Translator)
|
||||||
|
- 目的: マイク入力とスピーカー出力をリアルタイムに文字起こし・翻訳し、VR オーバーレイや OSC/WebSocket 経由で外部に送出するバックエンドロジック。
|
||||||
|
- 言語: Python
|
||||||
|
|
||||||
|
対象ユーザー
|
||||||
|
|
||||||
|
- VR 環境でリアルタイム翻訳・文字起こしを利用したいエンドユーザー
|
||||||
|
- フロントエンド(GUI)や VR クライアント(OSC)と連携するアプリケーション開発者
|
||||||
|
|
||||||
|
主要機能(機能要件)
|
||||||
|
|
||||||
|
1. 音声の取り込み・文字起こし
|
||||||
|
- マイク(送信)およびスピーカー(受信)から音声を取得し、ローカル Whisper(faster-whisper)または外部サービスによりテキスト化する。
|
||||||
|
- 音声エネルギー(音量)監視を行い、閾値ベースで検出する。
|
||||||
|
|
||||||
|
2. 翻訳
|
||||||
|
- DeepL / DeepL API / 各クラウド翻訳 / ローカル CTranslate2 モデルの複数バックエンドをサポート。
|
||||||
|
- 複数出力言語への一括翻訳、翻訳エンジンのフォールバック(CTranslate2 など)。
|
||||||
|
- 翻訳モデルのダウンロードと管理機能。
|
||||||
|
|
||||||
|
3. 表示・通知
|
||||||
|
- OpenVR オーバーレイ(small/large)用の画像生成と更新。
|
||||||
|
- OSC による VR へのメッセージ送信(typing/通知等)。
|
||||||
|
- WebSocket サーバーを介した外部クライアントへの JSON ブロードキャスト。
|
||||||
|
|
||||||
|
4. 入出力インターフェース
|
||||||
|
- stdin ラインベースの JSON コマンド受信(mainloop が実装)。
|
||||||
|
- stdout に対して構造化された JSON レスポンスを出力(printResponse/printLog)。
|
||||||
|
|
||||||
|
5. 設定・永続化
|
||||||
|
- JSON ベースの設定ファイルを使用(`config.py` による読み書きとデバウンス保存)。
|
||||||
|
|
||||||
|
6. ロギングと監視
|
||||||
|
- プロセスログ(process.log)とエラーログ(error.log)をローテーションで管理。
|
||||||
|
- ウォッチドッグ機構で定期的に死活チェック・コールバック。
|
||||||
|
|
||||||
|
非機能要件
|
||||||
|
|
||||||
|
- プラットフォーム: 主に Windows(Audio 周りは WASAPI を利用)を想定。クロスプラットフォームでの import 安全性を考慮。
|
||||||
|
- 可用性: 外部依存(PyAudio, CUDA, ctranslate2 等)が無い環境でも安全にインポートでき、機能劣化しつつ動作する。
|
||||||
|
- パフォーマンス: ローカルモデル利用時は GPU を利用して計算性能を確保。compute type 選択ロジックを実装。
|
||||||
|
- セキュリティ: 外部への API キー(DeepL など)は設定で扱い、コード上では平文保持を避ける(設定ファイルに保存)。
|
||||||
|
|
||||||
|
運用フロー
|
||||||
|
|
||||||
|
- 起動: stdin でコマンドを受け付ける mainloop を実行。必要な初期化は遅延実行(lazy init)を採用。
|
||||||
|
- モデル重ダウンロード: CTranslate2/Whisper 重みは `weights/` 配下にダウンロードし、チェックサム等で整合性確認。
|
||||||
|
- 障害時: 例外は utils.errorLogging() でトレースを error.log に出力。重要機能はフォールバック実装。
|
||||||
|
|
||||||
|
インターフェース(抜粋)
|
||||||
|
|
||||||
|
- stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": <base64(JSON)|any>}
|
||||||
|
- stdout(JSON): 標準化されたレスポンスを printResponse/printLog が出力(status, endpoint, result など)。
|
||||||
|
|
||||||
|
依存関係(オプション含む)
|
||||||
|
|
||||||
|
- 必須(実装時想定): requests, packaging, flashtext, pillow, pyaudiowpatch, speech_recognition
|
||||||
|
- ローカル推奨: faster-whisper, ctranslate2, torch(GPU 利用時)
|
||||||
|
- Windows 固有(音声ループバック): pycaw, comtypes
|
||||||
|
|
||||||
|
参考: 実装上の安全設計として optional な import は try/except でガードしており、存在しない依存があっても import 時にクラッシュしない。
|
||||||
|
|
||||||
|
## 最近の更新 (2025-10-20 translate_api ブランチ)
|
||||||
|
|
||||||
|
本章は既存仕様への差分のみを記載します。コードベースの事実に基づく更新点です。
|
||||||
|
|
||||||
|
### 翻訳エンジン / モデル管理の拡張
|
||||||
|
|
||||||
|
- OpenAI / Plamo / Gemini の選択モデルプロパティを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` にリネーム (旧名称は保存対象から移行)。
|
||||||
|
- 新規ローカル LLM 接続: LMStudio (`LMSTUDIO_URL`, `SELECTABLE_LMSTUDIO_MODEL_LIST`, `SELECTED_LMSTUDIO_MODEL`) を追加。
|
||||||
|
- 新規ローカル LLM 接続: Ollama (`SELECTABLE_OLLAMA_MODEL_LIST`, `SELECTED_OLLAMA_MODEL`) を追加。
|
||||||
|
- OpenAI 認証キー設定時にモデル一覧 (`SELECTABLE_OPENAI_MODEL_LIST`) を自動取得し、未選択時に第一候補へフォールバックする処理を追加。メソッド名を `OpenAi` → `OpenAI` に統一。
|
||||||
|
|
||||||
|
### エンドポイント追加 / 変更
|
||||||
|
|
||||||
|
`mainloop.py` の `mapping` / `run_mapping` に以下を追加:
|
||||||
|
|
||||||
|
- `/get/data/selectable_lmstudio_model_list`, `/get/data/selected_lmstudio_model`, `/set/data/selected_lmstudio_model`, `/get/data/lmstudio_url`, `/set/data/lmstudio_url`
|
||||||
|
- `/get/data/ollama_connection`, `/get/data/selectable_ollama_model_list`, `/get/data/selected_ollama_model`, `/set/data/selected_ollama_model`
|
||||||
|
- OpenAI 系: `getOpenAIAuthKey`, `setOpenAIAuthKey`, `delOpenAIAuthKey`, `getOpenAIModelList`, `getOpenAIModel`, `setOpenAIModel` に名称統一。
|
||||||
|
|
||||||
|
### 翻訳言語定義の構造変更
|
||||||
|
|
||||||
|
- CTranslate2 の言語定義をトップレベル直接キー (例: `m2m100_418M-ct2-int8`) から `translation_lang['CTranslate2'][weight_type]` のネスト構造へ再編。利用側の互換ロジック (`model.findTranslationEngines`) は weight_type 経由で参照するよう修正。
|
||||||
|
- 新規 YAML 言語マッピングファイル `models/translation/languages/languages.yml` を追加。`config.init_config()` 内で `loadTranslationLanguages()` を呼び出し統合 (失敗時は空辞書フォールバック)。
|
||||||
|
|
||||||
|
### プロンプトファイルの整理
|
||||||
|
|
||||||
|
- `translation_gemini.yml`, `translation_lmstudio.yml` から `supported_languages` ブロックを削除し、`system_prompt` 内に簡潔化。
|
||||||
|
|
||||||
|
### リソース / PyInstaller
|
||||||
|
|
||||||
|
- PyInstaller spec (`backend.spec`, `backend_cuda.spec`) の `datas` に `./src-python/models/translation/prompt` および `./src-python/models/translation/languages` を追加。
|
||||||
|
- フォント配置を `fonts/` 直下から `src-python/models/overlay/fonts/` へ移動し、`overlay_image.py` にビルド時 (`_internal/fonts`) と開発時の動的探索ロジックを追加。
|
||||||
|
|
||||||
|
### 依存パッケージの追加
|
||||||
|
|
||||||
|
- `requirements.txt` / `requirements_cuda.txt` に `PyYAML==6.0.2` (YAML読込), `google-genai==1.45.0`, `grpcio==1.67.1` を追加。
|
||||||
|
|
||||||
|
### 認証処理の微調整
|
||||||
|
|
||||||
|
- Plamo / Gemini 認証メソッドで `root_path=config.PATH_LOCAL` を渡すよう変更し、ローカル参照を統一。
|
||||||
|
- OpenAI モデル設定メソッドの名称を `setTranslatorOpenAiModel` → `setTranslatorOpenAIModel` に変更。
|
||||||
|
|
||||||
|
### テスト拡張
|
||||||
|
|
||||||
|
- `backend_test.py` に全言語ペア翻訳網羅テスト `test_translate_all_language_pairs()` を追加。結果を `translation_test_results.json` として保存。
|
||||||
|
|
||||||
|
### 内部ユーティリティの修正
|
||||||
|
|
||||||
|
- `downloadCTranslate2Tokenizer()` が `tokenizer_path` を正しく作成し `transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)` を使用するよう修正。
|
||||||
|
|
||||||
|
### 影響範囲まとめ
|
||||||
|
|
||||||
|
| 区分 | 影響内容 |
|
||||||
|
|------|----------|
|
||||||
|
| 設定 | 新規プロパティ / 既存名称変更 (`SELECTED_*`, `LMSTUDIO_URL`) |
|
||||||
|
| エンドポイント | LMStudio / Ollama / OpenAI 名称統一追加 |
|
||||||
|
| 翻訳言語 | CTranslate2 ネスト構造 / YAML マッピング導入 |
|
||||||
|
| リソース | PyInstaller datas 追加 / フォントパス移行 |
|
||||||
|
| 依存 | PyYAML / google-genai / grpcio 追加 |
|
||||||
|
| 認証 | OpenAI/Plamo/Gemini 認証後モデルリスト更新 & root_path 引数追加 |
|
||||||
|
| テスト | 全言語ペア網羅テスト追加 |
|
||||||
|
| ユーティリティ | Tokenizer ダウンロード処理修正 |
|
||||||
|
|
||||||
|
### ライセンス影響
|
||||||
|
|
||||||
|
追加された依存は既存 LICENSE の記載範囲に変更を強制するものではなく、ライセンス本文の更新不要 (現行 OSS ライセンス許容範囲内)。
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 設計書
|
||||||
|
|
||||||
|
概要
|
||||||
|
- 本設計書はアプリケーションのアーキテクチャ、主要コンポーネント、並列化モデル、エラー処理ポリシー、設定の保存方針を記述する。
|
||||||
|
|
||||||
|
アーキテクチャ概要
|
||||||
|
- 層構造
|
||||||
|
- mainloop: stdin ベースのコマンド受け取り、ワーカー(複数スレッド)で実行。
|
||||||
|
- controller: GUI/フロントエンドからの操作とモデルの仲介。`Controller` がビジネスロジックを実行。
|
||||||
|
- model: 実際の機能(翻訳、文字起こし、オーバーレイ、OSC、WebSocket、デバイス管理)を提供するファサード的シングルトン。
|
||||||
|
- models/*: 翻訳、文字起こしなどのドメイン別実装(Translator, AudioTranscriber, Overlay, WebSocketServer ...)。
|
||||||
|
- device_manager: 音声デバイス検出・監視(Windows の場合は WASAPI/pycaw を利用)。
|
||||||
|
- utils: 共通ユーティリティ(ロギング、ネットワークチェック、compute device 列挙など)。
|
||||||
|
|
||||||
|
初期化ポリシー
|
||||||
|
- 重い初期化(GPU モデルロード、OpenVR 初期化など)は import 時に行わず、`model.init()` か要求時の `ensure_initialized()` にて遅延実行。
|
||||||
|
- `DeviceManager` は import 時に軽量な init を行い、監視スレッドは `startMonitoring()` で開始する。
|
||||||
|
|
||||||
|
並列化・同期モデル
|
||||||
|
- mainloop.Main は 1 つの受信スレッド(stdin 읽取り)と N 個のハンドラワーカースレッドを持つ。
|
||||||
|
- 各リクエストはキューに入れられ、handler() により処理される。
|
||||||
|
- 有効/無効の切替(/set/enable/**, /set/disable/**)は同一リソースを競合しないよう正規化キーで Lock を割り当てる。
|
||||||
|
- モデル内部では threadFnc(Thread ラッパ)で周期的な送信処理や監視処理を実装。
|
||||||
|
- Audio 録音や文字起こしは専用の Queue を用い、Producer(Recorder)と Consumer(AudioTranscriber)を分離。
|
||||||
|
|
||||||
|
エラー処理
|
||||||
|
- すべての外周呼び出しは try/except で保護し、`utils.errorLogging()` によってトレースバックを error.log に出力する。
|
||||||
|
- JSON シリアライズに失敗した場合はフォールバック JSON を stdout に出力してプロセスを止めない。
|
||||||
|
- VRAM 関連のエラーは model.detectVRAMError() で判定し、該当する機能(翻訳等)を無効化してユーザーに通知する。
|
||||||
|
|
||||||
|
設定管理
|
||||||
|
- `config.py` が単一の Config シングルトンを持ち、変更はデバウンスして JSON ファイルへ保存。
|
||||||
|
- GUI からの操作は Controller が受け取り、Config を更新する。
|
||||||
|
|
||||||
|
ログ
|
||||||
|
- `utils.setupLogger` によりローテートファイルハンドラを使ったログを実装(process.log / error.log)。
|
||||||
|
- stdout には構造化ログを出力してフロントエンドと通信する。
|
||||||
|
|
||||||
|
インターフェース一覧(抜粋)
|
||||||
|
- STDIN/STDOUT プロトコル: mainloop の JSON 入出力(詳細は `mainloop.py` の mapping を参照)
|
||||||
|
- OSC: `models.osc.OSCHandler` が OSC 送受信と OSCQuery を管理
|
||||||
|
- WebSocket: `models.websocket.WebSocketServer` がクライアント管理とメッセージブロードキャストを担う
|
||||||
|
|
||||||
|
スレッド図(要点)
|
||||||
|
- main_thread: メイン(stdin 読み取り、キュー投入)
|
||||||
|
- handler_threads: キューから取り出し処理
|
||||||
|
- device_manager.th_monitoring: デバイス監視
|
||||||
|
- model.mic_print_transcript / speaker_print_transcript: 音声 -> 翻訳結果送出のループ
|
||||||
|
- websocket_server_thread: WebSocket サーバの asyncio ループを別スレッドで実行
|
||||||
|
|
||||||
|
拡張性・互換性設計
|
||||||
|
- 依存性は try/except でガードして optional 機能として扱う(例: faster-whisper が無くても import は成功する)。
|
||||||
|
- 翻訳エンジンは backend 名で抽象化され、Translator クラスにより統一インターフェースを提供。
|
||||||
|
|
||||||
|
運用上の考慮
|
||||||
|
- 大きなモデルファイル(Whisper, CTranslate2)をダウンロードする仕組みを持ち、進捗を GUI に報告する。
|
||||||
|
- GPU 計算タイプは utils.getBestComputeType で選択し、不適切な設定を検出した場合はフォールバック。
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# 詳細設計書
|
||||||
|
|
||||||
|
この文書は主要クラス・関数の詳細、データ構造、例外ケース、スレッドの振る舞いを記載する。
|
||||||
|
|
||||||
|
目次
|
||||||
|
- Model
|
||||||
|
- Controller
|
||||||
|
- Main (mainloop)
|
||||||
|
- DeviceManager
|
||||||
|
- Utils
|
||||||
|
- Clipboard(クリップボード機能)
|
||||||
|
- Telemetry(テレメトリ機能)
|
||||||
|
- モデルの重みダウンロードと整合性
|
||||||
|
|
||||||
|
## Model
|
||||||
|
- シングルトン: `model = Model()`
|
||||||
|
- 遅延初期化: `init()` と `ensure_initialized()` を備え、init は重いリソース(Overlay, Translator, Watchdog, OSC ハンドラ等)を構築する。
|
||||||
|
- 主な責務
|
||||||
|
- 翻訳/文字起こし関連の起動停止ラッパ
|
||||||
|
- Overlay/OSChandler/WebSocket の操作
|
||||||
|
- キーワード検出(flashtext)と重複検出
|
||||||
|
- VRAM エラー検出とフォールバック
|
||||||
|
- クリップボード操作(copy/paste)
|
||||||
|
- テレメトリ初期化・シャットダウン
|
||||||
|
- 重要属性(抜粋)
|
||||||
|
- `translator` : Translator インスタンス
|
||||||
|
- `overlay` / `overlay_image` : Overlay 系
|
||||||
|
- `mic_*`, `speaker_*` : 録音、トランスクリプタ、energy recorder
|
||||||
|
- `watchdog` : Watchdog
|
||||||
|
- `osc_handler`, `websocket_server`
|
||||||
|
- `clipboard` : Clipboard インスタンス(2026-01-11 追加)
|
||||||
|
- `telemetry` : テレメトリシステム(2026-01-11 追加)
|
||||||
|
- スレッド制御
|
||||||
|
- threadFnc を用いて周期処理を回す。stop/pause/resume が可能。
|
||||||
|
- Clipboard の VR 監視スレッドはデーモンスレッド(アプリ終了時に強制停止可能)
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
- GUI からの要求を受け、Model を操作して結果を run() コールバックへ返す。
|
||||||
|
- 各種設定変更 (/set/ や /get/ エンドポイント) を実装。
|
||||||
|
- 翻訳/文字起こし/オーバーレイ連携ロジックを持ち、メッセージ整形(messageFormatter)や OSC の送信を行う。
|
||||||
|
- ダウンロード作業は別スレッドで行い、進捗を run_mapping を通して通知。
|
||||||
|
- クリップボード/テレメトリ制御エンドポイント追加(2026-01-11)
|
||||||
|
- `getClipboard()` / `setEnableClipboard()` / `setDisableClipboard()`
|
||||||
|
- `getTelemetry()` / `setEnableTelemetry()` / `setDisableTelemetry()`
|
||||||
|
|
||||||
|
## Main (mainloop.Main)
|
||||||
|
- stdin を readline() で受け取り JSON を parse、endpoint と data をキューへ投入。
|
||||||
|
- worker_count 個の handler スレッドが queue を取り出し `_call_handler` を実行。
|
||||||
|
- endpoint ロック正規化: `/set/enable/...` と `/set/disable/...` は同じ正規化キー `/lock/set/...` を共有して排他制御。
|
||||||
|
- エラーレスポンスの標準化と再試行ロジック(status==423 は再キュー化)。
|
||||||
|
|
||||||
|
## DeviceManager
|
||||||
|
- シングルトン。初期化は軽量で、`init()` により内部構造をセット、実デバイスは `update()` で取得。
|
||||||
|
- Windows 環境では COM イベント (pycaw/MMNotificationClient) を用いた検出か、PyAudio によるポーリングでデバイス一覧を構成。
|
||||||
|
- コールバック設計: 変更検出時に Controller のコールバックを呼び出して UI 更新を促す。
|
||||||
|
|
||||||
|
## Utils
|
||||||
|
- `validateDictStructure(data, structure)` : JSON 構造検証。
|
||||||
|
- `getComputeDeviceList()` / `getBestComputeType()` : CPU/CUDA を列挙し、推奨 compute_type を返す。
|
||||||
|
- `setupLogger()` / `printLog()` / `printResponse()` / `errorLogging()` : ログ、標準出力の整形、エラー記録。
|
||||||
|
- ネットワーク/ソケット/IP アドレス検査ユーティリティ。
|
||||||
|
|
||||||
|
## Clipboard(クリップボード機能)
|
||||||
|
|
||||||
|
### 概要(2026-01-11 追加)
|
||||||
|
VRChat 内でのテキスト送信効率向上のため、翻訳結果をクリップボードにコピーし、自動ペースト機能を提供。
|
||||||
|
|
||||||
|
### 設計
|
||||||
|
- **シングルトン**: `models.clipboard.clipboard.Clipboard` クラス
|
||||||
|
- **初期化**: `Model.__init__()` で `self.clipboard = Clipboard()` インスタンス化
|
||||||
|
- **VR監視スレッド**: デーモンスレッドで 10 秒間隔 SteamVR 監視、VRChat アプリ名を自動検出
|
||||||
|
- **コピー・ペースト**:
|
||||||
|
- `setCopyToClipboard(text: str) -> bool`: 複数バックエンド対応(Windows clip、pyperclip、tkinter)
|
||||||
|
- `setPasteFromClipboard() -> bool`: PyAutoGUI による Ctrl+V 送信、VRChat ウィンドウ自動フォーカス
|
||||||
|
|
||||||
|
### 依存ライブラリ
|
||||||
|
- `pyautogui==0.9.54`: キー入力シミュレーション(requirements.txt 追加)
|
||||||
|
- `openvr`: VR アプリ名検出
|
||||||
|
- `psutil`: プロセス検索
|
||||||
|
- `pyperclip`(オプション): クリップボード操作フォールバック
|
||||||
|
|
||||||
|
### 設定・制御
|
||||||
|
- `config.ENABLE_CLIPBOARD`: True/False で機能制御
|
||||||
|
- Controller エンドポイント
|
||||||
|
- `/get/data/clipboard`: 状態取得(`config.ENABLE_CLIPBOARD` の値)
|
||||||
|
- `/set/enable/clipboard`: `config.ENABLE_CLIPBOARD` を True に設定
|
||||||
|
- `/set/disable/clipboard`: `config.ENABLE_CLIPBOARD` を False に設定
|
||||||
|
|
||||||
|
### 動作フロー
|
||||||
|
1. Model 初期化時に Clipboard インスタンス生成(VR 監視スレッド起動)
|
||||||
|
2. Controller が `config.ENABLE_CLIPBOARD == True` をチェック
|
||||||
|
3. 有効時のみ `model.setCopyToClipboard(result)` と `model.setPasteFromClipboard()` を呼び出し
|
||||||
|
4. Clipboard インスタンス自体は常に稼働(`clipboard.is_enabled` は常に True)
|
||||||
|
|
||||||
|
### エラーハンドリング
|
||||||
|
- コピー失敗: False を返却、リトライなし
|
||||||
|
- ペースト失敗: False を返却
|
||||||
|
- VR アプリ名検出失敗: app_name = None、現在フォーカスウィンドウにペースト
|
||||||
|
- 例外はログ出力し握りつぶし
|
||||||
|
|
||||||
|
## Telemetry(テレメトリ機能)
|
||||||
|
|
||||||
|
### 概要(2026-01-11 追加)
|
||||||
|
Aptabase を用いた匿名な使用状況データ収集。デフォルト有効、ユーザー制御可能。
|
||||||
|
|
||||||
|
### 設計
|
||||||
|
- **デフォルト有効**: `config.ENABLE_TELEMETRY = True`
|
||||||
|
- **初期化**: `model.telemetryInit(enabled=config.ENABLE_TELEMETRY, app_version=config.VERSION)`
|
||||||
|
- **シャットダウン**: `model.telemetryShutdown()` で app_closed イベント送信
|
||||||
|
- **プライバシー重視**: 個人情報・入力内容・音声データ一切送信なし
|
||||||
|
|
||||||
|
### イベント種別(送信固定)
|
||||||
|
- `app_started`: アプリ起動
|
||||||
|
- `app_closed`: アプリ終了(最後のイベント)
|
||||||
|
- `core_feature`: 機能開始(translation / mic_speech_to_text / speaker_speech_to_text / text_input)
|
||||||
|
|
||||||
|
### 動作フロー
|
||||||
|
1. アプリ起動時に telemetryInit() 呼び出し
|
||||||
|
2. 機能開始時に track_core_feature() で 1 セッション 1 回のみ送信
|
||||||
|
3. アプリ終了時に telemetryShutdown() で app_closed 送信
|
||||||
|
4. config.ENABLE_TELEMETRY = False で一切の通信・スレッド停止
|
||||||
|
|
||||||
|
### 設定・制御
|
||||||
|
- `config.ENABLE_TELEMETRY`: True/False で機能制御
|
||||||
|
- `model.telemetry.enabled`: 現在の有効状態
|
||||||
|
- Controller エンドポイント
|
||||||
|
- `/get/data/telemetry`: 状態取得
|
||||||
|
- `/set/enable/telemetry`: 有効化(telemetryInit 呼び出し)
|
||||||
|
- `/set/disable/telemetry`: 無効化(telemetryShutdown 呼び出し)
|
||||||
|
|
||||||
|
### エラーハンドリング
|
||||||
|
- API 通信失敗時: 例外握りつぶし、アプリ動作に影響なし
|
||||||
|
- オフライン時: 機能停止のみ、再送・バッファリングなし
|
||||||
|
- 無効化時: 一切の通信・スレッド・処理停止
|
||||||
|
- Aptabase SDK ログ: CRITICAL レベルのみに制限(ノイズ削減)
|
||||||
|
|
||||||
|
## モデル重みダウンロード
|
||||||
|
- `models.translation.translation_utils` と `models.transcription.transcription_whisper` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。
|
||||||
|
- GUI からの要求は Controller により非同期スレッドで実行され、進捗コールバックが run_mapping を介してフロントエンドに渡る。
|
||||||
|
|
||||||
|
## エッジケース / 例外処理
|
||||||
|
- 外部 API のレート制限や認証エラーは呼び出し元に 400 系のレスポンスを返し、必要であればフォールバック実装(CTranslate2 への切替)を行う。
|
||||||
|
- 大きなモデル実行時の VRAM エラーは検出し、当該機能を無効化してユーザへ通知する。
|
||||||
|
- 音声デバイスが存在しない場合は NoDevice を返し、UI 側で扱う。
|
||||||
|
|
||||||
|
## テスト観点
|
||||||
|
- メッセージ受信/送信のエンドツーエンド: stdin -> handler -> Controller -> Model -> printResponse の流れ。
|
||||||
|
- デバイス挙動: DeviceManager.update() がデバイス一覧を取得できるか(PyAudio 経由)。
|
||||||
|
- モデルダウンロード: ダウンロード成功・失敗、チェックサム検証。
|
||||||
|
- ログ/エラー: errorLogging() による例外トレースが error.log に記録されるか。
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Any, Tuple
|
||||||
|
from threading import Thread, Event, Lock
|
||||||
|
from queue import Queue, Empty
|
||||||
|
import logging
|
||||||
|
from controller import Controller # noqa: E402
|
||||||
|
from utils import printLog, printResponse, errorLogging, encodeBase64 # noqa: E402
|
||||||
|
|
||||||
|
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
run_mapping = {
|
||||||
|
"enable_translation":"/run/enable_translation",
|
||||||
|
"enable_transcription_send":"/run/enable_transcription_send",
|
||||||
|
"enable_transcription_receive":"/run/enable_transcription_receive",
|
||||||
|
|
||||||
|
"connected_network":"/run/connected_network",
|
||||||
|
"enable_ai_models":"/run/enable_ai_models",
|
||||||
|
|
||||||
|
"transcription_mic":"/run/transcription_send_mic_message",
|
||||||
|
"transcription_speaker":"/run/transcription_receive_speaker_message",
|
||||||
|
|
||||||
|
"check_mic_volume":"/run/check_mic_volume",
|
||||||
|
"check_speaker_volume":"/run/check_speaker_volume",
|
||||||
|
|
||||||
|
"error_device":"/run/error_device",
|
||||||
|
"error_translation_engine":"/run/error_translation_engine",
|
||||||
|
|
||||||
|
"error_translation_chat_vram_overflow":"/run/error_translation_chat_vram_overflow",
|
||||||
|
"error_translation_mic_vram_overflow":"/run/error_translation_mic_vram_overflow",
|
||||||
|
"error_translation_speaker_vram_overflow":"/run/error_translation_speaker_vram_overflow",
|
||||||
|
"error_transcription_mic_vram_overflow":"/run/error_transcription_mic_vram_overflow",
|
||||||
|
"error_transcription_speaker_vram_overflow":"/run/error_transcription_speaker_vram_overflow",
|
||||||
|
|
||||||
|
"word_filter":"/run/word_filter",
|
||||||
|
|
||||||
|
"download_progress_ctranslate2_weight":"/run/download_progress_ctranslate2_weight",
|
||||||
|
"downloaded_ctranslate2_weight":"/run/downloaded_ctranslate2_weight",
|
||||||
|
"error_ctranslate2_weight":"/run/error_ctranslate2_weight",
|
||||||
|
"download_progress_whisper_weight":"/run/download_progress_whisper_weight",
|
||||||
|
"downloaded_whisper_weight":"/run/downloaded_whisper_weight",
|
||||||
|
"error_whisper_weight":"/run/error_whisper_weight",
|
||||||
|
|
||||||
|
"selected_mic_host":"/run/selected_mic_host",
|
||||||
|
"selected_mic_device":"/run/selected_mic_device",
|
||||||
|
"selected_speaker_device":"/run/selected_speaker_device",
|
||||||
|
|
||||||
|
"selected_translation_engines":"/run/selected_translation_engines",
|
||||||
|
"translation_engines":"/run/translation_engines",
|
||||||
|
|
||||||
|
"selected_translation_compute_type":"/run/selected_translation_compute_type",
|
||||||
|
"selected_transcription_compute_type":"/run/selected_transcription_compute_type",
|
||||||
|
|
||||||
|
"selectable_plamo_model_list":"/run/selectable_plamo_model_list",
|
||||||
|
"selected_plamo_model":"/run/selected_plamo_model",
|
||||||
|
"selectable_gemini_model_list":"/run/selectable_gemini_model_list",
|
||||||
|
"selected_gemini_model":"/run/selected_gemini_model",
|
||||||
|
"selectable_openai_model_list":"/run/selectable_openai_model_list",
|
||||||
|
"selected_openai_model":"/run/selected_openai_model",
|
||||||
|
"selectable_groq_model_list":"/run/selectable_groq_model_list",
|
||||||
|
"selected_groq_model":"/run/selected_groq_model",
|
||||||
|
"selectable_openrouter_model_list":"/run/selectable_openrouter_model_list",
|
||||||
|
"selected_openrouter_model":"/run/selected_openrouter_model",
|
||||||
|
"selectable_lmstudio_model_list":"/run/selectable_lmstudio_model_list",
|
||||||
|
"selected_lmstudio_model":"/run/selected_lmstudio_model",
|
||||||
|
"selectable_ollama_model_list":"/run/selectable_ollama_model_list",
|
||||||
|
"selected_ollama_model":"/run/selected_ollama_model",
|
||||||
|
|
||||||
|
"selectable_mic_host_list":"/run/selectable_mic_host_list",
|
||||||
|
"selectable_mic_device_list":"/run/selectable_mic_device_list",
|
||||||
|
"selectable_speaker_device_list":"/run/selectable_speaker_device_list",
|
||||||
|
|
||||||
|
"software_update_info":"/run/software_update_info",
|
||||||
|
|
||||||
|
"initialization_progress":"/run/initialization_progress",
|
||||||
|
"initialization_complete":"/run/initialization_complete",
|
||||||
|
|
||||||
|
"enable_osc_query":"/run/enable_osc_query",
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(status:int, endpoint:str, result:Any) -> None:
|
||||||
|
printResponse(status, endpoint, result)
|
||||||
|
|
||||||
|
controller = Controller()
|
||||||
|
controller.setRunMapping(run_mapping)
|
||||||
|
controller.setRun(run)
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
# Main Window
|
||||||
|
"/set/enable/translation": {"status": False, "variable":controller.setEnableTranslation},
|
||||||
|
"/set/disable/translation": {"status": False, "variable":controller.setDisableTranslation},
|
||||||
|
|
||||||
|
"/set/enable/transcription_send": {"status": False, "variable":controller.setEnableTranscriptionSend},
|
||||||
|
"/set/disable/transcription_send": {"status": False, "variable":controller.setDisableTranscriptionSend},
|
||||||
|
|
||||||
|
"/set/enable/transcription_receive": {"status": False, "variable":controller.setEnableTranscriptionReceive},
|
||||||
|
"/set/disable/transcription_receive": {"status": False, "variable":controller.setDisableTranscriptionReceive},
|
||||||
|
|
||||||
|
"/set/enable/foreground": {"status": True, "variable":controller.setEnableForeground},
|
||||||
|
"/set/disable/foreground": {"status": True, "variable":controller.setDisableForeground},
|
||||||
|
|
||||||
|
"/get/data/selected_tab_no": {"status": True, "variable":controller.getSelectedTabNo},
|
||||||
|
"/set/data/selected_tab_no": {"status": True, "variable":controller.setSelectedTabNo},
|
||||||
|
|
||||||
|
"/get/data/main_window_sidebar_compact_mode": {"status": True, "variable":controller.getMainWindowSidebarCompactMode},
|
||||||
|
"/set/enable/main_window_sidebar_compact_mode": {"status": True, "variable":controller.setEnableMainWindowSidebarCompactMode},
|
||||||
|
"/set/disable/main_window_sidebar_compact_mode": {"status": True, "variable":controller.setDisableMainWindowSidebarCompactMode},
|
||||||
|
|
||||||
|
"/get/data/selectable_translation_engines": {"status": True, "variable":controller.getTranslationEngines},
|
||||||
|
"/get/data/selectable_language_list": {"status": True, "variable":controller.getListLanguageAndCountry},
|
||||||
|
|
||||||
|
"/get/data/selected_translation_engines": {"status": False, "variable":controller.getSelectedTranslationEngines},
|
||||||
|
"/set/data/selected_translation_engines": {"status": True, "variable":controller.setSelectedTranslationEngines},
|
||||||
|
|
||||||
|
"/get/data/selected_your_languages": {"status": True, "variable":controller.getSelectedYourLanguages},
|
||||||
|
"/set/data/selected_your_languages": {"status": True, "variable":controller.setSelectedYourLanguages},
|
||||||
|
|
||||||
|
"/get/data/selected_target_languages": {"status": True, "variable":controller.getSelectedTargetLanguages},
|
||||||
|
"/set/data/selected_target_languages": {"status": True, "variable":controller.setSelectedTargetLanguages},
|
||||||
|
|
||||||
|
"/get/data/selectable_transcription_engines": {"status": False, "variable":controller.getTranscriptionEngines},
|
||||||
|
"/get/data/selected_transcription_engine": {"status": False, "variable":controller.getSelectedTranscriptionEngine},
|
||||||
|
"/set/data/selected_transcription_engine": {"status": False, "variable":controller.setSelectedTranscriptionEngine},
|
||||||
|
|
||||||
|
"/run/send_message_box": {"status": False, "variable":controller.sendMessageBox},
|
||||||
|
"/run/typing_message_box": {"status": False, "variable":controller.typingMessageBox},
|
||||||
|
"/run/stop_typing_message_box": {"status": False, "variable":controller.stopTypingMessageBox},
|
||||||
|
|
||||||
|
"/run/send_text_overlay": {"status": True, "variable":controller.sendTextOverlay},
|
||||||
|
|
||||||
|
"/get/data/telemetry" : {"status": True, "variable":controller.getTelemetry},
|
||||||
|
"/set/enable/telemetry" : {"status": True, "variable":controller.setEnableTelemetry},
|
||||||
|
"/set/disable/telemetry" : {"status": True, "variable":controller.setDisableTelemetry},
|
||||||
|
"/run/shutdown": {"status": True, "variable":controller.shutdown},
|
||||||
|
|
||||||
|
"/run/swap_your_language_and_target_language": {"status": True, "variable":controller.swapYourLanguageAndTargetLanguage},
|
||||||
|
|
||||||
|
"/run/update_software": {"status": True, "variable":controller.updateSoftware},
|
||||||
|
"/run/update_cuda_software": {"status": True, "variable":controller.updateCudaSoftware},
|
||||||
|
|
||||||
|
# Config Window
|
||||||
|
# Appearance
|
||||||
|
"/get/data/version": {"status": True, "variable":controller.getVersion},
|
||||||
|
|
||||||
|
"/get/data/transparency": {"status": True, "variable":controller.getTransparency},
|
||||||
|
"/set/data/transparency": {"status": True, "variable":controller.setTransparency},
|
||||||
|
|
||||||
|
"/get/data/ui_scaling": {"status": True, "variable":controller.getUiScaling},
|
||||||
|
"/set/data/ui_scaling": {"status": True, "variable":controller.setUiScaling},
|
||||||
|
|
||||||
|
"/get/data/textbox_ui_scaling": {"status": True, "variable":controller.getTextboxUiScaling},
|
||||||
|
"/set/data/textbox_ui_scaling": {"status": True, "variable":controller.setTextboxUiScaling},
|
||||||
|
|
||||||
|
"/get/data/message_box_ratio": {"status": True, "variable":controller.getMessageBoxRatio},
|
||||||
|
"/set/data/message_box_ratio": {"status": True, "variable":controller.setMessageBoxRatio},
|
||||||
|
|
||||||
|
"/get/data/send_message_button_type": {"status": True, "variable":controller.getSendMessageButtonType},
|
||||||
|
"/set/data/send_message_button_type": {"status": True, "variable":controller.setSendMessageButtonType},
|
||||||
|
|
||||||
|
"/get/data/show_resend_button": {"status": True, "variable":controller.getShowResendButton},
|
||||||
|
"/set/enable/show_resend_button": {"status": True, "variable":controller.setEnableShowResendButton},
|
||||||
|
"/set/disable/show_resend_button": {"status": True, "variable":controller.setDisableShowResendButton},
|
||||||
|
|
||||||
|
"/get/data/font_family": {"status": True, "variable":controller.getFontFamily},
|
||||||
|
"/set/data/font_family": {"status": True, "variable":controller.setFontFamily},
|
||||||
|
|
||||||
|
"/get/data/ui_language": {"status": True, "variable":controller.getUiLanguage},
|
||||||
|
"/set/data/ui_language": {"status": True, "variable":controller.setUiLanguage},
|
||||||
|
|
||||||
|
"/get/data/main_window_geometry": {"status": True, "variable":controller.getMainWindowGeometry},
|
||||||
|
"/set/data/main_window_geometry": {"status": True, "variable":controller.setMainWindowGeometry},
|
||||||
|
|
||||||
|
# Compute device
|
||||||
|
"/get/data/compute_mode": {"status": True, "variable":controller.getComputeMode},
|
||||||
|
"/get/data/selectable_translation_compute_device_list": {"status": True, "variable":controller.getComputeDeviceList},
|
||||||
|
"/get/data/selected_translation_compute_device": {"status": True, "variable":controller.getSelectedTranslationComputeDevice},
|
||||||
|
"/set/data/selected_translation_compute_device": {"status": True, "variable":controller.setSelectedTranslationComputeDevice},
|
||||||
|
"/get/data/selectable_transcription_compute_device_list": {"status": True, "variable":controller.getComputeDeviceList},
|
||||||
|
"/get/data/selected_transcription_compute_device": {"status": True, "variable":controller.getSelectedTranscriptionComputeDevice},
|
||||||
|
"/set/data/selected_transcription_compute_device": {"status": True, "variable":controller.setSelectedTranscriptionComputeDevice},
|
||||||
|
|
||||||
|
# Translation
|
||||||
|
"/get/data/selectable_ctranslate2_weight_type_dict": {"status": True, "variable":controller.getSelectableCtranslate2WeightTypeDict},
|
||||||
|
|
||||||
|
"/get/data/selected_ctranslate2_weight_type": {"status": True, "variable":controller.getCtranslate2WeightType},
|
||||||
|
"/set/data/selected_ctranslate2_weight_type": {"status": True, "variable":controller.setCtranslate2WeightType},
|
||||||
|
|
||||||
|
"/get/data/selected_translation_compute_type": {"status": True, "variable":controller.getSelectedTranslationComputeType},
|
||||||
|
"/set/data/selected_translation_compute_type": {"status": True, "variable":controller.setSelectedTranslationComputeType},
|
||||||
|
|
||||||
|
"/run/download_ctranslate2_weight": {"status": True, "variable":controller.downloadCtranslate2Weight},
|
||||||
|
|
||||||
|
"/get/data/deepl_auth_key": {"status": False, "variable":controller.getDeepLAuthKey},
|
||||||
|
"/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey},
|
||||||
|
"/delete/data/deepl_auth_key": {"status": False, "variable":controller.delDeeplAuthKey},
|
||||||
|
|
||||||
|
"/get/data/selectable_plamo_model_list": {"status": False, "variable":controller.getPlamoModelList},
|
||||||
|
"/get/data/selected_plamo_model": {"status": False, "variable":controller.getPlamoModel},
|
||||||
|
"/set/data/selected_plamo_model": {"status": False, "variable":controller.setPlamoModel},
|
||||||
|
"/get/data/plamo_auth_key": {"status": False, "variable":controller.getPlamoAuthKey},
|
||||||
|
"/set/data/plamo_auth_key": {"status": False, "variable":controller.setPlamoAuthKey},
|
||||||
|
"/delete/data/plamo_auth_key": {"status": False, "variable":controller.delPlamoAuthKey},
|
||||||
|
|
||||||
|
"/get/data/selectable_gemini_model_list": {"status": True, "variable":controller.getGeminiModelList},
|
||||||
|
"/get/data/selected_gemini_model": {"status": True, "variable":controller.getGeminiModel},
|
||||||
|
"/set/data/selected_gemini_model": {"status": True, "variable":controller.setGeminiModel},
|
||||||
|
"/get/data/gemini_auth_key": {"status": True, "variable":controller.getGeminiAuthKey},
|
||||||
|
"/set/data/gemini_auth_key": {"status": True, "variable":controller.setGeminiAuthKey},
|
||||||
|
"/delete/data/gemini_auth_key": {"status": True, "variable":controller.delGeminiAuthKey},
|
||||||
|
|
||||||
|
"/get/data/selectable_openai_model_list": {"status": True, "variable":controller.getOpenAIModelList},
|
||||||
|
"/get/data/selected_openai_model": {"status": True, "variable":controller.getOpenAIModel},
|
||||||
|
"/set/data/selected_openai_model": {"status": True, "variable":controller.setOpenAIModel},
|
||||||
|
"/get/data/openai_auth_key": {"status": True, "variable":controller.getOpenAIAuthKey},
|
||||||
|
"/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey},
|
||||||
|
"/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey},
|
||||||
|
|
||||||
|
"/get/data/selectable_groq_model_list": {"status": True, "variable":controller.getGroqModelList},
|
||||||
|
"/get/data/selected_groq_model": {"status": True, "variable":controller.getGroqModel},
|
||||||
|
"/set/data/selected_groq_model": {"status": True, "variable":controller.setGroqModel},
|
||||||
|
"/get/data/groq_auth_key": {"status": True, "variable":controller.getGroqAuthKey},
|
||||||
|
"/set/data/groq_auth_key": {"status": True, "variable":controller.setGroqAuthKey},
|
||||||
|
"/delete/data/groq_auth_key": {"status": True, "variable":controller.delGroqAuthKey},
|
||||||
|
|
||||||
|
"/get/data/selectable_openrouter_model_list": {"status": True, "variable":controller.getOpenRouterModelList},
|
||||||
|
"/get/data/selected_openrouter_model": {"status": True, "variable":controller.getOpenRouterModel},
|
||||||
|
"/set/data/selected_openrouter_model": {"status": True, "variable":controller.setOpenRouterModel},
|
||||||
|
"/get/data/openrouter_auth_key": {"status": True, "variable":controller.getOpenRouterAuthKey},
|
||||||
|
"/set/data/openrouter_auth_key": {"status": True, "variable":controller.setOpenRouterAuthKey},
|
||||||
|
"/delete/data/openrouter_auth_key": {"status": True, "variable":controller.delOpenRouterAuthKey},
|
||||||
|
|
||||||
|
"/get/data/connected_lmstudio": {"status": True, "variable":controller.getTranslatorLMStudioConnection},
|
||||||
|
"/run/lmstudio_connection": {"status": True, "variable":controller.checkTranslatorLMStudioConnection},
|
||||||
|
"/get/data/selectable_lmstudio_model_list": {"status": True, "variable":controller.getTranslatorLStudioModelList},
|
||||||
|
"/get/data/selected_lmstudio_model": {"status": True, "variable":controller.getTranslatorLMStudioModel},
|
||||||
|
"/set/data/selected_lmstudio_model": {"status": True, "variable":controller.setTranslatorLMStudioModel},
|
||||||
|
"/get/data/lmstudio_url": {"status": True, "variable":controller.getTranslatorLMStudioURL},
|
||||||
|
"/set/data/lmstudio_url": {"status": True, "variable":controller.setTranslatorLMStudioURL},
|
||||||
|
|
||||||
|
"/get/data/connected_ollama": {"status": True, "variable":controller.getTranslatorOllamaConnection},
|
||||||
|
"/run/ollama_connection": {"status": True, "variable":controller.checkTranslatorOllamaConnection},
|
||||||
|
"/get/data/selectable_ollama_model_list": {"status": True, "variable":controller.getTranslatorOllamaModelList},
|
||||||
|
"/get/data/selected_ollama_model": {"status": True, "variable":controller.getTranslatorOllamaModel},
|
||||||
|
"/set/data/selected_ollama_model": {"status": True, "variable":controller.setTranslatorOllamaModel},
|
||||||
|
|
||||||
|
# Transliteration
|
||||||
|
"/get/data/convert_message_to_romaji": {"status": True, "variable":controller.getConvertMessageToRomaji},
|
||||||
|
"/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji},
|
||||||
|
"/set/disable/convert_message_to_romaji": {"status": True, "variable":controller.setDisableConvertMessageToRomaji},
|
||||||
|
|
||||||
|
"/get/data/convert_message_to_hiragana": {"status": True, "variable":controller.getConvertMessageToHiragana},
|
||||||
|
"/set/enable/convert_message_to_hiragana": {"status": True, "variable":controller.setEnableConvertMessageToHiragana},
|
||||||
|
"/set/disable/convert_message_to_hiragana": {"status": True, "variable":controller.setDisableConvertMessageToHiragana},
|
||||||
|
|
||||||
|
# Transcription
|
||||||
|
"/get/data/selectable_mic_host_list": {"status": True, "variable":controller.getMicHostList},
|
||||||
|
"/get/data/selectable_mic_device_list": {"status": True, "variable":controller.getMicDeviceList},
|
||||||
|
"/get/data/selectable_speaker_device_list": {"status": True, "variable":controller.getSpeakerDeviceList},
|
||||||
|
|
||||||
|
# "/get/data/max_mic_threshold": {"status": True, "variable":controller.getMaxMicThreshold},
|
||||||
|
# "/get/data/max_speaker_threshold": {"status": True, "variable":controller.getMaxSpeakerThreshold},
|
||||||
|
|
||||||
|
"/get/data/auto_mic_select": {"status": True, "variable":controller.getAutoMicSelect},
|
||||||
|
"/set/enable/auto_mic_select": {"status": True, "variable":controller.setEnableAutoMicSelect},
|
||||||
|
"/set/disable/auto_mic_select": {"status": True, "variable":controller.setDisableAutoMicSelect},
|
||||||
|
|
||||||
|
"/get/data/selected_mic_host": {"status": True, "variable":controller.getSelectedMicHost},
|
||||||
|
"/set/data/selected_mic_host": {"status": True, "variable":controller.setSelectedMicHost},
|
||||||
|
|
||||||
|
"/get/data/selected_mic_device": {"status": True, "variable":controller.getSelectedMicDevice},
|
||||||
|
"/set/data/selected_mic_device": {"status": True, "variable":controller.setSelectedMicDevice},
|
||||||
|
|
||||||
|
"/get/data/mic_threshold": {"status": True, "variable":controller.getMicThreshold},
|
||||||
|
"/set/data/mic_threshold": {"status": True, "variable":controller.setMicThreshold},
|
||||||
|
|
||||||
|
"/get/data/mic_automatic_threshold": {"status": True, "variable":controller.getMicAutomaticThreshold},
|
||||||
|
"/set/enable/mic_automatic_threshold": {"status": True, "variable":controller.setEnableMicAutomaticThreshold},
|
||||||
|
"/set/disable/mic_automatic_threshold": {"status": True, "variable":controller.setDisableMicAutomaticThreshold},
|
||||||
|
|
||||||
|
"/get/data/mic_record_timeout": {"status": True, "variable":controller.getMicRecordTimeout},
|
||||||
|
"/set/data/mic_record_timeout": {"status": True, "variable":controller.setMicRecordTimeout},
|
||||||
|
|
||||||
|
"/get/data/mic_phrase_timeout": {"status": True, "variable":controller.getMicPhraseTimeout},
|
||||||
|
"/set/data/mic_phrase_timeout": {"status": True, "variable":controller.setMicPhraseTimeout},
|
||||||
|
|
||||||
|
"/get/data/mic_max_phrases": {"status": True, "variable":controller.getMicMaxPhrases},
|
||||||
|
"/set/data/mic_max_phrases": {"status": True, "variable":controller.setMicMaxPhrases},
|
||||||
|
|
||||||
|
"/get/data/hotkeys": {"status": True, "variable":controller.getHotkeys},
|
||||||
|
"/set/data/hotkeys": {"status": True, "variable":controller.setHotkeys},
|
||||||
|
|
||||||
|
"/get/data/plugins_status": {"status": True, "variable":controller.getPluginsStatus},
|
||||||
|
"/set/data/plugins_status": {"status": True, "variable":controller.setPluginsStatus},
|
||||||
|
|
||||||
|
"/get/data/mic_avg_logprob": {"status": True, "variable":controller.getMicAvgLogprob},
|
||||||
|
"/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob},
|
||||||
|
|
||||||
|
"/get/data/mic_no_speech_prob": {"status": True, "variable":controller.getMicNoSpeechProb},
|
||||||
|
"/set/data/mic_no_speech_prob": {"status": True, "variable":controller.setMicNoSpeechProb},
|
||||||
|
|
||||||
|
"/set/enable/check_mic_threshold": {"status": True, "variable":controller.setEnableCheckMicThreshold},
|
||||||
|
"/set/disable/check_mic_threshold": {"status": True, "variable":controller.setDisableCheckMicThreshold},
|
||||||
|
|
||||||
|
"/get/data/mic_word_filter": {"status": True, "variable":controller.getMicWordFilter},
|
||||||
|
"/set/data/mic_word_filter": {"status": True, "variable":controller.setMicWordFilter},
|
||||||
|
|
||||||
|
"/get/data/auto_speaker_select": {"status": True, "variable":controller.getAutoSpeakerSelect},
|
||||||
|
"/set/enable/auto_speaker_select": {"status": True, "variable":controller.setEnableAutoSpeakerSelect},
|
||||||
|
"/set/disable/auto_speaker_select": {"status": True, "variable":controller.setDisableAutoSpeakerSelect},
|
||||||
|
|
||||||
|
"/get/data/selected_speaker_device": {"status": True, "variable":controller.getSelectedSpeakerDevice},
|
||||||
|
"/set/data/selected_speaker_device": {"status": True, "variable":controller.setSelectedSpeakerDevice},
|
||||||
|
|
||||||
|
"/get/data/speaker_threshold": {"status": True, "variable":controller.getSpeakerThreshold},
|
||||||
|
"/set/data/speaker_threshold": {"status": True, "variable":controller.setSpeakerThreshold},
|
||||||
|
|
||||||
|
"/get/data/speaker_automatic_threshold": {"status": True, "variable":controller.getSpeakerAutomaticThreshold},
|
||||||
|
"/set/enable/speaker_automatic_threshold": {"status": True, "variable":controller.setEnableSpeakerAutomaticThreshold},
|
||||||
|
"/set/disable/speaker_automatic_threshold": {"status": True, "variable":controller.setDisableSpeakerAutomaticThreshold},
|
||||||
|
|
||||||
|
"/get/data/speaker_record_timeout": {"status": True, "variable":controller.getSpeakerRecordTimeout},
|
||||||
|
"/set/data/speaker_record_timeout": {"status": True, "variable":controller.setSpeakerRecordTimeout},
|
||||||
|
|
||||||
|
"/get/data/speaker_phrase_timeout": {"status": True, "variable":controller.getSpeakerPhraseTimeout},
|
||||||
|
"/set/data/speaker_phrase_timeout": {"status": True, "variable":controller.setSpeakerPhraseTimeout},
|
||||||
|
|
||||||
|
"/get/data/speaker_max_phrases": {"status": True, "variable":controller.getSpeakerMaxPhrases},
|
||||||
|
"/set/data/speaker_max_phrases": {"status": True, "variable":controller.setSpeakerMaxPhrases},
|
||||||
|
|
||||||
|
"/get/data/speaker_avg_logprob": {"status": True, "variable":controller.getSpeakerAvgLogprob},
|
||||||
|
"/set/data/speaker_avg_logprob": {"status": True, "variable":controller.setSpeakerAvgLogprob},
|
||||||
|
|
||||||
|
"/get/data/speaker_no_speech_prob": {"status": True, "variable":controller.getSpeakerNoSpeechProb},
|
||||||
|
"/set/data/speaker_no_speech_prob": {"status": True, "variable":controller.setSpeakerNoSpeechProb},
|
||||||
|
|
||||||
|
"/set/enable/check_speaker_threshold": {"status": True, "variable":controller.setEnableCheckSpeakerThreshold},
|
||||||
|
"/set/disable/check_speaker_threshold": {"status": True, "variable":controller.setDisableCheckSpeakerThreshold},
|
||||||
|
|
||||||
|
"/get/data/selectable_whisper_weight_type_dict": {"status": True, "variable":controller.getSelectableWhisperWeightTypeDict},
|
||||||
|
|
||||||
|
"/get/data/selected_whisper_weight_type": {"status": True, "variable":controller.getWhisperWeightType},
|
||||||
|
"/set/data/selected_whisper_weight_type": {"status": True, "variable":controller.setWhisperWeightType},
|
||||||
|
|
||||||
|
"/get/data/selected_transcription_compute_type": {"status": True, "variable":controller.getSelectedTranscriptionComputeType},
|
||||||
|
"/set/data/selected_transcription_compute_type": {"status": True, "variable":controller.setSelectedTranscriptionComputeType},
|
||||||
|
|
||||||
|
"/run/download_whisper_weight": {"status": True, "variable":controller.downloadWhisperWeight},
|
||||||
|
|
||||||
|
# VR
|
||||||
|
"/get/data/overlay_small_log": {"status": True, "variable":controller.getOverlaySmallLog},
|
||||||
|
"/set/enable/overlay_small_log": {"status": True, "variable":controller.setEnableOverlaySmallLog},
|
||||||
|
"/set/disable/overlay_small_log": {"status": True, "variable":controller.setDisableOverlaySmallLog},
|
||||||
|
|
||||||
|
"/get/data/overlay_small_log_settings": {"status": True, "variable":controller.getOverlaySmallLogSettings},
|
||||||
|
"/set/data/overlay_small_log_settings": {"status": True, "variable":controller.setOverlaySmallLogSettings},
|
||||||
|
|
||||||
|
"/get/data/overlay_large_log": {"status": True, "variable":controller.getOverlayLargeLog},
|
||||||
|
"/set/enable/overlay_large_log": {"status": True, "variable":controller.setEnableOverlayLargeLog},
|
||||||
|
"/set/disable/overlay_large_log": {"status": True, "variable":controller.setDisableOverlayLargeLog},
|
||||||
|
|
||||||
|
"/get/data/overlay_large_log_settings": {"status": True, "variable":controller.getOverlayLargeLogSettings},
|
||||||
|
"/set/data/overlay_large_log_settings": {"status": True, "variable":controller.setOverlayLargeLogSettings},
|
||||||
|
|
||||||
|
"/get/data/overlay_show_only_translated_messages": {"status": True, "variable":controller.getOverlayShowOnlyTranslatedMessages},
|
||||||
|
"/set/enable/overlay_show_only_translated_messages": {"status": True, "variable":controller.setEnableOverlayShowOnlyTranslatedMessages},
|
||||||
|
"/set/disable/overlay_show_only_translated_messages": {"status": True, "variable":controller.setDisableOverlayShowOnlyTranslatedMessages},
|
||||||
|
|
||||||
|
# Others
|
||||||
|
"/get/data/send_message_format_parts": {"status": True, "variable":controller.getSendMessageFormatParts},
|
||||||
|
"/set/data/send_message_format_parts": {"status": True, "variable":controller.setSendMessageFormatParts},
|
||||||
|
"/get/data/received_message_format_parts": {"status": True, "variable":controller.getReceivedMessageFormatParts},
|
||||||
|
"/set/data/received_message_format_parts": {"status": True, "variable":controller.setReceivedMessageFormatParts},
|
||||||
|
|
||||||
|
"/get/data/auto_clear_message_box": {"status": True, "variable":controller.getAutoClearMessageBox},
|
||||||
|
"/set/enable/auto_clear_message_box": {"status": True, "variable":controller.setEnableAutoClearMessageBox},
|
||||||
|
"/set/disable/auto_clear_message_box": {"status": True, "variable":controller.setDisableAutoClearMessageBox},
|
||||||
|
|
||||||
|
"/get/data/send_only_translated_messages": {"status": True, "variable":controller.getSendOnlyTranslatedMessages},
|
||||||
|
"/set/enable/send_only_translated_messages": {"status": True, "variable":controller.setEnableSendOnlyTranslatedMessages},
|
||||||
|
"/set/disable/send_only_translated_messages": {"status": True, "variable":controller.setDisableSendOnlyTranslatedMessages},
|
||||||
|
|
||||||
|
"/get/data/logger_feature": {"status": True, "variable":controller.getLoggerFeature},
|
||||||
|
"/set/enable/logger_feature": {"status": True, "variable":controller.setEnableLoggerFeature},
|
||||||
|
"/set/disable/logger_feature": {"status": True, "variable":controller.setDisableLoggerFeature},
|
||||||
|
|
||||||
|
"/run/open_filepath_logs": {"status": True, "variable":controller.openFilepathLogs},
|
||||||
|
|
||||||
|
"/get/data/vrc_mic_mute_sync": {"status": True, "variable":controller.getVrcMicMuteSync},
|
||||||
|
"/set/enable/vrc_mic_mute_sync": {"status": True, "variable":controller.setEnableVrcMicMuteSync},
|
||||||
|
"/set/disable/vrc_mic_mute_sync": {"status": True, "variable":controller.setDisableVrcMicMuteSync},
|
||||||
|
|
||||||
|
"/get/data/send_message_to_vrc": {"status": True, "variable":controller.getSendMessageToVrc},
|
||||||
|
"/set/enable/send_message_to_vrc": {"status": True, "variable":controller.setEnableSendMessageToVrc},
|
||||||
|
"/set/disable/send_message_to_vrc": {"status": True, "variable":controller.setDisableSendMessageToVrc},
|
||||||
|
|
||||||
|
"/get/data/send_received_message_to_vrc": {"status": True, "variable":controller.getSendReceivedMessageToVrc},
|
||||||
|
"/set/enable/send_received_message_to_vrc": {"status": True, "variable":controller.setEnableSendReceivedMessageToVrc},
|
||||||
|
"/set/disable/send_received_message_to_vrc": {"status": True, "variable":controller.setDisableSendReceivedMessageToVrc},
|
||||||
|
|
||||||
|
# WebSocket Settings
|
||||||
|
"/get/data/websocket_host": {"status": True, "variable":controller.getWebSocketHost},
|
||||||
|
"/set/data/websocket_host": {"status": True, "variable":controller.setWebSocketHost},
|
||||||
|
"/get/data/websocket_port": {"status": True, "variable":controller.getWebSocketPort},
|
||||||
|
"/set/data/websocket_port": {"status": True, "variable":controller.setWebSocketPort},
|
||||||
|
"/get/data/websocket_server": {"status": True, "variable":controller.getWebSocketServer},
|
||||||
|
"/set/enable/websocket_server": {"status": True, "variable":controller.setEnableWebSocketServer},
|
||||||
|
"/set/disable/websocket_server": {"status": True, "variable":controller.setDisableWebSocketServer},
|
||||||
|
|
||||||
|
# Clipboard Settings
|
||||||
|
"/get/data/clipboard": {"status": True, "variable":controller.getClipboard},
|
||||||
|
"/set/enable/clipboard": {"status": True, "variable":controller.setEnableClipboard},
|
||||||
|
"/set/disable/clipboard": {"status": True, "variable":controller.setDisableClipboard},
|
||||||
|
|
||||||
|
# Advanced Settings
|
||||||
|
"/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress},
|
||||||
|
"/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress},
|
||||||
|
|
||||||
|
"/get/data/osc_port": {"status": True, "variable":controller.getOscPort},
|
||||||
|
"/set/data/osc_port": {"status": True, "variable":controller.setOscPort},
|
||||||
|
|
||||||
|
"/get/data/notification_vrc_sfx": {"status": True, "variable":controller.getNotificationVrcSfx},
|
||||||
|
"/set/enable/notification_vrc_sfx": {"status": True, "variable":controller.setEnableNotificationVrcSfx},
|
||||||
|
"/set/disable/notification_vrc_sfx": {"status": True, "variable":controller.setDisableNotificationVrcSfx},
|
||||||
|
|
||||||
|
"/run/open_filepath_config_file": {"status": True, "variable":controller.openFilepathConfigFile},
|
||||||
|
|
||||||
|
# "/run/start_watchdog": {"status": True, "variable":controller.startWatchdog},
|
||||||
|
"/run/feed_watchdog": {"status": True, "variable":controller.feedWatchdog},
|
||||||
|
# "/run/stop_watchdog": {"status": True, "variable":controller.stopWatchdog},
|
||||||
|
}
|
||||||
|
|
||||||
|
init_mapping = {key:value for key, value in mapping.items() if key.startswith("/get/data/")}
|
||||||
|
controller.setInitMapping(init_mapping)
|
||||||
|
|
||||||
|
DEFAULT_WORKER_COUNT = 3 # 必要なら増やす
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = DEFAULT_WORKER_COUNT) -> None:
|
||||||
|
self.queue: Queue[Tuple[str, Any]] = Queue()
|
||||||
|
self._stop_event: Event = Event()
|
||||||
|
self.controller = controller_instance
|
||||||
|
self.mapping = mapping_data
|
||||||
|
self._threads: list[Thread] = []
|
||||||
|
self._worker_count = worker_count
|
||||||
|
|
||||||
|
# エンドポイントごとの排他制御用 Lock を作成
|
||||||
|
# enable/disable ペアは同じロックキーに正規化する
|
||||||
|
def _canonical_lock_key(endpoint: str) -> str:
|
||||||
|
if not isinstance(endpoint, str):
|
||||||
|
return str(endpoint)
|
||||||
|
if endpoint.startswith("/set/enable/"):
|
||||||
|
return "/lock/set/" + endpoint[len("/set/enable/"):]
|
||||||
|
if endpoint.startswith("/set/disable/"):
|
||||||
|
return "/lock/set/" + endpoint[len("/set/disable/"):]
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
# mapping に含まれるすべてのエンドポイントを走査して正規化キー集合を作る
|
||||||
|
lock_keys = set()
|
||||||
|
for key in self.mapping.keys():
|
||||||
|
lock_keys.add(_canonical_lock_key(key))
|
||||||
|
|
||||||
|
# 正規化キーごとに Lock を割り当てる
|
||||||
|
self._endpoint_locks: dict[str, Lock] = {k: Lock() for k in lock_keys}
|
||||||
|
|
||||||
|
# 正規化関数をインスタンスに保存
|
||||||
|
self._canonical_lock_key = _canonical_lock_key
|
||||||
|
|
||||||
|
def receiver(self) -> None:
|
||||||
|
"""Read lines from stdin, parse JSON and enqueue requests.
|
||||||
|
|
||||||
|
Uses blocking readline but honors stop via _stop_event checked between reads.
|
||||||
|
"""
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
if not line:
|
||||||
|
# EOF reached; sleep briefly and re-check stop event
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
received_data = json.loads(line.strip())
|
||||||
|
|
||||||
|
if received_data:
|
||||||
|
endpoint = received_data.get("endpoint")
|
||||||
|
data = received_data.get("data")
|
||||||
|
data = encodeBase64(data) if data is not None else None
|
||||||
|
printLog(endpoint, {"receive_data": data})
|
||||||
|
self.queue.put((endpoint, data))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# malformed input; log and continue
|
||||||
|
errorLogging()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
def startReceiver(self) -> None:
|
||||||
|
th_receiver = Thread(target=self.receiver, name="main_receiver")
|
||||||
|
th_receiver.daemon = True
|
||||||
|
th_receiver.start()
|
||||||
|
self._threads.append(th_receiver)
|
||||||
|
|
||||||
|
def handleRequest(self, endpoint: str, data: Any = None) -> tuple:
|
||||||
|
result = None # デフォルト値を設定
|
||||||
|
status = 500 # デフォルト値を設定
|
||||||
|
|
||||||
|
handler = self.mapping.get(endpoint)
|
||||||
|
if handler is None:
|
||||||
|
response = "Invalid endpoint"
|
||||||
|
status = 404
|
||||||
|
elif handler["status"] is False:
|
||||||
|
response = "Locked endpoint"
|
||||||
|
status = 423
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
response = handler["variable"](data)
|
||||||
|
status = response.get("status")
|
||||||
|
result = response.get("result")
|
||||||
|
time.sleep(0.2) # 処理の安定化のために少し待機
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
result = "Internal error"
|
||||||
|
status = 500
|
||||||
|
|
||||||
|
return result, status
|
||||||
|
|
||||||
|
def _call_handler(self, endpoint: str, data: Any = None) -> tuple:
|
||||||
|
result = None
|
||||||
|
status = 500
|
||||||
|
handler = self.mapping.get(endpoint)
|
||||||
|
if handler is None:
|
||||||
|
response = "Invalid endpoint"
|
||||||
|
status = 404
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
response = handler["variable"](data)
|
||||||
|
status = response.get("status", 500)
|
||||||
|
result = response.get("result", None)
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
result = "Internal error"
|
||||||
|
status = 500
|
||||||
|
return result, status
|
||||||
|
|
||||||
|
def handler(self) -> None:
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
endpoint, data = self.queue.get(timeout=0.5)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# endpoint をロック用の正規化キーに変換してロックを取得
|
||||||
|
lock_key = self._canonical_lock_key(endpoint)
|
||||||
|
lock = self._endpoint_locks.get(lock_key)
|
||||||
|
|
||||||
|
if lock is not None:
|
||||||
|
acquired = lock.acquire(blocking=False)
|
||||||
|
if not acquired:
|
||||||
|
# 同一機能で既に処理中 -> 少し待って再キュー
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.queue.put((endpoint, data))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
result, status = self._call_handler(endpoint, data)
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
else:
|
||||||
|
result, status = self._call_handler(endpoint, data)
|
||||||
|
|
||||||
|
if status == 423:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.queue.put((endpoint, data))
|
||||||
|
else:
|
||||||
|
printLog(endpoint, {"status": status, "send_data": result})
|
||||||
|
printResponse(status, endpoint, result)
|
||||||
|
|
||||||
|
def startHandler(self) -> None:
|
||||||
|
for i in range(max(1, self._worker_count)):
|
||||||
|
th_handler = Thread(target=self.handler, name=f"main_handler_{i}")
|
||||||
|
th_handler.daemon = True
|
||||||
|
th_handler.start()
|
||||||
|
self._threads.append(th_handler)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the main loop to keep the program running."""
|
||||||
|
try:
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def stop(self, wait: float = 2.0) -> None:
|
||||||
|
"""Signal threads to stop and wait for them to finish.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wait: maximum seconds to wait for threads to join.
|
||||||
|
"""
|
||||||
|
# Controller 経由でシャットダウン(model.shutdown() → telemetry.shutdown() が呼ばれる)
|
||||||
|
try:
|
||||||
|
self.controller.shutdown()
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
|
||||||
|
self._stop_event.set()
|
||||||
|
# give threads a chance to exit
|
||||||
|
start = time.time()
|
||||||
|
for th in self._threads:
|
||||||
|
remaining = max(0.0, wait - (time.time() - start))
|
||||||
|
th.join(timeout=remaining)
|
||||||
|
|
||||||
|
# 外部から参照可能なインスタンスを提供
|
||||||
|
main_instance = Main(controller_instance=controller, mapping_data=mapping)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main_instance.startReceiver()
|
||||||
|
main_instance.startHandler()
|
||||||
|
|
||||||
|
main_instance.controller.setWatchdogCallback(main_instance.stop)
|
||||||
|
main_instance.controller.init()
|
||||||
|
|
||||||
|
# mappingのすべてのstatusをTrueにする
|
||||||
|
for key in mapping.keys():
|
||||||
|
mapping[key]["status"] = True
|
||||||
|
|
||||||
|
main_instance.start()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""models package init for static analysis and packaging."""
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# subpackages are discovered implicitly
|
||||||
|
]
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from psutil import process_iter
|
||||||
|
import openvr
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils import printLog
|
||||||
|
except ImportError:
|
||||||
|
def printLog(data, *args, **kwargs):
|
||||||
|
print(data, *args, **kwargs)
|
||||||
|
|
||||||
|
def checkSteamvrRunning() -> bool:
|
||||||
|
_proc_name = "vrmonitor.exe" if os.name == "nt" else "vrmonitor"
|
||||||
|
return _proc_name in (p.name() for p in process_iter())
|
||||||
|
|
||||||
|
# Windows-specific imports via ctypes will be used when focusing windows
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
import ctypes
|
||||||
|
import ctypes.wintypes as wintypes
|
||||||
|
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
||||||
|
|
||||||
|
def find_windows_by_title_substring(substring: str):
|
||||||
|
HWND = wintypes.HWND
|
||||||
|
callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, HWND, wintypes.LPARAM)
|
||||||
|
found = []
|
||||||
|
|
||||||
|
def _cb(hwnd, lParam):
|
||||||
|
length = user32.GetWindowTextLengthW(hwnd)
|
||||||
|
if length == 0:
|
||||||
|
return True
|
||||||
|
buf = ctypes.create_unicode_buffer(length + 1)
|
||||||
|
# fill buffer with window title
|
||||||
|
user32.GetWindowTextW(hwnd, buf, length + 1)
|
||||||
|
title = buf.value
|
||||||
|
if substring.lower() in title.lower():
|
||||||
|
found.append(hwnd)
|
||||||
|
return True
|
||||||
|
|
||||||
|
user32.EnumWindows(callback_type(_cb), 0)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def find_windows_by_process_name(proc_name: str):
|
||||||
|
# iterate windows and match process id to name
|
||||||
|
HWND = wintypes.HWND
|
||||||
|
callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, HWND, wintypes.LPARAM)
|
||||||
|
found = []
|
||||||
|
|
||||||
|
def _cb(hwnd, lParam):
|
||||||
|
pid = wintypes.DWORD()
|
||||||
|
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
p = psutil.Process(pid.value)
|
||||||
|
if p.name().lower() == proc_name.lower():
|
||||||
|
found.append(hwnd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
user32.EnumWindows(callback_type(_cb), 0)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def focus_window(hwnd) -> bool:
|
||||||
|
try:
|
||||||
|
# Restore and set foreground
|
||||||
|
SW_RESTORE = 9
|
||||||
|
user32.ShowWindow(hwnd, SW_RESTORE)
|
||||||
|
res = user32.SetForegroundWindow(hwnd)
|
||||||
|
return bool(res)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy_to_clipboard_windows(text: str) -> bool:
|
||||||
|
try:
|
||||||
|
p = Popen(['clip'], stdin=PIPE, shell=False)
|
||||||
|
# Write as UTF-16LE with BOM so Windows clipboard receives correct Unicode
|
||||||
|
bom_utf16le = b"\xff\xfe"
|
||||||
|
p.communicate(bom_utf16le + text.encode('utf-16le'))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy_to_clipboard_pyperclip(text: str) -> bool:
|
||||||
|
try:
|
||||||
|
import pyperclip
|
||||||
|
pyperclip.copy(text)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy_to_clipboard_tk(text: str) -> bool:
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
r = tk.Tk()
|
||||||
|
r.withdraw()
|
||||||
|
r.clipboard_clear()
|
||||||
|
r.clipboard_append(text)
|
||||||
|
r.update()
|
||||||
|
r.destroy()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy_to_clipboard(text: str) -> bool:
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
if copy_to_clipboard_windows(text):
|
||||||
|
return True
|
||||||
|
if copy_to_clipboard_pyperclip(text):
|
||||||
|
return True
|
||||||
|
if copy_to_clipboard_tk(text):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def paste_via_pyautogui(countdown: int = 0) -> bool:
|
||||||
|
try:
|
||||||
|
import pyautogui
|
||||||
|
except Exception:
|
||||||
|
printLog('pyautogui not installed. Install with: pip install pyautogui')
|
||||||
|
return False
|
||||||
|
|
||||||
|
for i in range(countdown, 0, -1):
|
||||||
|
print(i, end=' ', flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# pyautogui.hotkey is a safe cross-platform way to send keys
|
||||||
|
pyautogui.hotkey('ctrl', 'v')
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
printLog(f'pyautogui failed to send hotkey: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Clipboard:
|
||||||
|
def __init__(self):
|
||||||
|
self._vr_monitor_thread = None
|
||||||
|
self._stop_monitoring = False
|
||||||
|
self.app_name = None
|
||||||
|
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
"""Initialize clipboard by starting VR monitor thread."""
|
||||||
|
self._stop_monitoring = False
|
||||||
|
self._vr_monitor_thread = threading.Thread(target=self._monitor_steamvr, daemon=True)
|
||||||
|
self._vr_monitor_thread.start()
|
||||||
|
self.app_name = None
|
||||||
|
printLog("Clipboard initialized. Waiting for SteamVR.")
|
||||||
|
|
||||||
|
def _monitor_steamvr(self):
|
||||||
|
"""Monitor SteamVR startup in background thread."""
|
||||||
|
printLog("Clipboard: VR monitor thread started.")
|
||||||
|
while not self._stop_monitoring:
|
||||||
|
if checkSteamvrRunning():
|
||||||
|
printLog("Clipboard: SteamVR detected. Setting up app info.")
|
||||||
|
self._setup_vr_app_name()
|
||||||
|
break
|
||||||
|
time.sleep(10)
|
||||||
|
printLog("Clipboard: VR monitor thread ended.")
|
||||||
|
|
||||||
|
def _setup_vr_app_name(self):
|
||||||
|
"""Setup VR application name from OpenVR."""
|
||||||
|
try:
|
||||||
|
openvr.init(openvr.VRApplication_Background)
|
||||||
|
apps = openvr.VRApplications()
|
||||||
|
|
||||||
|
app_count = apps.getApplicationCount()
|
||||||
|
running_apps = []
|
||||||
|
|
||||||
|
for i in range(app_count):
|
||||||
|
key = apps.getApplicationKeyByIndex(i)
|
||||||
|
name = apps.getApplicationPropertyString(
|
||||||
|
key,
|
||||||
|
openvr.VRApplicationProperty_Name_String
|
||||||
|
)
|
||||||
|
running_apps.append((key, name))
|
||||||
|
|
||||||
|
self.app_name = None
|
||||||
|
for key, name in running_apps:
|
||||||
|
if key.startswith("steam.app"):
|
||||||
|
self.app_name = name
|
||||||
|
break
|
||||||
|
openvr.shutdown()
|
||||||
|
except Exception as e:
|
||||||
|
printLog(f"Clipboard: Error setting up VR app name: {e}")
|
||||||
|
self.app_name = None
|
||||||
|
|
||||||
|
def copy_and_paste(self, message: str, window_name: str|None = None, countdown: int = 0) -> bool:
|
||||||
|
window_name = window_name if window_name is not None else self.app_name
|
||||||
|
|
||||||
|
# If window_name is provided, attempt to focus it (Windows only).
|
||||||
|
# If window_name is None, skip focusing and paste into the currently focused window.
|
||||||
|
if window_name is not None and sys.platform == 'win32':
|
||||||
|
printLog(f"paste: attempting to focus window matching '{window_name}'")
|
||||||
|
focused = False
|
||||||
|
|
||||||
|
# try title substring match first
|
||||||
|
wins = find_windows_by_title_substring(window_name)
|
||||||
|
printLog(f"paste: found {wins} windows matching title substring '{window_name}'")
|
||||||
|
for hwnd in wins:
|
||||||
|
if focus_window(hwnd):
|
||||||
|
focused = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# if not found by title, try treating window_name as process name
|
||||||
|
if not focused:
|
||||||
|
wins = find_windows_by_process_name(window_name)
|
||||||
|
printLog(f"paste: found {wins} windows matching process name '{window_name}'")
|
||||||
|
for hwnd in wins:
|
||||||
|
if focus_window(hwnd):
|
||||||
|
focused = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not focused:
|
||||||
|
printLog(f"copy_and_paste: no window found matching '{window_name}'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# small delay to allow focus to settle
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# copy
|
||||||
|
copied = copy_to_clipboard(message)
|
||||||
|
if not copied:
|
||||||
|
printLog("copy_and_paste: failed to copy to clipboard")
|
||||||
|
return False
|
||||||
|
# paste
|
||||||
|
pasted = paste_via_pyautogui(countdown)
|
||||||
|
return bool(pasted)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
clipboard = Clipboard()
|
||||||
|
clipboard.copy_and_paste("Sample text to copy to clipboard.", window_name=None, countdown=3)
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"""OSC helpers and a thin OSCQuery-enabled server wrapper.
|
||||||
|
|
||||||
|
This module provides `OSCHandler`, a convenience wrapper used by the
|
||||||
|
application to send OSC messages and expose OSCQuery endpoints when the
|
||||||
|
target address is localhost. The implementation is defensive: missing
|
||||||
|
utilities are handled gracefully and logging helpers are used where
|
||||||
|
available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
from time import sleep
|
||||||
|
from threading import Thread
|
||||||
|
from pythonosc import udp_client, dispatcher, osc_server
|
||||||
|
try:
|
||||||
|
from tinyoscquery.queryservice import OSCQueryService
|
||||||
|
from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient
|
||||||
|
from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port
|
||||||
|
from tinyoscquery.shared.node import OSCAccess
|
||||||
|
except Exception:
|
||||||
|
# tinyoscquery is optional for non-local usage; functionality that
|
||||||
|
# depends on it will be disabled if it's missing.
|
||||||
|
OSCQueryService = None # type: ignore
|
||||||
|
OSCQueryBrowser = None # type: ignore
|
||||||
|
OSCQueryClient = None # type: ignore
|
||||||
|
def get_open_udp_port() -> int: # type: ignore
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_open_tcp_port() -> int: # type: ignore
|
||||||
|
return 0
|
||||||
|
OSCAccess = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils import errorLogging
|
||||||
|
except Exception:
|
||||||
|
def errorLogging() -> None:
|
||||||
|
import traceback
|
||||||
|
print("Error occurred:", traceback.format_exc())
|
||||||
|
|
||||||
|
class OSCHandler:
|
||||||
|
"""Thin wrapper managing OSC send/receive and optional OSCQuery advertising.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip_address: OSC server client target / bind address
|
||||||
|
port: UDP port to send to
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ip_address: str = "127.0.0.1", port: int = 9000) -> None:
|
||||||
|
|
||||||
|
self.is_osc_query_enabled: bool = ip_address in ["127.0.0.1", "localhost"]
|
||||||
|
|
||||||
|
self.osc_ip_address: str = ip_address
|
||||||
|
self.osc_port: int = port
|
||||||
|
self.osc_parameter_muteself: str = "/avatar/parameters/MuteSelf"
|
||||||
|
self.osc_parameter_chatbox_typing: str = "/chatbox/typing"
|
||||||
|
self.osc_parameter_chatbox_input: str = "/chatbox/input"
|
||||||
|
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
|
||||||
|
self.osc_server_name: str = "VRChat-Client"
|
||||||
|
self.osc_server = None
|
||||||
|
self.osc_query_service = None
|
||||||
|
self.osc_query_service_name: str = "VRCT"
|
||||||
|
self.osc_server_ip_address: str = ip_address
|
||||||
|
self.http_port: Optional[int] = None
|
||||||
|
self.osc_server_port: Optional[int] = None
|
||||||
|
self.dict_filter_and_target: Dict[str, Callable] = {}
|
||||||
|
self.browser = None
|
||||||
|
|
||||||
|
def getIsOscQueryEnabled(self) -> bool:
|
||||||
|
"""Return whether OSCQuery support is enabled (local addresses only)."""
|
||||||
|
return self.is_osc_query_enabled
|
||||||
|
|
||||||
|
def setOscIpAddress(self, ip_address: str) -> None:
|
||||||
|
"""Change the OSC target IP address and reinitialize services."""
|
||||||
|
self.is_osc_query_enabled = ip_address in ["127.0.0.1", "localhost"]
|
||||||
|
|
||||||
|
self.oscServerStop()
|
||||||
|
self.osc_ip_address = ip_address
|
||||||
|
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
|
||||||
|
self.receiveOscParameters()
|
||||||
|
|
||||||
|
def setOscPort(self, port: int) -> None:
|
||||||
|
"""Change the OSC UDP port used for sending and reinitialize services."""
|
||||||
|
self.oscServerStop()
|
||||||
|
self.osc_port = port
|
||||||
|
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
|
||||||
|
self.receiveOscParameters()
|
||||||
|
|
||||||
|
# send OSC message typing
|
||||||
|
def sendTyping(self, flag: bool = False) -> None:
|
||||||
|
"""Send /chatbox/typing with a boolean flag."""
|
||||||
|
self.udp_client.send_message(self.osc_parameter_chatbox_typing, [flag])
|
||||||
|
|
||||||
|
# send OSC message
|
||||||
|
def sendMessage(self, message: str = "", notification: bool = True) -> None:
|
||||||
|
"""Send /chatbox/input if message is non-empty.
|
||||||
|
|
||||||
|
The second argument historically was a boolean flag for clearing; we keep
|
||||||
|
compatibility by sending [message, True, notification].
|
||||||
|
"""
|
||||||
|
if len(message) > 0:
|
||||||
|
self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification])
|
||||||
|
|
||||||
|
def getOSCParameterValue(self, address: str) -> Any:
|
||||||
|
if not self.is_osc_query_enabled:
|
||||||
|
# OSCQueryが無効な場合はNoneを返す
|
||||||
|
return None
|
||||||
|
value: Any = None
|
||||||
|
try:
|
||||||
|
# browserインスタンスを再利用し、毎回の生成と破棄を避ける
|
||||||
|
if self.browser is None:
|
||||||
|
# OSCQueryBrowser may not be available; guard
|
||||||
|
if OSCQueryBrowser is None:
|
||||||
|
return None
|
||||||
|
self.browser = OSCQueryBrowser()
|
||||||
|
sleep(1) # 初回のみスリープ
|
||||||
|
|
||||||
|
service = self.browser.find_service_by_name(self.osc_server_name)
|
||||||
|
if service is not None:
|
||||||
|
osc_query_client = OSCQueryClient(service)
|
||||||
|
mute_self_node = osc_query_client.query_node(address)
|
||||||
|
# mute_self_node may be None when the node is not present on the
|
||||||
|
# remote OSCQuery service. Also mute_self_node.value may be None
|
||||||
|
# or an empty list. Guard against those cases to avoid
|
||||||
|
# AttributeError: 'NoneType' object has no attribute 'value'
|
||||||
|
if mute_self_node is None:
|
||||||
|
return None
|
||||||
|
# prefer explicit checks rather than relying on exceptions
|
||||||
|
node_value = getattr(mute_self_node, 'value', None)
|
||||||
|
if not node_value:
|
||||||
|
return None
|
||||||
|
value = node_value[0]
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
# エラー発生時にbrowserをリセットして次回再初期化
|
||||||
|
if self.browser is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(self.browser, 'zc') and self.browser.zc is not None:
|
||||||
|
self.browser.zc.close()
|
||||||
|
if hasattr(self.browser, 'browser') and self.browser.browser is not None:
|
||||||
|
self.browser.browser.cancel()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.browser = None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def getOSCParameterMuteSelf(self) -> Optional[bool]:
|
||||||
|
"""Return the value of the MuteSelf parameter when available, else None."""
|
||||||
|
return self.getOSCParameterValue(self.osc_parameter_muteself)
|
||||||
|
|
||||||
|
def setDictFilterAndTarget(self, dict_filter_and_target: Dict[str, Callable]) -> None:
|
||||||
|
"""Set the mapping from OSC address filters to handler callables."""
|
||||||
|
self.dict_filter_and_target = dict_filter_and_target
|
||||||
|
|
||||||
|
def receiveOscParameters(self) -> None:
|
||||||
|
"""Start a local OSC server and advertise OSCQuery endpoints when supported.
|
||||||
|
|
||||||
|
If `tinyoscquery` is not available or OSCQuery is disabled, this is a
|
||||||
|
no-op.
|
||||||
|
"""
|
||||||
|
if not self.is_osc_query_enabled or OSCQueryService is None:
|
||||||
|
# OSCQuery が無効またはライブラリが無い場合は何もしない
|
||||||
|
return
|
||||||
|
|
||||||
|
self.osc_server_port = get_open_udp_port()
|
||||||
|
self.http_port = get_open_tcp_port()
|
||||||
|
osc_dispatcher = dispatcher.Dispatcher()
|
||||||
|
for filter, target in self.dict_filter_and_target.items():
|
||||||
|
osc_dispatcher.map(filter, target)
|
||||||
|
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher)
|
||||||
|
Thread(target=self.oscServerServe, daemon=True).start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# osc_server_name + UTC timestamp でユニークなサービス名を生成
|
||||||
|
service_name = f"{self.osc_query_service_name}:{int(time.time())}"
|
||||||
|
self.osc_query_service = OSCQueryService(service_name, self.http_port, self.osc_server_port)
|
||||||
|
for filter, target in self.dict_filter_and_target.items():
|
||||||
|
# OSCAccess may be None when tinyoscquery is not present; guard
|
||||||
|
if OSCAccess is not None:
|
||||||
|
self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
errorLogging()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
def oscServerServe(self) -> None:
|
||||||
|
"""Run the OSC server loop with a longer poll interval to reduce CPU."""
|
||||||
|
# ポーリング間隔を長くして(2秒から10秒に)CPUの使用率を削減
|
||||||
|
if self.osc_server is not None:
|
||||||
|
self.osc_server.serve_forever(10)
|
||||||
|
|
||||||
|
def oscServerStop(self) -> None:
|
||||||
|
"""Stop and clean up any running OSC server and OSCQuery service."""
|
||||||
|
if isinstance(self.osc_server, osc_server.ThreadingOSCUDPServer):
|
||||||
|
try:
|
||||||
|
self.osc_server.shutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.osc_server = None
|
||||||
|
if OSCQueryService is not None and isinstance(self.osc_query_service, OSCQueryService):
|
||||||
|
try:
|
||||||
|
self.osc_query_service.http_server.shutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.osc_query_service = None
|
||||||
|
# browser がある場合はクリーンアップ
|
||||||
|
if self.browser is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(self.browser, 'zc') and self.browser.zc is not None:
|
||||||
|
self.browser.zc.close()
|
||||||
|
if hasattr(self.browser, 'browser') and self.browser.browser is not None:
|
||||||
|
self.browser.browser.cancel()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.browser = None
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
handler = OSCHandler()
|
||||||
|
handler.setDictFilterAndTarget({
|
||||||
|
"/avatar/parameters/MuteSelf": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||||
|
"/chatbox/typing": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||||
|
"/chatbox/input": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||||
|
})
|
||||||
|
handler.receiveOscParameters()
|
||||||
|
sleep(5)
|
||||||
|
handler.sendTyping(True)
|
||||||
|
sleep(1)
|
||||||
|
handler.sendMessage(message="Hello World 1", notification=True)
|
||||||
|
sleep(10)
|
||||||
|
|
||||||
|
print("IP address changed to 192.168.193.2")
|
||||||
|
handler.setOscIpAddress("192.168.193.2")
|
||||||
|
sleep(5)
|
||||||
|
handler.sendMessage(message="Hello World 2", notification=True)
|
||||||
|
|
||||||
|
print("IP address changed to 127.0.0.1")
|
||||||
|
handler.setOscIpAddress("127.0.0.1")
|
||||||
|
sleep(5)
|
||||||
|
handler.sendMessage(message="Hello World 3", notification=True)
|
||||||
|
sleep(10)
|
||||||
|
handler.oscServerStop()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""models.overlay package init for static analysis."""
|
||||||
|
|
||||||
|
from . import overlay_utils # re-export helper for ease-of-use in tooling
|
||||||
|
|
||||||
|
__all__ = ["overlay_utils"]
|
||||||