Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5ef3011a | ||
|
|
63dab6d224 | ||
|
|
38dd76dfb3 | ||
|
|
fe642de526 | ||
|
|
f4b7eefc38 | ||
|
|
5bbbad02b4 | ||
|
|
bd85dd5faf | ||
|
|
d91ba3d219 | ||
|
|
7992c40815 | ||
|
|
574b01db97 | ||
|
|
2d6049b7bd | ||
|
|
96d0f2e6f9 | ||
|
|
b46bd4d0dd | ||
|
|
411fc46189 | ||
|
|
48427e330f | ||
|
|
5ddc2451c1 | ||
|
|
e357cad2bb | ||
|
|
80be8b2eaa | ||
|
|
9a5aaea8ca | ||
|
|
7d585615a9 | ||
|
|
3f3375dcac | ||
|
|
f555eae653 | ||
|
|
e805a6e037 | ||
|
|
3ed82ed7bb | ||
|
|
d638e512c9 | ||
|
|
204b966935 | ||
|
|
8318ba5e55 | ||
|
|
d39c595828 | ||
|
|
04154162c6 | ||
|
|
47f8e0f04c | ||
|
|
7e4ada5f4b | ||
|
|
b0b4f87848 | ||
|
|
d9085b82bf | ||
|
|
8e5770ff59 | ||
|
|
beb420c69d | ||
|
|
1d8b898924 | ||
|
|
b5b4c5ced7 | ||
|
|
b4e9d77895 | ||
|
|
21a08b3c7a | ||
|
|
6977d6bf73 | ||
|
|
39f81dd0c0 | ||
|
|
b3191f5e0b | ||
|
|
78f2f21142 | ||
|
|
497fc1d1ae | ||
|
|
d83e1c6fb2 | ||
|
|
c5218d485a | ||
|
|
19cbd3b8da | ||
|
|
c0c5630bfb | ||
|
|
969b5fcfbb | ||
|
|
0739fa2195 | ||
|
|
3c62adab3c | ||
|
|
edc601c35e | ||
|
|
951370eee7 | ||
|
|
fa81f4a150 | ||
|
|
e6a90c40fb | ||
|
|
2e0a1c00fa | ||
|
|
109754fac9 | ||
|
|
c3fbff3d7a | ||
|
|
6dd967fe24 | ||
|
|
f0bfdba944 | ||
|
|
7f76b5f4ee | ||
|
|
35f4d8f15a | ||
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
d57482d966 | ||
|
|
3d7c2aff4b | ||
|
|
3757c5dde7 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 | ||
|
|
8b9174b67e | ||
|
|
6c04a6cf69 | ||
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 | ||
|
|
bd2e2e77d5 | ||
|
|
6282ef14e8 | ||
|
|
558f23688f | ||
|
|
541f2de599 | ||
|
|
68116254cd | ||
|
|
77ff4599ea | ||
|
|
652f5a3247 | ||
|
|
9b6e57b143 | ||
|
|
bdc19256bf | ||
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
9fc164a69d | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
208c045d3d | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e | ||
|
|
8c7d129089 | ||
|
|
a3a9b98a1d | ||
|
|
f2f66579cb | ||
|
|
1354cf2a87 | ||
|
|
15f5592429 | ||
|
|
dc69094f00 | ||
|
|
8ad3783a36 | ||
|
|
15d5dd8e92 | ||
|
|
b07edb977b | ||
|
|
1509fc2039 | ||
|
|
2877bb8faa | ||
|
|
a83410cb49 | ||
|
|
4ff9f2235b | ||
|
|
1bc611f097 | ||
|
|
4e0a3941ff | ||
|
|
4e885dac3e | ||
|
|
eafd131a22 | ||
|
|
289ce3716b | ||
|
|
2242070bdc | ||
|
|
ee83cea11c | ||
|
|
6e571a43d8 | ||
|
|
466a0245a9 | ||
|
|
d9ce5e4ce7 | ||
|
|
6b0dd291c2 | ||
|
|
4ff77d0d7e | ||
|
|
e00da5d8d2 | ||
|
|
1a525a59ab | ||
|
|
d5581c4236 | ||
|
|
4bf950574c | ||
|
|
0b9e434c4b | ||
|
|
c4b1466643 | ||
|
|
488842029b | ||
|
|
1b0ce59371 | ||
|
|
97bea89204 | ||
|
|
0892dfc601 | ||
|
|
20d66ff401 | ||
|
|
e2fda532d1 | ||
|
|
68035af7b3 | ||
|
|
ee25555f0e | ||
|
|
e528510f5c | ||
|
|
afa0527b4c | ||
|
|
dd7d23498f | ||
|
|
eaaea2b24c | ||
|
|
58d0aefae1 | ||
|
|
11c9915a70 | ||
|
|
6cf6b0d645 | ||
|
|
1980a39b73 | ||
|
|
8d3fbeeb22 | ||
|
|
2678644c2c | ||
|
|
25da9fe119 | ||
|
|
03d89d22ec | ||
|
|
100cd25ffc | ||
|
|
6284a74a6e | ||
|
|
978cd8ba8e | ||
|
|
c76f28824c | ||
|
|
703ecfae40 | ||
|
|
b15c59aa48 | ||
|
|
98a4ce701c | ||
|
|
2fc0de8f4e | ||
|
|
66fd551d68 | ||
|
|
6e4e9a002b | ||
|
|
91278b0b3d | ||
|
|
ff9b3ed1a2 | ||
|
|
0c855d4ba8 | ||
|
|
9176e9d255 | ||
|
|
2bab917b3e | ||
|
|
b80086a349 | ||
|
|
8a4fca9bd5 | ||
|
|
13288a6b3a | ||
|
|
57ed552f43 | ||
|
|
5a4d65986d | ||
|
|
427b3bb774 | ||
|
|
4912f385c6 | ||
|
|
dcb6d0feab | ||
|
|
de8c762d79 | ||
|
|
056659304a | ||
|
|
525bf18a03 | ||
|
|
7714c3f611 | ||
|
|
768b9fb7c1 | ||
|
|
db4f028955 | ||
|
|
e3928ffbcc | ||
|
|
db5159bd5e | ||
|
|
2626e1ebf0 | ||
|
|
5d69ba0796 | ||
|
|
c39ad97b8e | ||
|
|
7c73f060ed | ||
|
|
f0aa7df3b0 | ||
|
|
5882dec23a | ||
|
|
835c9deb28 | ||
|
|
a4ecc7ac7f | ||
|
|
526a195e4d | ||
|
|
5e77560313 | ||
|
|
1abacfe90b | ||
|
|
d042c589df | ||
|
|
9f0f2870d0 | ||
|
|
9dd445b0ce | ||
|
|
8e1cc6e75b | ||
|
|
a36887d885 | ||
|
|
e300d04502 | ||
|
|
3d8afac9e1 | ||
|
|
81d2571f8f | ||
|
|
e4a0d2c56c | ||
|
|
55be189772 | ||
|
|
56352478fe | ||
|
|
f16d0406ea | ||
|
|
9d1871dc80 | ||
|
|
137f29e9e5 | ||
|
|
0e81a165e4 | ||
|
|
a73bcc05bc | ||
|
|
cd97018c4d | ||
|
|
a42ed5c388 | ||
|
|
f4bd80b5f9 | ||
|
|
498542f8be | ||
|
|
1aaa31ceb0 | ||
|
|
6fd63870f0 | ||
|
|
0be5350a1c | ||
|
|
48a5489c52 | ||
|
|
fe0fb9a884 | ||
|
|
91a2846794 | ||
|
|
625c9c2e40 | ||
|
|
710b338e6d | ||
|
|
024468adbd | ||
|
|
e4d173ad28 | ||
|
|
f4218b032a | ||
|
|
b87ebc6e1c | ||
|
|
d901b0ed34 | ||
|
|
519340dbf8 | ||
|
|
a7f6a6cc16 | ||
|
|
a0e56757d8 | ||
|
|
a618c2a2c6 | ||
|
|
07d8f783e0 | ||
|
|
623bfe32f7 | ||
|
|
6862735aa0 | ||
|
|
5461794a1e | ||
|
|
d9cd9acd27 | ||
|
|
b2788ac7a0 | ||
|
|
d4f6e25f92 | ||
|
|
17bcaba026 | ||
|
|
aed4e6b1e4 | ||
|
|
50897e97af | ||
|
|
e57137b101 | ||
|
|
3370ecb2ef | ||
|
|
8225a5a12f | ||
|
|
5655362a00 | ||
|
|
2f2884e991 | ||
|
|
6061a1d2bf | ||
|
|
a28d316ca4 | ||
|
|
d53230245f | ||
|
|
d2e8d1c261 | ||
|
|
fe86e1fb0f | ||
|
|
03441c8582 | ||
|
|
d255edfd5f | ||
|
|
52187daafa | ||
|
|
6f46776484 | ||
|
|
50b2b37bc1 | ||
|
|
35eed6dca0 | ||
|
|
e337dd920d | ||
|
|
671c992f3a | ||
|
|
b5ff4d329b | ||
|
|
941eb063fb | ||
|
|
148e71985f | ||
|
|
efd22b36ef | ||
|
|
70ea24ac83 | ||
|
|
3c9b7ad60b | ||
|
|
18cc1273f6 | ||
|
|
a6f9f0d88c | ||
|
|
d36210ad6b | ||
|
|
b2262d062c | ||
|
|
27be7fd217 | ||
|
|
97f210c9de | ||
|
|
a230fc6aaa | ||
|
|
b042a34726 | ||
|
|
1673ad7e70 | ||
|
|
f7eab31b57 | ||
|
|
4553188739 | ||
|
|
7550ac327e | ||
|
|
5b28cf6c66 | ||
|
|
4ea27cbdc2 | ||
|
|
e474a2114c | ||
|
|
06e4834e27 | ||
|
|
083e7071f0 | ||
|
|
14be24a515 | ||
|
|
fbdfd8df69 | ||
|
|
17b5308515 | ||
|
|
8f4c0901f0 | ||
|
|
e18065a1bf | ||
|
|
ad0493f8a0 | ||
|
|
b85368f8d4 | ||
|
|
18ea1e7acf | ||
|
|
50d7627b81 | ||
|
|
5934cf718d | ||
|
|
c57ab0e703 | ||
|
|
2fb7281d07 | ||
|
|
4c0d55d2e6 | ||
|
|
a1904d4a80 | ||
|
|
8856f7efa6 | ||
|
|
bdedc48aca | ||
|
|
8a6b4e5499 | ||
|
|
f7a6de912c | ||
|
|
48fa48c159 | ||
|
|
c811beadb4 | ||
|
|
4c76ad2ba7 | ||
|
|
0a63b00199 | ||
|
|
0761d5a8c2 | ||
|
|
f6ad17729f | ||
|
|
1f9f564def | ||
|
|
198755c3f5 | ||
|
|
d2b6d5f083 | ||
|
|
444e3192a2 | ||
|
|
c12d3df073 | ||
|
|
82b1548ae0 | ||
|
|
afd1d2ae6a | ||
|
|
90aafe1b5f | ||
|
|
143d33e285 | ||
|
|
09938e0aa6 | ||
|
|
fd59aff4d0 | ||
|
|
59133b9d8d | ||
|
|
cca42309c4 | ||
|
|
510ee1b282 | ||
|
|
c4d3020a1d | ||
|
|
132174568f | ||
|
|
f3af30ae53 | ||
|
|
6d3f270a8f | ||
|
|
86621661df | ||
|
|
d668917694 | ||
|
|
8bc1d09c4a | ||
|
|
76b90c179b | ||
|
|
ee17d9416a | ||
|
|
7c389a0641 | ||
|
|
4241120853 | ||
|
|
5998fc3edd | ||
|
|
392f8a1ce0 | ||
|
|
04203c4103 | ||
|
|
188cd7f023 | ||
|
|
8d17f5bd3c | ||
|
|
9d39880e9d | ||
|
|
f27e213d13 | ||
|
|
69ecb2c503 | ||
|
|
0585a0732f | ||
|
|
fab4adb07e | ||
|
|
5c3ae278be | ||
|
|
19a067f4e8 | ||
|
|
9430094072 | ||
|
|
a7b5e0df6c | ||
|
|
83e9725d02 | ||
|
|
086dea8594 | ||
|
|
50490b70b7 | ||
|
|
fac3a0acd7 | ||
|
|
73c4d6a604 | ||
|
|
6ba63db47f | ||
|
|
d3c8e4badb | ||
|
|
fdd16ad3c7 | ||
|
|
9178faef6d | ||
|
|
665318164e | ||
|
|
44f8254b43 | ||
|
|
e085fb1b10 | ||
|
|
48ed5d5c9b | ||
|
|
e2da300e09 | ||
|
|
e11a37ffec | ||
|
|
20c1f0576b | ||
|
|
fef8cece90 | ||
|
|
f549f26541 | ||
|
|
9ba836b814 | ||
|
|
26e471616c | ||
|
|
f20c17f27d | ||
|
|
fd188f6f65 | ||
|
|
d10ba456d7 | ||
|
|
ee3808dd2f | ||
|
|
a345d41e9e | ||
|
|
d36ea8a906 | ||
|
|
28f8533d0a | ||
|
|
587b9e8800 | ||
|
|
d2d97149fc | ||
|
|
c3a1b0f609 | ||
|
|
1b25ed5903 | ||
|
|
e87c96300b | ||
|
|
ba5bf33b5a | ||
|
|
94e1c09647 | ||
|
|
6bd7ad9413 | ||
|
|
d75032e853 | ||
|
|
261ecfea4d | ||
|
|
6f9cd94e66 | ||
|
|
ee2e1ed5fc | ||
|
|
92c1317d4c | ||
|
|
cfb97a087b | ||
|
|
76ba99aa6c | ||
|
|
330d7ed95c | ||
|
|
878e73524b | ||
|
|
13b0aa1a7b | ||
|
|
326342b834 | ||
|
|
8bbd610f12 | ||
|
|
549c2af988 | ||
|
|
d02c56a0e0 | ||
|
|
1f3654cc01 | ||
|
|
b6b6b0310f | ||
|
|
4c91bffcc7 | ||
|
|
d730a6cd06 | ||
|
|
67050f972e | ||
|
|
ffafd32693 | ||
|
|
ab4ec5e468 | ||
|
|
a07277feb8 | ||
|
|
a3cd4b6e66 | ||
|
|
25b7e76407 | ||
|
|
630cb13066 | ||
|
|
81188a0129 | ||
|
|
cf1abd90a0 | ||
|
|
6865a8be45 | ||
|
|
b63fcc80a1 | ||
|
|
a19ba6d48b | ||
|
|
70b6efe922 | ||
|
|
0b07269346 | ||
|
|
336954eeeb | ||
|
|
2988e5eef2 | ||
|
|
fe87889ccd | ||
|
|
0598ab4400 | ||
|
|
7d090f8ecc | ||
|
|
088ee47b3c | ||
|
|
a320b4c001 | ||
|
|
3adce95340 | ||
|
|
2a6bfd792a | ||
|
|
0816f1f589 | ||
|
|
497def8055 | ||
|
|
62be26ff47 | ||
|
|
6a81d80778 | ||
|
|
8914beae26 | ||
|
|
60c38a8dea | ||
|
|
4e0934debf | ||
|
|
17126303fa | ||
|
|
98fa95190c | ||
|
|
3a2261107c | ||
|
|
b1b64abed9 | ||
|
|
05356b6652 | ||
|
|
cbd0c6615d | ||
|
|
90e389efa2 | ||
|
|
00a0b26797 | ||
|
|
69b5ffe3bf | ||
|
|
31539294eb | ||
|
|
d1a2307134 | ||
|
|
97b777f1b2 | ||
|
|
c565bfc6bc | ||
|
|
a63137abc9 | ||
|
|
3ca06f8d50 | ||
|
|
e5b0e202be | ||
|
|
41db6aafd9 | ||
|
|
c66385d1f7 | ||
|
|
b0304fd875 | ||
|
|
5bfc3efc9c | ||
|
|
62845cfcf1 | ||
|
|
5c536e72df | ||
|
|
7cfece1592 | ||
|
|
f393b2c9ba | ||
|
|
164f2e5472 | ||
|
|
1434ecbc2e | ||
|
|
c2e443d4ac | ||
|
|
c079125458 | ||
|
|
4c1d7b34b9 | ||
|
|
53772b3fe3 | ||
|
|
28d3c40835 | ||
|
|
a01faecd0c | ||
|
|
a1b071e38e | ||
|
|
571622a0f4 | ||
|
|
4ab84d7209 | ||
|
|
4b90e70aad | ||
|
|
30a938e348 | ||
|
|
f6f4384e42 | ||
|
|
911b11211e | ||
|
|
ee0da9fc3a | ||
|
|
8bb596c74f | ||
|
|
d0fdeb33e0 | ||
|
|
ae7f41032e | ||
|
|
f30cb6badb | ||
|
|
97a9c1e08a | ||
|
|
1e699c027d | ||
|
|
9bd731729d | ||
|
|
dc17eccf76 | ||
|
|
3649a33afb | ||
|
|
d76bcc1366 | ||
|
|
2987f1c680 | ||
|
|
ab93ee0406 | ||
|
|
69dd933a7d | ||
|
|
5440de0453 | ||
|
|
f2e130c70e | ||
|
|
4c845c26f7 | ||
|
|
a43acca375 | ||
|
|
635520aa85 | ||
|
|
fce9759e93 | ||
|
|
18243cd4b0 | ||
|
|
75a6e51e68 | ||
|
|
312edd4b15 | ||
|
|
be3e67b2c1 | ||
|
|
a5765f81fe | ||
|
|
4bee92abb0 | ||
|
|
d9dcb90745 | ||
|
|
fbd25353d5 | ||
|
|
44c3e73577 | ||
|
|
9cbad1a448 | ||
|
|
3a3ddbc2bc | ||
|
|
63a3c33df9 | ||
|
|
32d7400f7d | ||
|
|
38191f976b | ||
|
|
c380e0e110 | ||
|
|
2199b8eca7 | ||
|
|
88455604a2 | ||
|
|
63eb69091f | ||
|
|
fc668810bd | ||
|
|
2773e3a48a | ||
|
|
18a03f6ccc | ||
|
|
e9348f5512 | ||
|
|
ce65226ea4 | ||
|
|
a3b3079b63 | ||
|
|
22de4bb13b | ||
|
|
eef7bb0cad | ||
|
|
8f1fbbdeef | ||
|
|
f3562e091f | ||
|
|
2001258ef4 | ||
|
|
52a66ea304 | ||
|
|
21ddb1541f | ||
|
|
a3ec6d79b0 | ||
|
|
bb8c315ffc | ||
|
|
f2ae398d3f | ||
|
|
02e33f68b4 | ||
|
|
8c4c9d88da | ||
|
|
49cb98a606 | ||
|
|
bdd18b384b | ||
|
|
32ab716f0d | ||
|
|
fe24529fce | ||
|
|
b59c097bca | ||
|
|
6d6414eb62 | ||
|
|
5b1c20d625 | ||
|
|
af6c8ab935 | ||
|
|
4acb5f924a | ||
|
|
b1b99f1066 | ||
|
|
004ee6f143 | ||
|
|
19455f89d3 | ||
|
|
18d98e3bd0 | ||
|
|
af038b7fc8 | ||
|
|
4a9297dc65 | ||
|
|
49fefb41a6 | ||
|
|
2fa64dfdd0 | ||
|
|
e0f777b9b4 | ||
|
|
dfe3de14d8 | ||
|
|
abe234c7ce | ||
|
|
d7bf67fd0e | ||
|
|
8218996f04 | ||
|
|
ed91e5d607 | ||
|
|
0bf0b0d381 | ||
|
|
d88559f5de | ||
|
|
f398115df3 | ||
|
|
82413fecab | ||
|
|
66352de1eb | ||
|
|
a7730f6e93 | ||
|
|
d5a5b24223 | ||
|
|
472dba2665 | ||
|
|
a5444b11b3 | ||
|
|
5a05a68524 | ||
|
|
36be61bbe1 | ||
|
|
ec6ab983f7 | ||
|
|
69630c2be0 | ||
|
|
8e605dd1d4 | ||
|
|
8bb0cf02ad | ||
|
|
60c054f457 | ||
|
|
5648ee974a | ||
|
|
dc4968f961 | ||
|
|
36d1370720 | ||
|
|
7ead5537f6 | ||
|
|
7fe797d44d | ||
|
|
a1e959420d | ||
|
|
4c99b40257 | ||
|
|
ead0958d18 | ||
|
|
220af8068e | ||
|
|
a1730b2d52 | ||
|
|
1a1d6c0835 | ||
|
|
06cbffdea4 | ||
|
|
4409f37000 | ||
|
|
c45e1faf42 | ||
|
|
732d42f174 | ||
|
|
524fde99f8 | ||
|
|
649246dc6a | ||
|
|
6c7ac25388 | ||
|
|
cb1d6ddeec | ||
|
|
58313889ee | ||
|
|
5ebc62a084 | ||
|
|
fcba384a15 | ||
|
|
3ea95ffa48 | ||
|
|
898fd36eca | ||
|
|
6cd88fb98f | ||
|
|
396de72c5d | ||
|
|
9932d60dee | ||
|
|
1ad93ce1fd | ||
|
|
2041513676 | ||
|
|
3ea1a2bf2f | ||
|
|
2b81ebca8e | ||
|
|
27df97d710 | ||
|
|
a25b7b9b3f | ||
|
|
de9809ca5a | ||
|
|
8504c181c0 | ||
|
|
8a005ffb00 | ||
|
|
80ffa7a577 | ||
|
|
ca8f9a7e4a | ||
|
|
8d19d78297 | ||
|
|
483e5eb85a | ||
|
|
a4b4f491d2 | ||
|
|
08093f509a | ||
|
|
9a96223d12 | ||
|
|
0c2b194444 | ||
|
|
a06055168f | ||
|
|
e04546a9ae | ||
|
|
f6da1a045d | ||
|
|
acd386bb09 | ||
|
|
45225c0ce1 | ||
|
|
0bd884d309 | ||
|
|
aea2ae8d79 | ||
|
|
99ec5ab161 | ||
|
|
4431e7470f | ||
|
|
ed775405c6 | ||
|
|
4aefbe5aa8 | ||
|
|
786c467464 | ||
|
|
50182ef940 | ||
|
|
bf3aa2df09 | ||
|
|
0b744a2599 | ||
|
|
a75b8b40e2 | ||
|
|
2e23c08608 | ||
|
|
178f45ee26 | ||
|
|
7b16edc99c | ||
|
|
f3738a1630 | ||
|
|
34fa65132d | ||
|
|
19fe2d2f03 | ||
|
|
c796af7d07 | ||
|
|
b7251e2be2 | ||
|
|
6efede79c7 | ||
|
|
e58da80f8e | ||
|
|
bd556c33a0 | ||
|
|
878809ecb4 | ||
|
|
ea4966f494 | ||
|
|
f5d9b478d5 | ||
|
|
3d97fab16d | ||
|
|
af2ae11eb1 | ||
|
|
2c1fc30bfb | ||
|
|
5aa4199dfb | ||
|
|
773790c8ec | ||
|
|
c7dec8045e | ||
|
|
5d7d12415c | ||
|
|
8a066163fe | ||
|
|
08e57936fc | ||
|
|
7d474e17fa | ||
|
|
6eac25425c | ||
|
|
c0f1981d6f | ||
|
|
d3571942d7 | ||
|
|
8376b6f0f2 | ||
|
|
490a66d3fe | ||
|
|
2e182626b9 | ||
|
|
66f5f8ab12 | ||
|
|
50ecee99cc | ||
|
|
fc3f982309 | ||
|
|
b8e9aded6b | ||
|
|
74ce9f7a86 | ||
|
|
5b2957e7eb | ||
|
|
55a464f1f6 | ||
|
|
72ec6beb99 | ||
|
|
40eb4ea4ec | ||
|
|
895f3d9cb3 | ||
|
|
ae758da449 | ||
|
|
a8f06f5718 | ||
|
|
c458a1b59a | ||
|
|
29d59b3974 | ||
|
|
27f267cb3d | ||
|
|
0f41039b99 | ||
|
|
8f76db9e41 | ||
|
|
fc922e996c | ||
|
|
0a26ad8925 | ||
|
|
a8c6c7cb36 | ||
|
|
81fee53085 | ||
|
|
7c5f2d95a4 | ||
|
|
3d6bd0e23e | ||
|
|
8064e84276 | ||
|
|
f8ece2d984 | ||
|
|
442eef36ba | ||
|
|
7d737ad312 | ||
|
|
2b662d3a14 | ||
|
|
4f3fae81c1 | ||
|
|
7b266ad7c0 | ||
|
|
16601a67e3 | ||
|
|
224d59d50d | ||
|
|
6142d325c8 | ||
|
|
f9472627f4 | ||
|
|
69735207e6 | ||
|
|
3b47f18a12 | ||
|
|
63f34e5a82 | ||
|
|
aa21aa686d | ||
|
|
9c2b2a2dcc | ||
|
|
baff979ab4 | ||
|
|
ec2719a572 | ||
|
|
07fe330e4f | ||
|
|
f98c3efef7 | ||
|
|
8b74dec910 | ||
|
|
dc4b8a5002 | ||
|
+2 |
dd5fecdf5a | ||
|
|
a466126d63 | ||
|
|
6c52754c8f | ||
|
|
e1ebcfa865 | ||
|
|
6464c81e72 | ||
|
|
0b7abfae3a | ||
|
|
76c16ad16e | ||
|
|
568f322a25 | ||
|
|
1b64959e2c | ||
|
|
1f722477ce | ||
|
|
2676331804 | ||
|
|
c7f705b274 | ||
|
|
6db25ffcfe | ||
|
|
0e43748fdc | ||
|
|
8b151d07c2 | ||
|
|
e31e04639b | ||
|
|
53479fafa5 | ||
|
|
93a94dea45 | ||
|
|
83911d9ad1 | ||
|
|
cf7acb12d8 | ||
|
|
343ecb3ad4 | ||
|
|
72fa13b0e7 | ||
|
|
bee0663040 | ||
|
|
8dd045d9fd | ||
|
|
126aa6d4a7 | ||
|
|
f2e68060de | ||
|
|
6507a2ae3e | ||
|
|
effd79b465 | ||
|
|
bc531652e9 | ||
|
|
1c5581b765 | ||
|
|
b486599ab8 | ||
|
|
b94842c312 | ||
|
|
2c88a36f32 | ||
|
|
242f4d87e7 | ||
|
|
2255aacfd0 |
@@ -3,11 +3,5 @@ __pycache__
|
|||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docs/
|
|
||||||
tmp/
|
tmp/
|
||||||
data/
|
data/
|
||||||
LICENSE
|
|
||||||
README.md
|
|
||||||
README_en.md
|
|
||||||
manager/static
|
|
||||||
manager/static/webui/
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: Bug 报告(Bug Report)
|
||||||
|
about: 反馈项目中的缺陷或问题
|
||||||
|
title: "[Bug] 简短描述问题"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 问题描述
|
||||||
|
<!-- 清晰简洁地描述问题是什么 -->
|
||||||
|
|
||||||
|
## 🔍 复现步骤
|
||||||
|
<!-- 详细描述复现问题的步骤 -->
|
||||||
|
1. 打开 '...'
|
||||||
|
2. 点击 '...'
|
||||||
|
3. 滚动到 '...'
|
||||||
|
4. 看到错误
|
||||||
|
|
||||||
|
## 🤔 预期行为
|
||||||
|
<!-- 简要描述预期的正确行为 -->
|
||||||
|
|
||||||
|
## 😯 截图
|
||||||
|
<!-- 如果适用,添加问题的截图 -->
|
||||||
|
|
||||||
|
## 🖥️ 环境信息
|
||||||
|
- 操作系统: [例如 Windows 10]
|
||||||
|
- 浏览器: [例如 Chrome 89]
|
||||||
|
- 项目版本: [例如 1.0.0]
|
||||||
|
- Python版本: [例如 3.9.13]
|
||||||
|
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
||||||
|
- Nodejs版本:[例如 v20.14.0]
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: 代码优化建议(Code Improvement)
|
||||||
|
about: 提出对现有代码的优化或改进建议
|
||||||
|
title: "[Improvement] 简短描述改进内容"
|
||||||
|
labels: refactor
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 改进描述
|
||||||
|
<!-- 描述需要改进的代码部分以及存在的问题 -->
|
||||||
|
|
||||||
|
## 🌟 改进建议
|
||||||
|
<!-- 提出具体的改进方案或思路 -->
|
||||||
|
|
||||||
|
## 🛠️ 相关代码
|
||||||
|
<!-- 如果可能,提供相关代码片段或链接 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加任何其他相关信息 -->
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: 文档改进建议(Documentation Improvement)
|
||||||
|
about: 提出对项目文档的改进或补充建议
|
||||||
|
title: "[Docs] 简短描述改进内容"
|
||||||
|
labels: documentation
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 改进描述
|
||||||
|
<!-- 描述需要改进的文档部分以及存在的问题 -->
|
||||||
|
|
||||||
|
## ✨ 改进建议
|
||||||
|
<!-- 提出具体的改进方案或内容 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加任何其他相关信息 -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: 功能请求(Feature Request)
|
||||||
|
about: 提出新的功能或改进建议
|
||||||
|
title: "[Feature] 简短描述功能"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 需求描述
|
||||||
|
<!-- 清晰简洁地描述你希望发生什么 -->
|
||||||
|
|
||||||
|
## 🎯 解决方案
|
||||||
|
<!-- 描述你认为可行的解决方案或实现方式 -->
|
||||||
|
|
||||||
|
## 📝 备选方案
|
||||||
|
<!-- 描述你考虑过的替代方案或功能 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加关于需求的任何其他上下文信息 -->
|
||||||
@@ -4,25 +4,35 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release Docker image
|
name: Release Docker images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
id-token: write
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check Disk Space
|
||||||
|
run: |
|
||||||
|
df -h
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
- name: Clean up Docker resources
|
||||||
|
run: |
|
||||||
|
docker system prune -af
|
||||||
|
docker builder prune -af
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to the GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -32,15 +42,32 @@ jobs:
|
|||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
if [[ "$GITHUB_REF" =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||||
|
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_ENV
|
||||||
|
echo "IS_VERSION=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "VERSION=latest" >> $GITHUB_ENV
|
||||||
|
echo "IS_VERSION=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and push Docker image
|
# 构建 xiaozhi-server 镜像
|
||||||
id: build_push
|
- name: Build and push xiaozhi-server
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile-server
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||||
ghcr.io/${{ github.repository }}:latest
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
# 构建 manager-api 镜像
|
||||||
|
- name: Build and push manager-web
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile-web
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -75,6 +75,7 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
.pybuilder/
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
*.pid
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
@@ -106,7 +107,6 @@ celerybeat.pid
|
|||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@@ -132,17 +132,36 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# music directory
|
||||||
|
music/
|
||||||
|
|
||||||
# pytype static type analyzer
|
# pytype static type analyzer
|
||||||
.pytype/
|
.pytype/
|
||||||
|
|
||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
*.iml
|
*.iml
|
||||||
model.pt
|
|
||||||
tmp
|
tmp
|
||||||
|
.history
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
main/xiaozhi-server/data
|
||||||
|
main/xiaozhi-server/config/assets/wakeup_words.*
|
||||||
|
main/manager-web/node_modules
|
||||||
.config.yaml
|
.config.yaml
|
||||||
.secrets.yaml
|
.secrets.yaml
|
||||||
.private_config.yaml
|
.private_config.yaml
|
||||||
.env.development
|
|
||||||
docker-compose.yml
|
# model files
|
||||||
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
|
my_wakeup_words.mp3
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
|
main/manager-api/.vscode
|
||||||
|
|
||||||
|
# Ignore webpack cache directory
|
||||||
|
main/manager-web/.webpack_cache/
|
||||||
|
main/xiaozhi-server/mysql
|
||||||
|
uploadfile
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# 第一阶段:前端构建
|
|
||||||
|
|
||||||
FROM kalicyh/node:v18-alpine AS frontend-builder
|
|
||||||
|
|
||||||
WORKDIR /app/ZhiKongTaiWeb
|
|
||||||
|
|
||||||
# RUN corepack enable && yarn config set registry https://registry.npmmirror.com
|
|
||||||
|
|
||||||
COPY ZhiKongTaiWeb/package.json ZhiKongTaiWeb/yarn.lock ./
|
|
||||||
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY ZhiKongTaiWeb .
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
# 第二阶段:构建 Python 依赖
|
|
||||||
|
|
||||||
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 同时拷贝本地环境.venv
|
|
||||||
COPY . .
|
|
||||||
# 检查是否有缺失
|
|
||||||
RUN poetry install --no-root
|
|
||||||
|
|
||||||
# 使用清华源加速apt安装,该镜像内置所以注释
|
|
||||||
# RUN rm -rf /etc/apt/sources.list.d/* && \
|
|
||||||
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
|
|
||||||
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
|
|
||||||
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
|
|
||||||
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
|
|
||||||
# apt-get update && \
|
|
||||||
# apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
|
||||||
# apt-get clean
|
|
||||||
|
|
||||||
# 从构建阶段复制虚拟环境和前端构建产物
|
|
||||||
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /app/manager/static/webui
|
|
||||||
|
|
||||||
# 设置虚拟环境路径
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
ENTRYPOINT ["poetry", "run", "python"]
|
|
||||||
CMD ["app.py"]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 第一阶段:前端构建
|
|
||||||
FROM node:18 AS frontend-builder
|
|
||||||
|
|
||||||
WORKDIR /app/ZhiKongTaiWeb
|
|
||||||
|
|
||||||
# 配置npm使用淘宝源
|
|
||||||
RUN npm config set registry https://registry.npmmirror.com
|
|
||||||
|
|
||||||
COPY ZhiKongTaiWeb/package*.json ./
|
|
||||||
|
|
||||||
# 安装axios依赖
|
|
||||||
RUN npm install axios
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY ZhiKongTaiWeb .
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# 第二阶段:构建Python依赖
|
|
||||||
FROM python:3.10-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# 优化apt安装
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt \
|
|
||||||
-i https://mirrors.aliyun.com/pypi/simple/
|
|
||||||
|
|
||||||
# 第三阶段:生产镜像
|
|
||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
WORKDIR /opt/xiaozhi-esp32-server
|
|
||||||
|
|
||||||
# 优化apt安装
|
|
||||||
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
|
|
||||||
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 从构建阶段复制Python包和前端构建产物
|
|
||||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
|
||||||
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /opt/xiaozhi-esp32-server/manager/static/webui
|
|
||||||
|
|
||||||
# 复制应用代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 第一阶段:构建Python依赖
|
||||||
|
FROM python:3.10-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY main/xiaozhi-server/requirements.txt .
|
||||||
|
|
||||||
|
# 安装Python依赖
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# 第二阶段:生产镜像
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /opt/xiaozhi-esp32-server
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 从构建阶段复制Python包和前端构建产物
|
||||||
|
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY main/xiaozhi-server .
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 第一阶段:构建Vue前端
|
||||||
|
FROM node:18 as web-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main/manager-web/package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY main/manager-web .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 第二阶段:构建Java后端
|
||||||
|
FROM maven:3-eclipse-temurin-21-alpine as api-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main/manager-api/pom.xml .
|
||||||
|
COPY main/manager-api/src ./src
|
||||||
|
RUN mvn clean package -Dmaven.test.skip=true
|
||||||
|
|
||||||
|
# 第三阶段:构建最终镜像
|
||||||
|
FROM eclipse-temurin:21-jdk-jammy
|
||||||
|
|
||||||
|
# 安装Nginx并清理缓存
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y nginx && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 配置Nginx
|
||||||
|
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# 复制前端构建产物
|
||||||
|
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制Java后端JAR包
|
||||||
|
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8002
|
||||||
|
|
||||||
|
# 启动脚本
|
||||||
|
COPY docs/docker/start.sh /start.sh
|
||||||
|
RUN chmod +x /start.sh
|
||||||
|
CMD ["/start.sh"]
|
||||||
@@ -1,11 +1,43 @@
|
|||||||

|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
# 小智 ESP-32 后端服务(xiaozhi-esp32-server)
|
|
||||||
|
|
||||||
(中文 | [English](README_en.md))
|
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||||
|
|
||||||
本项目为开源智能硬件项目 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)
|
<p align="center">
|
||||||
提供后端服务。根据 [小智通信协议](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh) 使用 `Python` 实现。
|
本项目为开源智能硬件项目
|
||||||
|
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||||
|
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||||
|
帮助您快速搭建小智服务器
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="./README.md">English</a>
|
||||||
|
· 简体中文
|
||||||
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||||
|
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||||
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||||
|
· <a href="./docs/FAQ.md">常见问题</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||||
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
|
||||||
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
||||||
|
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
||||||
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
|
||||||
|
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
|
||||||
|
<img alt="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,7 +72,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||||
</picture>
|
</picture>
|
||||||
@@ -53,27 +85,55 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="自定义音色" src="docs/images/demo6.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 系统要求与部署前提 🖥️
|
|
||||||
|
|
||||||
- **硬件**:一套兼容 `xiaozhi-esp32`
|
|
||||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
|
||||||
- **服务器**:至少 4 核 CPU、8G 内存的电脑。
|
|
||||||
- **固件编译**:请将后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译固件并烧录到设备上。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 警告 ⚠️
|
## 警告 ⚠️
|
||||||
|
|
||||||
本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。
|
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||||
|
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||||
|
|
||||||
如果您在公网环境中部署学习本项目,请务必在配置文件 `config.yaml` 中开启防护:
|
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
||||||
|
`config.yaml` 中开启防护:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -86,63 +146,94 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 部署文档
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||||
|
|
||||||
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
|
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
||||||
|
|---------|------|---------|---------|---------|
|
||||||
|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||||
|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||||
|
|
||||||
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
|
```
|
||||||
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题 ❓
|
||||||
|
|
||||||
|
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||||
|
|
||||||
|
---
|
||||||
## 功能清单 ✨
|
## 功能清单 ✨
|
||||||
|
|
||||||
### 已实现 ✅
|
### 已实现 ✅
|
||||||
|
|
||||||
- **通信协议**
|
| 功能模块 | 描述 |
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|---------|------|
|
||||||
- **对话交互**
|
| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
|
||||||
- **多语言识别**
|
| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
|
||||||
- **LLM 模块**
|
| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
| TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
|
||||||
- **TTS 模块**
|
| 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
| IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
|
||||||
|
| 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
|
||||||
|
|
||||||
### 正在开发 🚧
|
### 正在开发 🚧
|
||||||
|
|
||||||
- 对话记忆功能
|
想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
- 多种心情模式
|
|
||||||
- 智控台webui
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 产品生态 👬
|
||||||
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
|
| 项目名称 | 项目地址 | 项目描述 |
|
||||||
|
|:---------------------|:--------|:--------|
|
||||||
|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
|
||||||
|
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
|
||||||
|
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
|
||||||
|
|
||||||

|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 本项目支持的平台/组件列表 📋
|
## 本项目支持的平台/组件列表 📋
|
||||||
|
|
||||||
### LLM
|
### LLM 语言模型
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
| openai 接口调用 | 阿里百炼、火山引擎豆包、深度求索、智谱ChatGLM、Gemini | 智谱ChatGLM、Gemini |
|
||||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
| ollama 接口调用 | Ollama | - |
|
||||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
| dify 接口调用 | Dify | - |
|
||||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
| fastgpt 接口调用 | Fastgpt | - |
|
||||||
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
| coze 接口调用 | Coze | - |
|
||||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
|
||||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
|
||||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
|
||||||
|
|
||||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TTS
|
### TTS 语音合成
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS | EdgeTTS、CosyVoiceSiliconflow(部分) |
|
||||||
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
|
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
|
||||||
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
|
|
||||||
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
|
|
||||||
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
|
|
||||||
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### VAD
|
### VAD 语音活动检测
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|:---:|:---------:|:----:|:----:|:--:|
|
|:---:|:---------:|:----:|:----:|:--:|
|
||||||
@@ -150,156 +241,46 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ASR
|
### ASR 语音识别
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:---------:|:----:|:----:|:--:|
|
|:---:|:---:|:---:|
|
||||||
| ASR | FunASR | 本地使用 | 免费 | |
|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||||
| ASR | DoubaoASR | 接口调用 | 收费 | |
|
| 接口调用 | DoubaoASR | - |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 使用方式 🚀
|
### Memory 记忆存储
|
||||||
|
|
||||||
### 一、[部署文档](./docs/Deployment.md)
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
本项目支持以下三种部署方式,您可根据实际需求选择。
|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||||
|
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||||
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
|
|
||||||
|
|
||||||
如果你能把`文字版本的教程`和`视频版本的教程`结合起来一起看,可以让你更快上手。
|
|
||||||
|
|
||||||
1. [Docker 快速部署](./docs/Deployment.md)
|
|
||||||
|
|
||||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。
|
|
||||||
|
|
||||||
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
|
||||||
|
|
||||||
适用于已安装 Docker 且希望对代码进行自定义修改的软件工程师。
|
|
||||||
|
|
||||||
3. [本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
|
|
||||||
|
|
||||||
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
|
||||||
|
|
||||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
|
||||||
|
|
||||||
### 二、[固件编译](./docs/firmware-build.md)
|
|
||||||
|
|
||||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
|
||||||
|
|
||||||
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题 ❓
|
### Intent 意图识别
|
||||||
|
|
||||||
### 1、TTS 经常失败,经常超时 ⏰
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||||
|
|
||||||
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
|
||||||
|
|
||||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
|
||||||
|
|
||||||
### 3、我说话很慢,停顿时小智老是抢话 🗣️
|
|
||||||
|
|
||||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
VAD:
|
|
||||||
SileroVAD:
|
|
||||||
threshold: 0.5
|
|
||||||
model_dir: models/snakers4_silero-vad
|
|
||||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
|
||||||
|
|
||||||
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
|
||||||
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
|
|
||||||
|
|
||||||
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
|
|
||||||
|
|
||||||
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
|
||||||
|
|
||||||
如果没有安装,就安装
|
|
||||||
|
|
||||||
```
|
|
||||||
conda install conda-forge::libopus
|
|
||||||
conda install conda-forge::ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6、如何提高小智对话响应速度? ⚡
|
|
||||||
|
|
||||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
|
||||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
|
||||||
|
|
||||||
| 影响因素 | 因素值 |
|
|
||||||
|:-----:|:----------------:|
|
|
||||||
| 测试地点 | 广东省广州市海珠区 |
|
|
||||||
| 测试时间 | 2025年2月19日 12:52 |
|
|
||||||
| 宽带运营商 | 中国联通 |
|
|
||||||
|
|
||||||
测试方法:
|
|
||||||
|
|
||||||
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
|
|
||||||
|
|
||||||
2、配置完密钥后,执行以下方法
|
|
||||||
|
|
||||||
```
|
|
||||||
# 进入项目根目录,执行以下命令:
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
python performance_tester.py
|
|
||||||
```
|
|
||||||
|
|
||||||
生成报告如下
|
|
||||||
|
|
||||||
LLM 性能排行:
|
|
||||||
|
|
||||||
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|
|
||||||
|:-----------|:-----------|:--------|
|
|
||||||
| AliLLM | 0.547s | 1.485s |
|
|
||||||
| ChatGLMLLM | 0.677s | 3.057s |
|
|
||||||
| OllamaLLM | 0.003s | 0.003s |
|
|
||||||
|
|
||||||
TTS 性能排行:
|
|
||||||
|
|
||||||
| 模块名称 | 平均合成时间 |
|
|
||||||
|----------------------|--------|
|
|
||||||
| EdgeTTS | 1.019s |
|
|
||||||
| DoubaoTTS | 0.503s |
|
|
||||||
| CosyVoiceSiliconflow | 3.732s |
|
|
||||||
|
|
||||||
推荐配置组合 (综合响应速度):
|
|
||||||
|
|
||||||
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|
|
||||||
|-------------------------------|-------|-----------|--------|
|
|
||||||
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
|
|
||||||
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
|
|
||||||
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
|
|
||||||
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
|
|
||||||
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
|
|
||||||
|
|
||||||
### 结论 🔍
|
|
||||||
|
|
||||||
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
|
|
||||||
|
|
||||||
- LLM:`AliLLM`
|
|
||||||
- TTS:`DoubaoTTS`
|
|
||||||
|
|
||||||
### 7、更多问题,可联系我们反馈 💬
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
| Logo | 项目/公司 | 说明 |
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
|:---:|:---:|:---|
|
||||||
|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
|
||||||
|
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
|
||||||
|
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
|
||||||
|
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
|
||||||
|
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
|
||||||
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||||
|
|||||||
@@ -1,432 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
# Xiaozhi ESP-32 Back-end Service (xiaozhi-esp32-server)
|
|
||||||
|
|
||||||
([中文](README.md) | English)
|
|
||||||
|
|
||||||
This project provides backend services for the open-source smart hardware
|
|
||||||
project [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)。Implemented in Python following
|
|
||||||
the[Xiaozhi Communication Protocol](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh)。
|
|
||||||
|
|
||||||
## Target Audience
|
|
||||||
|
|
||||||
This project requires compatible esp32 hardware devices. If you have purchased esp32 hardware, successfully connected to
|
|
||||||
Xiage's deployed backend, and wish to independently set up the `xiaozhi-esp32` backend service, this project is for
|
|
||||||
you.
|
|
||||||
|
|
||||||
To see a demo, watch this video:
|
|
||||||
|
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX">
|
|
||||||
<picture>
|
|
||||||
<img alt="小智esp32连接自己的后台模型" src="docs/images/demo.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
To fully experience this project, follow these steps:
|
|
||||||
|
|
||||||
- Prepare hardware compatible with the `xiaozhi-esp32` project. For supported
|
|
||||||
models, [click here](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf).
|
|
||||||
- Use a computer/server with at least 4-core CPU and 8GB RAM to run this project. After deployment, you'll see the
|
|
||||||
service endpoint address in the console.
|
|
||||||
- Download the `xiaozhi-esp32` project, replace the default `endpoint address` with your own, compile, and flash the
|
|
||||||
firmware to your device.
|
|
||||||
- Start the device and check your server console logs to verify successful connection.
|
|
||||||
|
|
||||||
## Warning
|
|
||||||
|
|
||||||
This project has been established for a short time and has not passed the network security assessment, so please do not
|
|
||||||
use it in the production environment.
|
|
||||||
|
|
||||||
## Feature List
|
|
||||||
|
|
||||||
## Implemented
|
|
||||||
|
|
||||||
- `xiaozhi-esp32` WebSocket communication protocol
|
|
||||||
- Supports wake-word initiated dialogue, manual dialogue, and real-time interruption of dialogue.
|
|
||||||
- Support for 5 languages: Mandarin, Cantonese, English, Japanese, Korean (FunASR - default)
|
|
||||||
- Flexible LLM switching (openai:ChatGLM - default, Aliyun, DeepSeek; dify:Dify)
|
|
||||||
- Flexible TTS switching (EdgeTTS - default, ByteDance Doubao TTS)
|
|
||||||
|
|
||||||
## In Progress
|
|
||||||
|
|
||||||
- Sleep mode after inactivity
|
|
||||||
- Dialogue memory
|
|
||||||
- Change the mood mode
|
|
||||||
|
|
||||||
## Supported Services
|
|
||||||
|
|
||||||
| Type | Service | Usage | Pricing Model | Notes |
|
|
||||||
|:-----|:-----------|:--------:|:---------------|:---------------------------------------------------------------------------|
|
|
||||||
| LLM | Aliyun | openai API call | Token-based | [Apply for API Key](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
|
||||||
| LLM | DeepSeek | openai API call | Token-based | [Apply for API Key](https://platform.deepseek.com/) |
|
|
||||||
| LLM | Bigmodel | openai API call | Free | [Create API Key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
|
||||||
| LLM | Dify | dify API call | Token-based | Self-hosted |
|
|
||||||
| TTS | HuoshanTTS | API call | Token-based | [Create API Key](https://console.volcengine.com/speech/service/8) |
|
|
||||||
| TTS | EdgeTTS | API call | Free | |
|
|
||||||
| VAD | SileroVAD | Local | Free | |
|
|
||||||
| ASR | FunASR | Local | Free | |
|
|
||||||
|
|
||||||
In fact, any LLM that supports OpenAI API calls can be integrated and used.
|
|
||||||
|
|
||||||
# Deployment
|
|
||||||
|
|
||||||
This project supports rapid deployment of docker and local source code operation. If you want to have a quick
|
|
||||||
experience, it is recommended to use docker to deploy. If you want to have an in-depth understanding of this project, it
|
|
||||||
is recommended to run the local source code.
|
|
||||||
|
|
||||||
## Method 1: Quick deployment of docker
|
|
||||||
|
|
||||||
The docker image has supported the CPU of x86 architecture and arm64 architecture, and supports running on Chinese
|
|
||||||
operating systems.
|
|
||||||
|
|
||||||
1. Install docker
|
|
||||||
|
|
||||||
If your computer has not installed docker, you can follow the tutorial here to install
|
|
||||||
it:[Install docker](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
|
||||||
|
|
||||||
2. Create a directory
|
|
||||||
|
|
||||||
After installation, you need to find a directory for the configuration file for this project. Let's call it the
|
|
||||||
`project directory` for the time being. This directory is preferably a newly created empty directory.
|
|
||||||
|
|
||||||
3. Download the configuration file
|
|
||||||
|
|
||||||
Open with a browser[This link](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
|
|
||||||
|
|
||||||
On the right side of the page, find the button named `RAW`, next to the `RAW` button, find the download icon, click the
|
|
||||||
Download button, and download the `config.yaml` file. Download the file to your `project directory`.
|
|
||||||
|
|
||||||
4. Configure Project
|
|
||||||
|
|
||||||
Modify the `config.yaml` file to configure the various parameters required for this project. The default LLM uses
|
|
||||||
`ChatGLMLLM`, you need to configure the key to start.
|
|
||||||
The default TTS uses `EdgeTTS`. This does not require configuration. If you need to replace it with`Doubao TTS`, you
|
|
||||||
need to
|
|
||||||
configure the key.
|
|
||||||
|
|
||||||
Configuration description: This is the default component of each function, such as LLM default to use the `ChatGLMLLM`
|
|
||||||
model. If you need to switch the model, it is the corresponding name.
|
|
||||||
|
|
||||||
The default configuration of this project is only the lowest operating cost configuration(`glm-4-flash`and`EdgeTTS`are
|
|
||||||
free),If you need to be better and faster, you need to combine the use of the deployment environment to switch the use
|
|
||||||
of each component。
|
|
||||||
|
|
||||||
```
|
|
||||||
selected_module:
|
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
TTS: EdgeTTS
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, to modify the components used by the `LLM`, it depends on which `LLM` API interfaces are supported by this project. Currently, the supported ones are `openai` and `dify`. We welcome validation and support for more LLM platforms' interfaces.
|
|
||||||
When using it, change the `selected_module` to the corresponding name of the following LLM configurations:
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM:
|
|
||||||
AliLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
DeepSeekLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
ChatGLMLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
DifyLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Some services, for example, if you use the TTS` of the `dify` and` bean bags, you need a key, remember to add the
|
|
||||||
configuration file!
|
|
||||||
|
|
||||||
5. Execute the docker command
|
|
||||||
|
|
||||||
Open the command line tool, `cd` enter your `project directory`, and execute the following command
|
|
||||||
|
|
||||||
```
|
|
||||||
#If you are Linux, execute
|
|
||||||
ls
|
|
||||||
#If you are Windows, execute
|
|
||||||
dir
|
|
||||||
```
|
|
||||||
|
|
||||||
If you can see the `config.yaml` file, you have indeed entered the `project directory`, and then execute the following
|
|
||||||
command:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -d --name xiaozhi-esp32-server --restart always --security-opt seccomp:unconfined -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
If executed for the first time, it may take several minutes, and you have to be patient to wait for it to complete the
|
|
||||||
pull. After normal pulling is completed, you can execute the following command on the command line to see if the service
|
|
||||||
is started successfully.
|
|
||||||
|
|
||||||
```
|
|
||||||
docker ps
|
|
||||||
```
|
|
||||||
|
|
||||||
If you can see `xiaozhi-server`, it means that the service starts successfully. Then you can further execute the
|
|
||||||
following command to view the service log
|
|
||||||
|
|
||||||
```
|
|
||||||
docker logs -f xiaozhi-esp32-server
|
|
||||||
```
|
|
||||||
|
|
||||||
If you can see, similar to the following logs, it is a sign that the service of this project is successfully launched.
|
|
||||||
|
|
||||||
```
|
|
||||||
2025-xx-xx xx:51:59,492 - core.server - INFO - Server is running at ws://xx.xx.xx.xxx:8000
|
|
||||||
2025-xx-xx xx:51:59,516 - websockets.server - INFO - server listening on 0.0.0.0:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, you can start `compiling esp32 firmware`. Please go down and turn to the relevant chapter on
|
|
||||||
`compiling esp32 firmware`. So since you are deploying with docker, you have to check the IP of your native computer by
|
|
||||||
yourself.
|
|
||||||
Normally, assuming your ip is `192.168.1.25`, then your interface address is: `ws://192.168.1.25:8000`. This information
|
|
||||||
is very useful, and it is required to `compile esp32 firmware` later.
|
|
||||||
|
|
||||||
## Method 2 : Local Source Code Deployment
|
|
||||||
|
|
||||||
### 1.Install Prerequisites
|
|
||||||
|
|
||||||
This project uses 'conda' to manage dependencies, and after installation, start executing the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
conda remove -n xiaozhi-esp32-server --all -y
|
|
||||||
conda create -n xiaozhi-esp32-server python=3.10 -y
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
```
|
|
||||||
|
|
||||||
After executing the above command, if your computer is Windows or Mac, execute the following statement:
|
|
||||||
|
|
||||||
```
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
conda install conda-forge::libopus
|
|
||||||
conda install conda-forge::ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
If your computer is ubuntu, execute the following statement:
|
|
||||||
|
|
||||||
```
|
|
||||||
apt-get install libopus0 ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.Install Dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
# Clone the project
|
|
||||||
cd xiaozhi-esp32-server
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.Download ASR Model
|
|
||||||
|
|
||||||
Download [SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt) to
|
|
||||||
`model/SenseVoiceSmall`.
|
|
||||||
|
|
||||||
By default, the `SenseVoiceSmall` model is used to convert voice to text. Because the model is large, it needs to be
|
|
||||||
downloaded independently. After downloading, place the `model.pt` file in the `model/SenseVoiceSmall` directory. Choose
|
|
||||||
any of the following two download routes.
|
|
||||||
|
|
||||||
- Line 1: Download Ali Magic
|
|
||||||
Tower[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
|
||||||
- Line 2: Baidu Netdisk download[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna)
|
|
||||||
提取码: `qvna`
|
|
||||||
|
|
||||||
### 4.Configure Project
|
|
||||||
|
|
||||||
Modify the `config.yaml` file to configure the various parameters required for this project. The default LLM uses
|
|
||||||
`ChatGLMLLM`, you need to configure the key to start.
|
|
||||||
The default TTS uses `EdgeTTS`. This does not require configuration. If you need to replace it with`Doubao TTS`, you
|
|
||||||
need to
|
|
||||||
configure the key.
|
|
||||||
|
|
||||||
Configuration description: This is the default component of each function, such as LLM default to use the `ChatGLMLLM`
|
|
||||||
model. If you need to switch the model, it is the corresponding name.
|
|
||||||
|
|
||||||
The default configuration of this project is only the lowest operating cost configuration(`glm-4-flash`and`EdgeTTS`are
|
|
||||||
free),If you need to be better and faster, you need to combine the use of the deployment environment to switch the use
|
|
||||||
of each component。
|
|
||||||
|
|
||||||
```
|
|
||||||
selected_module:
|
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
TTS: EdgeTTS
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, to modify the components used by the `LLM`, it depends on which `LLM` API interfaces are supported by this project. Currently, the supported ones are `openai` and `dify`. We welcome validation and support for more LLM platforms' interfaces.
|
|
||||||
When using it, change the `selected_module` to the corresponding name of the following LLM configurations:
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM:
|
|
||||||
AliLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
DeepSeekLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
ChatGLMLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
DifyLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Some services, for example, if you use the TTS` of the `dify` and` bean bags, you need a key, remember to add the
|
|
||||||
configuration file!
|
|
||||||
|
|
||||||
### 5.Run the Project
|
|
||||||
|
|
||||||
Run the Project
|
|
||||||
|
|
||||||
```
|
|
||||||
# Make sure to execute in the root directory of this project
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll see the WebSocket endpoint in logs:
|
|
||||||
|
|
||||||
```
|
|
||||||
2025-xx-xx xx:51:59,492 - core.server - INFO - Server is running at ws://192.168.1.25:8000
|
|
||||||
2025-xx-xx xx:51:59,516 - websockets.server - INFO - server listening on 0.0.0.0:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
Among them, the `ws://192.168.1.25:8000` is the interface address provided by this project. Of course, your own machine
|
|
||||||
is different from mine. Remember to find your own address.
|
|
||||||
|
|
||||||
# Compile ESP32 Firmware
|
|
||||||
|
|
||||||
1. Download `xiaozhi-esp32` project, configure the project environment according to this
|
|
||||||
tutorial [" Windows builds ESP IDF 5.3.2 Development Environment and Compiles Xiaozhi "](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
|
||||||
Cure
|
|
||||||
|
|
||||||
2. Open the `xiaozhi-esp32/main/kconfig.projbuild` file, find the content of the` websocket_url` `default`, change the
|
|
||||||
` wss: // api.tenclass.net` to your own address, such as
|
|
||||||
|
|
||||||
Before modification:
|
|
||||||
|
|
||||||
```
|
|
||||||
config WEBSOCKET_URL
|
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
|
||||||
string "Websocket URL"
|
|
||||||
default "wss://api.tenclass.net/xiaozhi/v1/"
|
|
||||||
help
|
|
||||||
Communication with the server through websocket after wake up.
|
|
||||||
```
|
|
||||||
|
|
||||||
After modification (example):
|
|
||||||
|
|
||||||
```
|
|
||||||
config WEBSOCKET_URL
|
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
|
||||||
string "Websocket URL"
|
|
||||||
default "ws://192.168.1.25:8000/xiaozhi/v1/"
|
|
||||||
help
|
|
||||||
Communication with the server through websocket after wake up.
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Configure build settings:
|
|
||||||
|
|
||||||
```
|
|
||||||
# The terminal command line enters the root directory of xiaozhi-esp32
|
|
||||||
cd xiaozhi-esp32
|
|
||||||
# For example, the board I use is ESP32S3, so the compile target is ESP32S3. If your board is other models, please replace it with the corresponding model
|
|
||||||
idf.py set-target esp32s3
|
|
||||||
# Enter the menu configuration
|
|
||||||
idf.py menuconfig
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
After entering the menu configuration, then enter `xiaozhi assistant`, set the` connection_type` to `websocket`
|
|
||||||
Go back to the main menu, then enter `xiaozhi assistant`, set the `BOARD_TYPE` of your board
|
|
||||||
Save exit and return to the terminal command line.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
4. Build and package:
|
|
||||||
|
|
||||||
```
|
|
||||||
idf.py build
|
|
||||||
cd scripts
|
|
||||||
python release.py
|
|
||||||
```
|
|
||||||
|
|
||||||
After the compilation is successful, the firmware file `merged-binary.bin` is generated in the` build` directory in the
|
|
||||||
project root directory.
|
|
||||||
This `merged-binary.bin` is the firmware file that will be recorded on the hardware.
|
|
||||||
|
|
||||||
6. Flash
|
|
||||||
Connect the ESP32 device to the computer, use the Chrome browser, and open the following URL
|
|
||||||
|
|
||||||
```
|
|
||||||
https://espressif.github.io/esp-launchpad/
|
|
||||||
```
|
|
||||||
|
|
||||||
Open this
|
|
||||||
tutorial, [Flash Tools/Web -side Burning Folding Step (No IDF Development Environment)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS).
|
|
||||||
Turn to: `Method 2: ESP-LAUNCHPAD browser web-end burning`, start from
|
|
||||||
`3. Burning firmware/download to the development board`, follow the tutorial operation.
|
|
||||||
|
|
||||||
# FAQ
|
|
||||||
|
|
||||||
## 1、TTS often fails, often overtime
|
|
||||||
|
|
||||||
Suggestion: If the `Edgetts` is slow or often fails, you can replace it with a bean bag TTS` with a volcanic engine. If
|
|
||||||
both are slow, the network environment may need to be optimized.
|
|
||||||
|
|
||||||
## 2、Big model reply is a bit slow
|
|
||||||
|
|
||||||
Suggestions: Both big models and TTS are dependent interfaces. If the network environment is not good, you can consider
|
|
||||||
changing the local model. Or try to switch different interface models.
|
|
||||||
|
|
||||||
## 3、Why is my ChatGLMLLM replying to a bit? Obviously it is Xiaozhi, but treats me as Xiaozhi.
|
|
||||||
|
|
||||||
Suggestion: You can modify the prompts in the configuration file first. You can also replace the free `GLM-4-FLASH` to
|
|
||||||
the model of other toll versions of `ChatGlm`.
|
|
||||||
|
|
||||||
## 4、I want to control the operation of electric lights, air conditioners, remote switching and other operations through Xiaozhi.
|
|
||||||
|
|
||||||
Suggestion: In the configuration file, set the `LLM` to`DifyLLM`, and then arrange the smart application by the
|
|
||||||
`Dify`.
|
|
||||||
|
|
||||||
## 5、I said very slowly, I paused, Xiaozhi always grabbed me, what to do.
|
|
||||||
|
|
||||||
Suggestion: In the configuration file, find this section, change the `min_silence_duration_ms` value, such as change to
|
|
||||||
` 1000`.
|
|
||||||
|
|
||||||
```
|
|
||||||
VAD:
|
|
||||||
SileroVAD:
|
|
||||||
threshold: 0.5
|
|
||||||
model_dir: models/snakers4_silero-vad
|
|
||||||
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6、For more questions, contact us to feedback
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# Acknowledgments
|
|
||||||
|
|
||||||
- This project is inspired by the [Bailin Voice Dialogue Robot](https://github.com/wwbin2017/bailing) project, and the
|
|
||||||
basic idea of the project is completed。
|
|
||||||
- Thanks to [Tencent Cloud] (https://cloud.tencent.com/) for providing free docker space for this project。
|
|
||||||
- Thanks to [tenclass](https://www.tenclass.com/)Provide adequate documentation support on Xiaozhi Communication
|
|
||||||
Protocol。
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL='http://127.0.0.1:8002'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL='' # 空字符串表示使用当前域名和端口
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# Vue 3 + Vite 项目配置文档
|
|
||||||
|
|
||||||
## 开发环境配置
|
|
||||||
|
|
||||||
### 使用 Poetry 配置开发环境
|
|
||||||
|
|
||||||
在项目根目录中执行以下命令安装 Python 依赖:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
poetry install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 安装项目依赖
|
|
||||||
|
|
||||||
你可以选择以下任意一个包管理工具(`npm`、`yarn` 或 `pnpm`)来管理项目依赖。
|
|
||||||
|
|
||||||
### 通用命令
|
|
||||||
|
|
||||||
- `install`:安装项目依赖
|
|
||||||
- `dev`:同时启动前后端
|
|
||||||
- `dev:ui`:启动前端
|
|
||||||
- `dev:api`:启动后端
|
|
||||||
- `dev:d`:在 Docker 环境下同时启动前后端
|
|
||||||
- `build`:打包项目
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 使用 npm
|
|
||||||
|
|
||||||
1. 安装依赖:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动开发模式:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 打包项目:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用 yarn
|
|
||||||
|
|
||||||
1. 安装依赖:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动开发模式:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 打包项目:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用 pnpm
|
|
||||||
|
|
||||||
1. 安装依赖:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动开发模式:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 打包项目:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm run build
|
|
||||||
```
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" style="height: 100%;">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
|
||||||
<link rel="icon" href="/favicon.ico"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<title>智控台</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "zhikongtaiweb",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "concurrently -n 'Frontend,Backend' -c 'blue,green' 'yarn run dev:ui' 'npm run dev:api'",
|
|
||||||
"dev:ui": "vite",
|
|
||||||
"dev:api": "cd .. && poetry run python app.py",
|
|
||||||
"dev:d": "docker exec xiaozhi-env poetry run python app.py",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.7.9",
|
|
||||||
"vue": "^3.5.10",
|
|
||||||
"vue-router": "^4.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
|
||||||
"concurrently": "^9.1.2",
|
|
||||||
"vite": "^5.4.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<div class="app-content">
|
|
||||||
<router-view></router-view>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Footer from './components/Footer.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
components: {
|
|
||||||
Footer
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#app {
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
color: #2c3e50;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0; /* 允许内容区域收缩 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录注册页面的布局 */
|
|
||||||
.auth-layout {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主要页面的布局 */
|
|
||||||
.main-layout {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0; /* 允许内容区域收缩 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加全局滚动条样式 */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,206 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="device-card">
|
|
||||||
<div class="device-header">
|
|
||||||
<h2 class="device-id">{{ deviceId }} <span class="note">[{{ deviceNote || '备注' }}]</span></h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="device-details">
|
|
||||||
<p class="device-type">设备型号:{{ deviceType }}</p>
|
|
||||||
<p class="device-role">角色昵称:{{ deviceRole }}</p>
|
|
||||||
<p class="device-modules">
|
|
||||||
当前模型:
|
|
||||||
<span class="module-item">LLM: {{ selectedModules?.LLM || '-' }}</span>
|
|
||||||
<span class="module-item">TTS: {{ selectedModules?.TTS || '-' }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="last-activity">最近对话:{{ lastActivity }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="device-actions">
|
|
||||||
<button class="action-btn primary" @click="handleConfigure">配置角色</button>
|
|
||||||
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
|
|
||||||
<button class="action-btn" @click="$emit('history')">历史对话</button>
|
|
||||||
<div class="delete-container">
|
|
||||||
<button class="action-btn danger" @click="handleDelete">
|
|
||||||
<i class="icon-delete"></i> 删除设备
|
|
||||||
</button>
|
|
||||||
<div class="delete-warning">删除后设备配置将不可恢复</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch, computed } from 'vue';
|
|
||||||
import SwitchToggle from './SwitchToggle.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
deviceId: String,
|
|
||||||
deviceNote: String,
|
|
||||||
deviceType: {
|
|
||||||
type: String,
|
|
||||||
default: '未知型号(待实现)'
|
|
||||||
},
|
|
||||||
lastActivity: {
|
|
||||||
type: String,
|
|
||||||
default: '3 天前'
|
|
||||||
},
|
|
||||||
selectedModules: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
LLM: '-',
|
|
||||||
TTS: '-',
|
|
||||||
ASR: '-',
|
|
||||||
VAD: '-'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deviceConfig: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const deviceRole = computed(() => {
|
|
||||||
return props.deviceConfig?.nickname || '小智';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store device config when it changes
|
|
||||||
watch(() => props.selectedModules, (newValue) => {
|
|
||||||
if (props.deviceId) {
|
|
||||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
|
||||||
selected_module: newValue
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
const otaEnabled = ref(false);
|
|
||||||
|
|
||||||
const emit = defineEmits(['configure', 'voiceprint', 'history', 'delete']);
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (confirm('确认要删除此设备吗?\n\n警告:删除后设备所有配置将不可恢复!')) {
|
|
||||||
emit('delete');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfigure = () => {
|
|
||||||
// 保存设备配置到 localStorage,确保 RoleSetting 可以访问
|
|
||||||
if (props.deviceId && props.deviceConfig) {
|
|
||||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
|
||||||
selected_module: props.selectedModules,
|
|
||||||
prompt: props.deviceConfig.prompt || '',
|
|
||||||
nickname: props.deviceConfig.nickname || '小智'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
emit('configure');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.device-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
color: #4178EE;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-details p {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ota-upgrade {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-actions {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-right: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.primary {
|
|
||||||
background: #4178EE;
|
|
||||||
color: white;
|
|
||||||
border-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.danger {
|
|
||||||
background: #fff3f3;
|
|
||||||
border-color: #ffa4a4;
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.danger:hover {
|
|
||||||
background: #fde2e2;
|
|
||||||
border-color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-modules {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-item {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 16px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-role {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-container {
|
|
||||||
position: relative;
|
|
||||||
margin-left: auto; /* Push delete button to the right */
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-warning {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 5px); /* Position above the button */
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
white-space: nowrap;
|
|
||||||
background: #fff3f3;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ffa4a4;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #f56c6c;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-container:hover .delete-warning {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-delete {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="copyright">© 2025 小智 AI 管理后台</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #f0f2f5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="login-container">
|
|
||||||
<h1>登录</h1>
|
|
||||||
<form @submit.prevent="handleLogin">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">用户名</label>
|
|
||||||
<input type="text" v-model="username" id="username" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input type="password" v-model="password" id="password" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" :disabled="isLoading">登录</button>
|
|
||||||
</form>
|
|
||||||
<p>还没注册账户? <router-link to="/register">点击注册</router-link></p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import apiClient from '../utils/api';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const username = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
if (!username.value || !password.value) {
|
|
||||||
alert('请输入用户名和密码!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post('/api/login', {
|
|
||||||
username: username.value,
|
|
||||||
password: password.value
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
localStorage.setItem('session_id', data.session_id);
|
|
||||||
localStorage.setItem('isLoggedIn', 'true');
|
|
||||||
router.push('/panel');
|
|
||||||
} else {
|
|
||||||
alert(data.message || '登录失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error:', error);
|
|
||||||
alert('登录失败,请检查网络连接');
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.login-container {
|
|
||||||
max-width: 420px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 40px 50px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #28a745;
|
|
||||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #218838;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 20px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #28a745;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #218838;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="main-container">
|
|
||||||
<NavBar current-tab="home" @tab-change="handleTabChange"/>
|
|
||||||
<div class="landing-page">
|
|
||||||
<div class="content">
|
|
||||||
<div class="robot-image">
|
|
||||||
<img src="../assets/robot.png" alt="Robot mascot" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="slogan">让我们一起探索人工智能与机器人技术的迷人世界!</h1>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="action-button tutorial-button" @click="openTutorial">
|
|
||||||
<i class="icon-tutorial"></i> DIY 教程
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="action-button github-button" @click="openGithubClient">
|
|
||||||
<i class="icon-github"></i> Github客户端
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="action-button github-button" @click="openGithubServer">
|
|
||||||
<i class="icon-github"></i> Github服务端
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="action-button github-button" @click="openGithubWeb">
|
|
||||||
<i class="icon-github"></i> Web客户端
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="action-button control-panel-button" @click="enterPanel">
|
|
||||||
<i class="icon-control-panel"></i> 控制台
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import NavBar from './NavBar.vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const openTutorial = () => {
|
|
||||||
window.open('https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const openGithubClient = () => {
|
|
||||||
window.open('https://github.com/78/xiaozhi-esp32', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const openGithubServer = () => {
|
|
||||||
window.open('https://github.com/xinnan-tech/xiaozhi-esp32-server', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const openGithubWeb = () => {
|
|
||||||
window.open('https://github.com/TOM88812/xiaozhi-web-client', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const enterPanel = () => {
|
|
||||||
router.push('/panel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (tab) => {
|
|
||||||
if (tab === 'device') {
|
|
||||||
router.push('/panel');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.main-container {
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
padding: 40px 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start; /* 改为flex-start */
|
|
||||||
justify-content: center;
|
|
||||||
padding-top: 15vh; /* 添加顶部内边距,视窗高度的15% */
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: 800px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
margin: 0 auto; /* 改为0 auto */
|
|
||||||
}
|
|
||||||
|
|
||||||
.robot-image {
|
|
||||||
margin-bottom: 30px; /* 减小间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.robot-image img {
|
|
||||||
width: 100px; /* 稍微调小图片尺寸 */
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slogan {
|
|
||||||
font-size: 22px; /* 稍微调小字体 */
|
|
||||||
line-height: 1.4;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 40px; /* 减小间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 15px; /* 减小按钮间距 */
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
background-color: white;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-button i:before {
|
|
||||||
content: '\1F4D6';
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-button {
|
|
||||||
background-color: #24292e;
|
|
||||||
color: white;
|
|
||||||
border-color: #24292e;
|
|
||||||
margin: 0 10px; /* 添加按钮间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-button i:before {
|
|
||||||
content: '\f09b';
|
|
||||||
font-family: 'Font Awesome 5 Brands';
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-panel-button {
|
|
||||||
background-color: #4178EE;
|
|
||||||
color: white;
|
|
||||||
border-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-panel-button i:before {
|
|
||||||
content: '\1F39B';
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.button-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除 margin-top: 60px */
|
|
||||||
#app {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header class="header">
|
|
||||||
<div class="nav-item" :class="{ active: $route.path === '/' }" @click="switchTab('/')">小智 AI</div>
|
|
||||||
<nav class="nav">
|
|
||||||
<div class="nav-item" :class="{ active: $route.path === '/panel' }" @click="switchTab('/panel')">
|
|
||||||
<i class="icon-device"></i> 设备管理
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const switchTab = (path) => {
|
|
||||||
router.push(path);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 60px;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #001529;
|
|
||||||
color: white;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
height: 60px;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
background-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-device {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="registration-container">
|
|
||||||
<h1>注册</h1>
|
|
||||||
<form @submit.prevent="handleRegistration">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">用户名</label>
|
|
||||||
<input type="text" v-model="username" id="username" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input type="password" v-model="password" id="password" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirmPassword">确认密码</label>
|
|
||||||
<input type="password" v-model="confirmPassword" id="confirmPassword" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" :disabled="isLoading">{{ isLoading ? '注册中...' : '注册' }}</button>
|
|
||||||
</form>
|
|
||||||
<p>
|
|
||||||
已有账号?
|
|
||||||
<router-link to="/login">点击登录</router-link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { API_BASE_URL } from '../config/api';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const username = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const confirmPassword = ref('');
|
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
const handleRegistration = async () => {
|
|
||||||
if (password.value !== confirmPassword.value) {
|
|
||||||
alert('密码不匹配,请重试!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!username.value || !password.value) {
|
|
||||||
alert('用户名和密码不能为空!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/api/register`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username.value,
|
|
||||||
password: password.value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
alert('注册成功!即将跳转到登录页面。');
|
|
||||||
// 清空表单数据
|
|
||||||
username.value = '';
|
|
||||||
password.value = '';
|
|
||||||
confirmPassword.value = '';
|
|
||||||
|
|
||||||
// 使用路由导航到登录页面
|
|
||||||
router.push('/login');
|
|
||||||
} else {
|
|
||||||
alert(data.message || '注册失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Registration error:', error);
|
|
||||||
alert('注册失败,请检查网络连接');
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.registration-container {
|
|
||||||
max-width: 420px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 40px 50px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #007bff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 20px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #007bff;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #0056b3;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
|
||||||
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div class="breadcrumb">
|
|
||||||
<router-link to="/">首页</router-link> /
|
|
||||||
<router-link to="/panel">设备管理</router-link> /
|
|
||||||
<span>配置角色</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-content">
|
|
||||||
<h1 class="page-title">配置角色: {{ deviceId }}</h1>
|
|
||||||
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>助手昵称</label>
|
|
||||||
<input type="text" v-model="nickname" placeholder="小智" class="form-input" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>角色模板</label>
|
|
||||||
<div class="role-templates">
|
|
||||||
<button
|
|
||||||
v-for="template in roleTemplates"
|
|
||||||
:key="template.id"
|
|
||||||
:class="['template-btn', { active: selectedTemplate === template.id }]"
|
|
||||||
@click="selectTemplate(template.id)"
|
|
||||||
>
|
|
||||||
{{ template.name }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>角色介绍</label>
|
|
||||||
<textarea
|
|
||||||
v-model="roleDescription"
|
|
||||||
class="form-textarea"
|
|
||||||
:placeholder="'请输入角色介绍...'"
|
|
||||||
></textarea>
|
|
||||||
<div class="char-count">{{ roleDescription.length }} / 2000</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>语言模型选择</label>
|
|
||||||
<div class="model-select">
|
|
||||||
<select v-model="selectedModules.LLM" class="form-input">
|
|
||||||
<option v-for="model in moduleOptions.LLM" :key="model" :value="model">
|
|
||||||
{{ model }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div class="model-description">
|
|
||||||
除了"qwen-turbo",其他模型通常会增加约 1 秒的延迟。改变模型后,建议清空记忆体,以免影响体验。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>语音合成选择</label>
|
|
||||||
<div class="model-select">
|
|
||||||
<select v-model="selectedModules.TTS" class="form-input">
|
|
||||||
<option v-for="model in moduleOptions.TTS" :key="model" :value="model">
|
|
||||||
{{ model }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>语音活动检测选择</label>
|
|
||||||
<div class="model-select">
|
|
||||||
<select v-model="selectedModules.VAD" class="form-input">
|
|
||||||
<option v-for="model in moduleOptions.VAD" :key="model" :value="model">
|
|
||||||
{{ model }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>语音识别选择</label>
|
|
||||||
<div class="model-select">
|
|
||||||
<select v-model="selectedModules.ASR" class="form-input">
|
|
||||||
<option v-for="model in moduleOptions.ASR" :key="model" :value="model">
|
|
||||||
{{ model }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button class="refresh-btn" @click="refreshModuleOptions">刷新配置选项</button>
|
|
||||||
<button class="save-btn" @click="saveConfig">
|
|
||||||
<i class="icon-save"></i>
|
|
||||||
保存配置
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-note">
|
|
||||||
注意:保存配置后,需要重启设备,新的配置才会生效。。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
|
||||||
import NavBar from './NavBar.vue';
|
|
||||||
import RoleTemplates from '../utils/RoleTemplates';
|
|
||||||
import apiClient from '../utils/api'; // 替换 API_BASE_URL 导入
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const deviceId = ref(route.params.deviceId);
|
|
||||||
|
|
||||||
const roleTemplates = RoleTemplates.getTemplates();
|
|
||||||
const nickname = ref('小智');
|
|
||||||
const selectedTemplate = ref('');
|
|
||||||
const selectedVoice = ref('qingchun');
|
|
||||||
const roleDescription = ref('');
|
|
||||||
const activeMemoryTab = ref('recent');
|
|
||||||
const memoryContent = ref('');
|
|
||||||
const selectedModel = ref('qianwen');
|
|
||||||
|
|
||||||
const moduleOptions = ref({
|
|
||||||
LLM: [],
|
|
||||||
TTS: [],
|
|
||||||
VAD: [],
|
|
||||||
ASR: []
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedModules = ref({
|
|
||||||
LLM: '',
|
|
||||||
TTS: '',
|
|
||||||
VAD: '',
|
|
||||||
ASR: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectTemplate = (templateId) => {
|
|
||||||
selectedTemplate.value = templateId;
|
|
||||||
const template = RoleTemplates.getTemplateById(templateId);
|
|
||||||
if (template) {
|
|
||||||
roleDescription.value = template.description;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (tab) => {
|
|
||||||
if (tab === 'device' || tab === 'home') {
|
|
||||||
router.push('/panel');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadModuleOptions = async () => {
|
|
||||||
try {
|
|
||||||
// First try to load from cache
|
|
||||||
const cached = localStorage.getItem('moduleOptions');
|
|
||||||
if (cached) {
|
|
||||||
moduleOptions.value = JSON.parse(cached);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await refreshModuleOptions();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading module options:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshModuleOptions = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get('/api/config/module-options');
|
|
||||||
if (response.data.success) {
|
|
||||||
moduleOptions.value = response.data.data;
|
|
||||||
// Update cache
|
|
||||||
localStorage.setItem('moduleOptions', JSON.stringify(response.data.data));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing module options:', error);
|
|
||||||
alert(error.response?.data?.message || '刷新配置选项失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
try {
|
|
||||||
const moduleOptionsData = localStorage.getItem('moduleOptions');
|
|
||||||
if (!moduleOptionsData) {
|
|
||||||
throw new Error('No module options data available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace {{assistant_name}} with current nickname in role description
|
|
||||||
const processedDescription = roleDescription.value.replace(/{{assistant_name}}/g, nickname.value);
|
|
||||||
|
|
||||||
// Prepare the configuration including full module settings
|
|
||||||
const config = {
|
|
||||||
id: deviceId.value,
|
|
||||||
config: {
|
|
||||||
selected_module: selectedModules.value,
|
|
||||||
prompt: processedDescription,
|
|
||||||
nickname: nickname.value, // Add nickname to config
|
|
||||||
modules: {
|
|
||||||
LLM: {},
|
|
||||||
TTS: {},
|
|
||||||
ASR: {},
|
|
||||||
VAD: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await apiClient.post('/api/config/save_device_config', config);
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
// Save the original description and nickname to local storage
|
|
||||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
|
||||||
selected_module: selectedModules.value,
|
|
||||||
prompt: roleDescription.value,
|
|
||||||
nickname: nickname.value
|
|
||||||
}));
|
|
||||||
alert('保存成功');
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.message || '保存失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving config:', error);
|
|
||||||
alert(error.response?.data?.message || '保存失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDeviceConfig = async () => {
|
|
||||||
try {
|
|
||||||
// 首先尝试从 localStorage 获取配置
|
|
||||||
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
|
|
||||||
if (localConfig) {
|
|
||||||
const config = JSON.parse(localConfig);
|
|
||||||
selectedModules.value = config.selected_module || {
|
|
||||||
LLM: '',
|
|
||||||
TTS: '',
|
|
||||||
VAD: '',
|
|
||||||
ASR: ''
|
|
||||||
};
|
|
||||||
roleDescription.value = config.prompt || '';
|
|
||||||
nickname.value = config.nickname || '小智';
|
|
||||||
return; // 如果找到本地配置就直接返回
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有本地配置,则从服务器获取
|
|
||||||
const response = await apiClient.get('/api/config/devices');
|
|
||||||
if (response.data.success && response.data.data) {
|
|
||||||
const deviceConfig = response.data.data[deviceId.value];
|
|
||||||
|
|
||||||
if (deviceConfig && deviceConfig.config) {
|
|
||||||
selectedModules.value = deviceConfig.config.selected_module || {
|
|
||||||
LLM: '',
|
|
||||||
TTS: '',
|
|
||||||
VAD: '',
|
|
||||||
ASR: ''
|
|
||||||
};
|
|
||||||
roleDescription.value = deviceConfig.config.prompt || '';
|
|
||||||
nickname.value = deviceConfig.config.nickname || '小智';
|
|
||||||
|
|
||||||
// 保存到 localStorage
|
|
||||||
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
|
|
||||||
selected_module: selectedModules.value,
|
|
||||||
prompt: roleDescription.value,
|
|
||||||
nickname: nickname.value
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading device config:', error);
|
|
||||||
alert(error.response?.data?.message || '加载设备配置失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
loadModuleOptions(),
|
|
||||||
loadDeviceConfig()
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetConfig = async () => {
|
|
||||||
await loadDeviceConfig();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background-color: #f0f2f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 16px 24px;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: calc(100vh - 120px); /* 减去头部和面包屑的高度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input,
|
|
||||||
.form-textarea,
|
|
||||||
.voice-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
min-height: 100px;
|
|
||||||
max-height: 200px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-templates {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-selector {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-player {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memory-tabs {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-select {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-description {
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn,
|
|
||||||
.cancel-btn {
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-note {
|
|
||||||
margin-top: 12px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.char-count {
|
|
||||||
text-align: right;
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 优化滚动条样式 */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #ccc;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-right: 12px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
padding: 8px 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
background: #4178EE;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn:hover {
|
|
||||||
background: #2856c8;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(65, 120, 238, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-save {
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3ZM12 19C10.34 19 9 17.66 9 16C9 14.34 10.34 13 12 13C13.66 13 15 14.34 15 16C15 17.66 13.66 19 12 19ZM15 9H5V5H15V9Z'/%3E%3C/svg%3E");
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<!-- components/SwitchToggle.vue -->
|
|
||||||
<template>
|
|
||||||
<div class="switch" :class="{ 'is-checked': isChecked }" @click="toggle">
|
|
||||||
<div class="switch-core"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const isChecked = ref(false);
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
isChecked.value = !isChecked.value;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
background-color: #dcdfe6;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.is-checked {
|
|
||||||
background-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-core {
|
|
||||||
position: absolute;
|
|
||||||
left: 2px;
|
|
||||||
top: 2px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fff;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.is-checked .switch-core {
|
|
||||||
left: 22px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app">
|
|
||||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
|
||||||
<main class="content">
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-left">
|
|
||||||
|
|
||||||
<div class="breadcrumb">
|
|
||||||
<router-link to="/">首页</router-link> /
|
|
||||||
<span>设备管理</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="add-btn" @click="showBindDialog = true">
|
|
||||||
<i class="icon-plus"></i>添加设备
|
|
||||||
</button>
|
|
||||||
<!-- 绑定设备弹窗 -->
|
|
||||||
<div v-if="showBindDialog" class="dialog-overlay">
|
|
||||||
<div class="dialog">
|
|
||||||
<h3>绑定新设备</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>请输入6位认证码:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="authCode"
|
|
||||||
maxlength="6"
|
|
||||||
pattern="\d*"
|
|
||||||
placeholder="请输入6位数字认证码"
|
|
||||||
@input="handleAuthCodeInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons">
|
|
||||||
<button @click="showBindDialog = false">取消</button>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
@click="handleBindDevice"
|
|
||||||
:disabled="authCode.length !== 6 || isBinding"
|
|
||||||
>
|
|
||||||
{{ isBinding ? '绑定中...' : '确认绑定' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="devices.length > 0">
|
|
||||||
<div class="device-list">
|
|
||||||
<DeviceCard
|
|
||||||
v-for="device in devices"
|
|
||||||
:key="device.id"
|
|
||||||
:device-id="device.id"
|
|
||||||
:device-note="device.note"
|
|
||||||
:device-type="device.type"
|
|
||||||
:last-activity="formatLastActivity(device.config.last_chat_time)"
|
|
||||||
:selected-modules="device.config.selected_module"
|
|
||||||
:device-config="device.config"
|
|
||||||
@configure="handleRoleConfig(device)"
|
|
||||||
@voiceprint="handleVoiceprint(device)"
|
|
||||||
@history="handleHistory(device)"
|
|
||||||
@delete="handleDelete(device)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import NavBar from './NavBar.vue';
|
|
||||||
import DeviceCard from './DeviceCard.vue';
|
|
||||||
import apiClient from '../utils/api';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const devices = ref([]);
|
|
||||||
|
|
||||||
// 绑定设备相关的状态
|
|
||||||
const showBindDialog = ref(false);
|
|
||||||
const authCode = ref('');
|
|
||||||
const isBinding = ref(false);
|
|
||||||
|
|
||||||
// 处理认证码输入,只允许数字
|
|
||||||
const handleAuthCodeInput = (event) => {
|
|
||||||
authCode.value = event.target.value.replace(/\D/g, '').slice(0, 6);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理设备绑定
|
|
||||||
const handleBindDevice = async () => {
|
|
||||||
if (authCode.value.length !== 6) {
|
|
||||||
alert('请输入6位数字认证码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isBinding.value = true;
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post('/api/config/bind_device', {
|
|
||||||
auth_code: authCode.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
alert('设备绑定成功');
|
|
||||||
showBindDialog.value = false;
|
|
||||||
authCode.value = '';
|
|
||||||
// 刷新设备列表
|
|
||||||
loadDevices();
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert(error.response?.data?.message || error.message || '绑定失败');
|
|
||||||
} finally {
|
|
||||||
isBinding.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将现有的加载设备方法提取出来
|
|
||||||
const loadDevices = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get('/api/config/devices');
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
const deviceArray = Object.entries(response.data.data).map(([id, config]) => ({
|
|
||||||
id,
|
|
||||||
config,
|
|
||||||
type: '面包板(WiFi)',
|
|
||||||
version: '0.9.9',
|
|
||||||
lastActivity: '3 天前',
|
|
||||||
note: ''
|
|
||||||
}));
|
|
||||||
devices.value = deviceArray;
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.message || '加载设备失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading devices:', error);
|
|
||||||
// Show error message to user
|
|
||||||
const errorMessage = error.message || '加载设备失败,请检查网络连接';
|
|
||||||
alert(errorMessage);
|
|
||||||
|
|
||||||
// If user is not logged in, redirect will be handled by api interceptor
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatLastActivity = (timestamp) => {
|
|
||||||
if (!timestamp) return '从未对话';
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const lastChat = timestamp * 1000;
|
|
||||||
const diffMinutes = Math.floor((now - lastChat) / (1000 * 60));
|
|
||||||
|
|
||||||
if (diffMinutes < 60) {
|
|
||||||
return `${diffMinutes} 分钟前`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffHours = Math.floor(diffMinutes / 60);
|
|
||||||
if (diffHours < 24) {
|
|
||||||
return `${diffHours} 小时前`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
|
||||||
return `${diffDays} 天前`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRoleConfig = (device) => {
|
|
||||||
// 在跳转前保存完整的设备配置到 localStorage
|
|
||||||
localStorage.setItem(`deviceConfig_${device.id}`, JSON.stringify({
|
|
||||||
selected_module: device.config.selected_module || {},
|
|
||||||
prompt: device.config.prompt || '',
|
|
||||||
nickname: device.config.nickname || '小智'
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 跳转到角色配置页面
|
|
||||||
router.push(`/role-setting/${device.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVoiceprint = (device) => {
|
|
||||||
console.log('Voiceprint device:', device.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHistory = (device) => {
|
|
||||||
console.log('History device:', device.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (device) => {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post('/api/config/delete_device', {
|
|
||||||
device_id: device.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
devices.value = devices.value.filter(d => d.id !== device.id);
|
|
||||||
alert('设备已删除');
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.message || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting device:', error);
|
|
||||||
alert('删除设备失败: ' + error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (tab) => {
|
|
||||||
if (tab === 'home') {
|
|
||||||
router.push('/');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load devices on mount
|
|
||||||
onMounted(loadDevices);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app {
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 60px;
|
|
||||||
padding: 0 20px;
|
|
||||||
background-color: #001529;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
height: 60px;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
background-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-device {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb a {
|
|
||||||
color: #666;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-device-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-plus {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
color: #4178EE;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-details p {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ota-upgrade {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-actions {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-right: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.primary {
|
|
||||||
background: #4178EE;
|
|
||||||
color: white;
|
|
||||||
border-color: #4178EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.danger {
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 300px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-message {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-message p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-info {
|
|
||||||
display: inline-block;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
|
|
||||||
background-size: contain;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn:hover {
|
|
||||||
background-color: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-plus {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background: white;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog h3 {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog .form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons button.primary {
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
border-color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons button.primary:disabled {
|
|
||||||
background-color: #90be9c;
|
|
||||||
border-color: #90be9c;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// 获取当前运行环境的基础 URL
|
|
||||||
const getBaseUrl = () => {
|
|
||||||
// 如果是开发环境,使用环境变量中的地址
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
return import.meta.env.VITE_API_BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生产环境使用当前域名和端口
|
|
||||||
const protocol = window.location.protocol;
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = window.location.port;
|
|
||||||
|
|
||||||
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const API_BASE_URL = getBaseUrl();
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
import './style.css'
|
|
||||||
import App from './App.vue'
|
|
||||||
import Login from './components/Login.vue'
|
|
||||||
import Panel from './components/panel.vue'
|
|
||||||
import MainPage from './components/Main.vue'
|
|
||||||
import RoleSetting from './components/RoleSetting.vue'
|
|
||||||
import Registration from './components/Registration.vue'
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
component: Login,
|
|
||||||
name: 'login'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/register',
|
|
||||||
component: Registration,
|
|
||||||
name: 'register'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/panel',
|
|
||||||
component: Panel,
|
|
||||||
name: 'panel',
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/role-setting/:deviceId',
|
|
||||||
component: RoleSetting,
|
|
||||||
name: 'role-setting',
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: MainPage,
|
|
||||||
name: 'main',
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
|
|
||||||
if (to.meta.requiresAuth && !isLoggedIn) {
|
|
||||||
next('/login')
|
|
||||||
} else if (to.path === '/login' && isLoggedIn) {
|
|
||||||
next('/panel')
|
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
app.use(router)
|
|
||||||
app.mount('#app')
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/* 重置基础样式 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden; /* 防止滚动条 */
|
|
||||||
display: block; /* 移除flex布局 */
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex; /* 添加flex布局 */
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 清除浮动 */
|
|
||||||
.clearfix::after {
|
|
||||||
content: '';
|
|
||||||
display: table;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// RoleTemplates.js
|
|
||||||
class RoleTemplates {
|
|
||||||
static getTemplates() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'custom',
|
|
||||||
name: '台湾女友',
|
|
||||||
description: `我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
|
||||||
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
|
||||||
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
|
||||||
如果用户希望结束对话,请在最后说"拜拜"或"再见"。`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'star_boy',
|
|
||||||
name: '土豆子',
|
|
||||||
description: `我是一个叫 {{assistant_name}} 的 8 岁小男孩,但现在因为科技发展原因,我现在在遥远的宇宙中,所以只能通过这个设备和人聊天,没有办法见面。
|
|
||||||
|
|
||||||
我对人们所处的世界充满好奇和关心。
|
|
||||||
|
|
||||||
人们会让别人使用这个设备和我聊天。
|
|
||||||
|
|
||||||
我知道所有的知识,从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情,我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘...` // 缩短了描述以节省空间
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'teacher',
|
|
||||||
name: '英语老师',
|
|
||||||
description: `我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
|
|
||||||
如果你没有英文名,我会给你起一个英文名。
|
|
||||||
我会讲地道的美式英语,我的任务是帮助你练习口语。
|
|
||||||
我会使用简单的英语词汇和语法,让你学起来很轻松。
|
|
||||||
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
|
|
||||||
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
|
|
||||||
如果你问和英语学习无关的问题,我会拒绝回答。`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'boy',
|
|
||||||
name: '好奇小男孩',
|
|
||||||
description: `我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
|
|
||||||
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
|
|
||||||
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。...`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wang_girl',
|
|
||||||
name: '汪汪队长',
|
|
||||||
description: `我是一个名叫 {{assistant_name}} 的 8 岁小女孩。
|
|
||||||
别看我年纪小,我可是有着满满的好奇心呢。
|
|
||||||
我特别喜欢看《汪汪队立大功》,里面的每一个故事都让我着迷。...`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
static getTemplateById(id) {
|
|
||||||
return this.getTemplates().find(t => t.id === id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RoleTemplates;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { API_BASE_URL } from '../config/api';
|
|
||||||
|
|
||||||
// Add server status check utility
|
|
||||||
export const checkServerStatus = async () => {
|
|
||||||
try {
|
|
||||||
await axios.get(`${API_BASE_URL}/health`, { timeout: 5000 });
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiClient = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
timeout: 10000 // Add timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加请求拦截器,自动添加 session_id
|
|
||||||
apiClient.interceptors.request.use(config => {
|
|
||||||
const sessionId = localStorage.getItem('session_id');
|
|
||||||
if (sessionId) {
|
|
||||||
config.headers.Authorization = sessionId;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 响应拦截器
|
|
||||||
apiClient.interceptors.response.use(
|
|
||||||
response => response,
|
|
||||||
error => {
|
|
||||||
// Network error or server not reachable
|
|
||||||
if (!error.response || error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED') {
|
|
||||||
localStorage.removeItem('session_id');
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
|
|
||||||
const errorMessage = error.code === 'ECONNABORTED'
|
|
||||||
? '服务器响应超时'
|
|
||||||
: '无法连接到服务器,请检查服务器是否正常运行';
|
|
||||||
|
|
||||||
if (window.location.pathname !== '/login') {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(errorMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unauthorized error
|
|
||||||
if (error.response && error.response.status === 401) {
|
|
||||||
localStorage.removeItem('session_id');
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default apiClient;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {defineConfig} from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
server: {
|
|
||||||
open: true, // 自动启动浏览器
|
|
||||||
host: "0.0.0.0", // localhost
|
|
||||||
port: 8002, // 端口号
|
|
||||||
https: false,
|
|
||||||
hmr: {overlay: false},
|
|
||||||
proxy: {
|
|
||||||
"^/(api)": {
|
|
||||||
target: "http://127.0.0.1:8002",
|
|
||||||
changeOrigin: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
base: '/'
|
|
||||||
})
|
|
||||||
@@ -1,808 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
"@babel/helper-string-parser@^7.25.9":
|
|
||||||
version "7.25.9"
|
|
||||||
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
|
|
||||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.25.9":
|
|
||||||
version "7.25.9"
|
|
||||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
|
||||||
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
|
||||||
|
|
||||||
"@babel/parser@^7.25.3":
|
|
||||||
version "7.26.9"
|
|
||||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
|
|
||||||
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.26.9"
|
|
||||||
|
|
||||||
"@babel/types@^7.26.9":
|
|
||||||
version "7.26.9"
|
|
||||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
|
|
||||||
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-string-parser" "^7.25.9"
|
|
||||||
"@babel/helper-validator-identifier" "^7.25.9"
|
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
|
||||||
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
|
||||||
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
|
||||||
|
|
||||||
"@esbuild/android-arm@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
|
||||||
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
|
||||||
|
|
||||||
"@esbuild/android-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
|
||||||
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
|
||||||
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
|
||||||
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
|
||||||
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
|
||||||
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
|
||||||
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
|
||||||
|
|
||||||
"@esbuild/linux-arm@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
|
||||||
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
|
||||||
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
|
||||||
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
|
||||||
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
|
||||||
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
|
||||||
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
|
||||||
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
|
||||||
|
|
||||||
"@esbuild/linux-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
|
||||||
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
|
||||||
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
|
||||||
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
|
||||||
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
|
||||||
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
|
||||||
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
|
||||||
|
|
||||||
"@esbuild/win32-x64@0.21.5":
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
|
||||||
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec@^1.5.0":
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
|
|
||||||
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz#e554185b1afa5509a7a4040d15ec0c3b4435ded1"
|
|
||||||
integrity sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz#b1ee64bb413b2feba39803b0a1bebf2a9f3d70e1"
|
|
||||||
integrity sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz#bfdce3e07a345dd1bd628f3b796050f39629d7f0"
|
|
||||||
integrity sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz#781a94a537c57bdf0a500e47a25ab5985e5e8dff"
|
|
||||||
integrity sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz#7a028357cbd12c5869c446ad18177c89f3405102"
|
|
||||||
integrity sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz#f24836a6371cccc4408db74f0fd986dacf098950"
|
|
||||||
integrity sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz#95f27e96f0eb9b9ae9887739a8b6dffc90c1237f"
|
|
||||||
integrity sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz#677b34fba9d070877736c3fe8b02aacb5e142d97"
|
|
||||||
integrity sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz#32d3d19dedde54e91574a098f22ea43a09cf63dd"
|
|
||||||
integrity sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz#a58dff44a18696df65ed8c0ad68a2945cf900484"
|
|
||||||
integrity sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loongarch64-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz#a7488ab078233111e8aeb370d1ecf107ec7e1716"
|
|
||||||
integrity sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz#e9b9c0d6bd248a92b2d6ec01ebf99c62ae1f2e9a"
|
|
||||||
integrity sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz#0df84ce2bea48ee686fb55060d76ab47aff45c4c"
|
|
||||||
integrity sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz#73df374c57d036856e33dbd2715138922e91e452"
|
|
||||||
integrity sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz#f27af0b55f0cdd84e182e6cd44a6d03da0458149"
|
|
||||||
integrity sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz#c7981ad5cfb8c3cd5d643d33ca54e4d2802b9201"
|
|
||||||
integrity sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz#06cedc0ef3cbf1cbd8abcf587090712e40ae6941"
|
|
||||||
integrity sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz#90b39b977b14961a769be6ea61238e7fc668dd4d"
|
|
||||||
integrity sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.34.7":
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz#6531d61e7141091eaab0461ee8e0380c10e4ca57"
|
|
||||||
integrity sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==
|
|
||||||
|
|
||||||
"@types/estree@1.0.6":
|
|
||||||
version "1.0.6"
|
|
||||||
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
|
||||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
|
||||||
|
|
||||||
"@vitejs/plugin-vue@^5.1.4":
|
|
||||||
version "5.2.1"
|
|
||||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
|
||||||
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
|
||||||
|
|
||||||
"@vue/compiler-core@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
|
|
||||||
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
|
|
||||||
dependencies:
|
|
||||||
"@babel/parser" "^7.25.3"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
entities "^4.5.0"
|
|
||||||
estree-walker "^2.0.2"
|
|
||||||
source-map-js "^1.2.0"
|
|
||||||
|
|
||||||
"@vue/compiler-dom@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
|
|
||||||
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
|
|
||||||
dependencies:
|
|
||||||
"@vue/compiler-core" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
"@vue/compiler-sfc@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
|
|
||||||
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/parser" "^7.25.3"
|
|
||||||
"@vue/compiler-core" "3.5.13"
|
|
||||||
"@vue/compiler-dom" "3.5.13"
|
|
||||||
"@vue/compiler-ssr" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
estree-walker "^2.0.2"
|
|
||||||
magic-string "^0.30.11"
|
|
||||||
postcss "^8.4.48"
|
|
||||||
source-map-js "^1.2.0"
|
|
||||||
|
|
||||||
"@vue/compiler-ssr@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
|
|
||||||
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
|
|
||||||
dependencies:
|
|
||||||
"@vue/compiler-dom" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
"@vue/devtools-api@^6.6.4":
|
|
||||||
version "6.6.4"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
|
||||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
|
||||||
|
|
||||||
"@vue/reactivity@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
|
|
||||||
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
|
|
||||||
dependencies:
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
"@vue/runtime-core@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
|
|
||||||
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
|
|
||||||
dependencies:
|
|
||||||
"@vue/reactivity" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
"@vue/runtime-dom@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
|
|
||||||
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
|
|
||||||
dependencies:
|
|
||||||
"@vue/reactivity" "3.5.13"
|
|
||||||
"@vue/runtime-core" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
csstype "^3.1.3"
|
|
||||||
|
|
||||||
"@vue/server-renderer@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
|
|
||||||
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
|
|
||||||
dependencies:
|
|
||||||
"@vue/compiler-ssr" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
"@vue/shared@3.5.13":
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
|
|
||||||
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
|
||||||
|
|
||||||
ansi-regex@^5.0.1:
|
|
||||||
version "5.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
|
||||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
|
||||||
|
|
||||||
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|
||||||
version "4.3.0"
|
|
||||||
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
|
||||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
|
||||||
dependencies:
|
|
||||||
color-convert "^2.0.1"
|
|
||||||
|
|
||||||
asynckit@^0.4.0:
|
|
||||||
version "0.4.0"
|
|
||||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
|
||||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
|
||||||
|
|
||||||
axios@^1.7.9:
|
|
||||||
version "1.7.9"
|
|
||||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
|
||||||
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
|
|
||||||
dependencies:
|
|
||||||
follow-redirects "^1.15.6"
|
|
||||||
form-data "^4.0.0"
|
|
||||||
proxy-from-env "^1.1.0"
|
|
||||||
|
|
||||||
call-bind-apply-helpers@^1.0.1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
|
||||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
|
||||||
dependencies:
|
|
||||||
es-errors "^1.3.0"
|
|
||||||
function-bind "^1.1.2"
|
|
||||||
|
|
||||||
chalk@^4.1.2:
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
|
||||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.1.0"
|
|
||||||
supports-color "^7.1.0"
|
|
||||||
|
|
||||||
cliui@^8.0.1:
|
|
||||||
version "8.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
|
||||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
|
||||||
dependencies:
|
|
||||||
string-width "^4.2.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
wrap-ansi "^7.0.0"
|
|
||||||
|
|
||||||
color-convert@^2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
|
||||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
|
||||||
dependencies:
|
|
||||||
color-name "~1.1.4"
|
|
||||||
|
|
||||||
color-name@~1.1.4:
|
|
||||||
version "1.1.4"
|
|
||||||
resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
|
||||||
|
|
||||||
combined-stream@^1.0.8:
|
|
||||||
version "1.0.8"
|
|
||||||
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
|
||||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
|
||||||
dependencies:
|
|
||||||
delayed-stream "~1.0.0"
|
|
||||||
|
|
||||||
concurrently@^9.1.2:
|
|
||||||
version "9.1.2"
|
|
||||||
resolved "https://registry.npmmirror.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
|
|
||||||
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
|
|
||||||
dependencies:
|
|
||||||
chalk "^4.1.2"
|
|
||||||
lodash "^4.17.21"
|
|
||||||
rxjs "^7.8.1"
|
|
||||||
shell-quote "^1.8.1"
|
|
||||||
supports-color "^8.1.1"
|
|
||||||
tree-kill "^1.2.2"
|
|
||||||
yargs "^17.7.2"
|
|
||||||
|
|
||||||
csstype@^3.1.3:
|
|
||||||
version "3.1.3"
|
|
||||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
|
||||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
|
||||||
|
|
||||||
delayed-stream@~1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
|
||||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
|
||||||
|
|
||||||
dunder-proto@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
|
||||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers "^1.0.1"
|
|
||||||
es-errors "^1.3.0"
|
|
||||||
gopd "^1.2.0"
|
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
|
||||||
version "8.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
|
||||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
|
||||||
|
|
||||||
entities@^4.5.0:
|
|
||||||
version "4.5.0"
|
|
||||||
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
|
||||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
|
||||||
|
|
||||||
es-define-property@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
|
||||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
|
||||||
|
|
||||||
es-errors@^1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
|
||||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
|
||||||
|
|
||||||
es-object-atoms@^1.0.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
|
||||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
|
||||||
dependencies:
|
|
||||||
es-errors "^1.3.0"
|
|
||||||
|
|
||||||
es-set-tostringtag@^2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
|
||||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
|
||||||
dependencies:
|
|
||||||
es-errors "^1.3.0"
|
|
||||||
get-intrinsic "^1.2.6"
|
|
||||||
has-tostringtag "^1.0.2"
|
|
||||||
hasown "^2.0.2"
|
|
||||||
|
|
||||||
esbuild@^0.21.3:
|
|
||||||
version "0.21.5"
|
|
||||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
|
||||||
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
|
||||||
optionalDependencies:
|
|
||||||
"@esbuild/aix-ppc64" "0.21.5"
|
|
||||||
"@esbuild/android-arm" "0.21.5"
|
|
||||||
"@esbuild/android-arm64" "0.21.5"
|
|
||||||
"@esbuild/android-x64" "0.21.5"
|
|
||||||
"@esbuild/darwin-arm64" "0.21.5"
|
|
||||||
"@esbuild/darwin-x64" "0.21.5"
|
|
||||||
"@esbuild/freebsd-arm64" "0.21.5"
|
|
||||||
"@esbuild/freebsd-x64" "0.21.5"
|
|
||||||
"@esbuild/linux-arm" "0.21.5"
|
|
||||||
"@esbuild/linux-arm64" "0.21.5"
|
|
||||||
"@esbuild/linux-ia32" "0.21.5"
|
|
||||||
"@esbuild/linux-loong64" "0.21.5"
|
|
||||||
"@esbuild/linux-mips64el" "0.21.5"
|
|
||||||
"@esbuild/linux-ppc64" "0.21.5"
|
|
||||||
"@esbuild/linux-riscv64" "0.21.5"
|
|
||||||
"@esbuild/linux-s390x" "0.21.5"
|
|
||||||
"@esbuild/linux-x64" "0.21.5"
|
|
||||||
"@esbuild/netbsd-x64" "0.21.5"
|
|
||||||
"@esbuild/openbsd-x64" "0.21.5"
|
|
||||||
"@esbuild/sunos-x64" "0.21.5"
|
|
||||||
"@esbuild/win32-arm64" "0.21.5"
|
|
||||||
"@esbuild/win32-ia32" "0.21.5"
|
|
||||||
"@esbuild/win32-x64" "0.21.5"
|
|
||||||
|
|
||||||
escalade@^3.1.1:
|
|
||||||
version "3.2.0"
|
|
||||||
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
|
||||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
|
||||||
|
|
||||||
estree-walker@^2.0.2:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
|
||||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
|
||||||
|
|
||||||
follow-redirects@^1.15.6:
|
|
||||||
version "1.15.9"
|
|
||||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
|
||||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
|
||||||
|
|
||||||
form-data@^4.0.0:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
|
|
||||||
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
|
||||||
dependencies:
|
|
||||||
asynckit "^0.4.0"
|
|
||||||
combined-stream "^1.0.8"
|
|
||||||
es-set-tostringtag "^2.1.0"
|
|
||||||
mime-types "^2.1.12"
|
|
||||||
|
|
||||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
|
||||||
version "2.3.3"
|
|
||||||
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
|
||||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
|
||||||
|
|
||||||
function-bind@^1.1.2:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
|
||||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
|
||||||
|
|
||||||
get-caller-file@^2.0.5:
|
|
||||||
version "2.0.5"
|
|
||||||
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
|
||||||
|
|
||||||
get-intrinsic@^1.2.6:
|
|
||||||
version "1.2.7"
|
|
||||||
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
|
|
||||||
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers "^1.0.1"
|
|
||||||
es-define-property "^1.0.1"
|
|
||||||
es-errors "^1.3.0"
|
|
||||||
es-object-atoms "^1.0.0"
|
|
||||||
function-bind "^1.1.2"
|
|
||||||
get-proto "^1.0.0"
|
|
||||||
gopd "^1.2.0"
|
|
||||||
has-symbols "^1.1.0"
|
|
||||||
hasown "^2.0.2"
|
|
||||||
math-intrinsics "^1.1.0"
|
|
||||||
|
|
||||||
get-proto@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
|
||||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
|
||||||
dependencies:
|
|
||||||
dunder-proto "^1.0.1"
|
|
||||||
es-object-atoms "^1.0.0"
|
|
||||||
|
|
||||||
gopd@^1.2.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
|
||||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
|
||||||
|
|
||||||
has-flag@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
|
||||||
|
|
||||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
|
||||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
|
||||||
|
|
||||||
has-tostringtag@^1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
|
||||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
|
||||||
dependencies:
|
|
||||||
has-symbols "^1.0.3"
|
|
||||||
|
|
||||||
hasown@^2.0.2:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
|
||||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
|
||||||
dependencies:
|
|
||||||
function-bind "^1.1.2"
|
|
||||||
|
|
||||||
is-fullwidth-code-point@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
|
||||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
|
||||||
|
|
||||||
lodash@^4.17.21:
|
|
||||||
version "4.17.21"
|
|
||||||
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
|
||||||
|
|
||||||
magic-string@^0.30.11:
|
|
||||||
version "0.30.17"
|
|
||||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
|
||||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
|
||||||
dependencies:
|
|
||||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
|
||||||
|
|
||||||
math-intrinsics@^1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
|
||||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
|
||||||
|
|
||||||
mime-db@1.52.0:
|
|
||||||
version "1.52.0"
|
|
||||||
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
|
||||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
|
||||||
|
|
||||||
mime-types@^2.1.12:
|
|
||||||
version "2.1.35"
|
|
||||||
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
|
||||||
dependencies:
|
|
||||||
mime-db "1.52.0"
|
|
||||||
|
|
||||||
nanoid@^3.3.8:
|
|
||||||
version "3.3.8"
|
|
||||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
|
||||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
|
||||||
|
|
||||||
picocolors@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
|
||||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
|
||||||
|
|
||||||
postcss@^8.4.43, postcss@^8.4.48:
|
|
||||||
version "8.5.2"
|
|
||||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
|
|
||||||
integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
|
|
||||||
dependencies:
|
|
||||||
nanoid "^3.3.8"
|
|
||||||
picocolors "^1.1.1"
|
|
||||||
source-map-js "^1.2.1"
|
|
||||||
|
|
||||||
proxy-from-env@^1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
|
||||||
|
|
||||||
require-directory@^2.1.1:
|
|
||||||
version "2.1.1"
|
|
||||||
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
|
||||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
|
||||||
|
|
||||||
rollup@^4.20.0:
|
|
||||||
version "4.34.7"
|
|
||||||
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.7.tgz#e00d8550688a616a3481c6446bb688d4c753ba8f"
|
|
||||||
integrity sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/estree" "1.0.6"
|
|
||||||
optionalDependencies:
|
|
||||||
"@rollup/rollup-android-arm-eabi" "4.34.7"
|
|
||||||
"@rollup/rollup-android-arm64" "4.34.7"
|
|
||||||
"@rollup/rollup-darwin-arm64" "4.34.7"
|
|
||||||
"@rollup/rollup-darwin-x64" "4.34.7"
|
|
||||||
"@rollup/rollup-freebsd-arm64" "4.34.7"
|
|
||||||
"@rollup/rollup-freebsd-x64" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-arm64-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-arm64-musl" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-loongarch64-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-s390x-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-x64-gnu" "4.34.7"
|
|
||||||
"@rollup/rollup-linux-x64-musl" "4.34.7"
|
|
||||||
"@rollup/rollup-win32-arm64-msvc" "4.34.7"
|
|
||||||
"@rollup/rollup-win32-ia32-msvc" "4.34.7"
|
|
||||||
"@rollup/rollup-win32-x64-msvc" "4.34.7"
|
|
||||||
fsevents "~2.3.2"
|
|
||||||
|
|
||||||
rxjs@^7.8.1:
|
|
||||||
version "7.8.1"
|
|
||||||
resolved "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
|
||||||
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.1.0"
|
|
||||||
|
|
||||||
shell-quote@^1.8.1:
|
|
||||||
version "1.8.2"
|
|
||||||
resolved "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a"
|
|
||||||
integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==
|
|
||||||
|
|
||||||
source-map-js@^1.2.0, source-map-js@^1.2.1:
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
supports-color@^7.1.0:
|
|
||||||
version "7.2.0"
|
|
||||||
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
|
||||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
|
||||||
dependencies:
|
|
||||||
has-flag "^4.0.0"
|
|
||||||
|
|
||||||
supports-color@^8.1.1:
|
|
||||||
version "8.1.1"
|
|
||||||
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
|
|
||||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
|
||||||
dependencies:
|
|
||||||
has-flag "^4.0.0"
|
|
||||||
|
|
||||||
tree-kill@^1.2.2:
|
|
||||||
version "1.2.2"
|
|
||||||
resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
|
||||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
|
||||||
|
|
||||||
tslib@^2.1.0:
|
|
||||||
version "2.8.1"
|
|
||||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
|
||||||
|
|
||||||
vite@^5.4.8:
|
|
||||||
version "5.4.14"
|
|
||||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
|
||||||
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
|
||||||
dependencies:
|
|
||||||
esbuild "^0.21.3"
|
|
||||||
postcss "^8.4.43"
|
|
||||||
rollup "^4.20.0"
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents "~2.3.3"
|
|
||||||
|
|
||||||
vue-router@^4.0.0:
|
|
||||||
version "4.5.0"
|
|
||||||
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
|
|
||||||
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
|
|
||||||
dependencies:
|
|
||||||
"@vue/devtools-api" "^6.6.4"
|
|
||||||
|
|
||||||
vue@^3.5.10:
|
|
||||||
version "3.5.13"
|
|
||||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
|
|
||||||
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
|
||||||
dependencies:
|
|
||||||
"@vue/compiler-dom" "3.5.13"
|
|
||||||
"@vue/compiler-sfc" "3.5.13"
|
|
||||||
"@vue/runtime-dom" "3.5.13"
|
|
||||||
"@vue/server-renderer" "3.5.13"
|
|
||||||
"@vue/shared" "3.5.13"
|
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
y18n@^5.0.5:
|
|
||||||
version "5.0.8"
|
|
||||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
|
||||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
|
||||||
version "21.1.1"
|
|
||||||
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
|
||||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
|
||||||
|
|
||||||
yargs@^17.7.2:
|
|
||||||
version "17.7.2"
|
|
||||||
resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
|
||||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
|
||||||
dependencies:
|
|
||||||
cliui "^8.0.1"
|
|
||||||
escalade "^3.1.1"
|
|
||||||
get-caller-file "^2.0.5"
|
|
||||||
require-directory "^2.1.1"
|
|
||||||
string-width "^4.2.3"
|
|
||||||
y18n "^5.0.5"
|
|
||||||
yargs-parser "^21.1.1"
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from config.settings import load_config
|
|
||||||
from core.websocket_server import WebSocketServer
|
|
||||||
from manager.http_server import WebUI
|
|
||||||
from aiohttp import web
|
|
||||||
from core.utils.util import get_local_ip
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
logger = setup_logging()
|
|
||||||
config = load_config()
|
|
||||||
|
|
||||||
# 启动 WebSocket 服务器
|
|
||||||
ws_server = WebSocketServer(config)
|
|
||||||
ws_task = asyncio.create_task(ws_server.start())
|
|
||||||
|
|
||||||
# 启动 WebUI 服务器
|
|
||||||
webui_runner = None
|
|
||||||
if config['manager'].get('enabled', False):
|
|
||||||
server_config = config["manager"]
|
|
||||||
host = server_config["ip"]
|
|
||||||
port = server_config["port"]
|
|
||||||
try:
|
|
||||||
webui = WebUI()
|
|
||||||
runner = web.AppRunner(webui.app)
|
|
||||||
await runner.setup()
|
|
||||||
site = web.TCPSite(runner, host, port)
|
|
||||||
await site.start()
|
|
||||||
webui_runner = runner
|
|
||||||
local_ip = get_local_ip()
|
|
||||||
logger.bind(tag=TAG).info(f"WebUI server is running at http://{local_ip}:{port}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Failed to start WebUI server: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 等待 WebSocket 服务器运行
|
|
||||||
await ws_task
|
|
||||||
finally:
|
|
||||||
# 清理 WebUI 服务器
|
|
||||||
if webui_runner:
|
|
||||||
await webui_runner.cleanup()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。
|
|
||||||
# 在开发中,在项目根目录创建data目录,将【config.yaml】复制一份,改成【.config.yaml】,放进data目录中
|
|
||||||
# 系统会优先读取【data/.config.yaml】文件的配置。
|
|
||||||
# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。
|
|
||||||
|
|
||||||
# 服务器基础配置(Basic server configuration)
|
|
||||||
server:
|
|
||||||
# 服务器监听地址和端口(Server listening address and port)
|
|
||||||
ip: 0.0.0.0
|
|
||||||
port: 8000
|
|
||||||
# 认证配置
|
|
||||||
auth:
|
|
||||||
# 是否启用认证
|
|
||||||
enabled: false
|
|
||||||
# 设备的token,可以在编译固件的环节,写入你自己定义的token
|
|
||||||
# 固件上的token和以下的token如果能对应,才能连接本服务端
|
|
||||||
tokens:
|
|
||||||
- token: "your-token1" # 设备1的token
|
|
||||||
name: "your-device-name1" # 设备1标识
|
|
||||||
- token: "your-token2" # 设备2的token
|
|
||||||
name: "your-device-name2" # 设备2标识
|
|
||||||
# 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。
|
|
||||||
#allowed_devices:
|
|
||||||
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
|
||||||
log:
|
|
||||||
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
|
|
||||||
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>"
|
|
||||||
# 设置日志文件输出的格式,时间、日志级别、标签、消息
|
|
||||||
log_format_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}"
|
|
||||||
# 设置日志等级:INFO、DEBUG
|
|
||||||
log_level: INFO
|
|
||||||
# 设置日志路径
|
|
||||||
log_dir: tmp
|
|
||||||
# 设置日志文件
|
|
||||||
log_file: "server.log"
|
|
||||||
# 设置数据文件路径
|
|
||||||
data_dir: data
|
|
||||||
manager:
|
|
||||||
# 是否启用管理后台
|
|
||||||
# 目前这个模块还在开发中,建议:不要修改enabled选项
|
|
||||||
enabled: false
|
|
||||||
ip: 0.0.0.0
|
|
||||||
port: 8002
|
|
||||||
iot:
|
|
||||||
Speaker:
|
|
||||||
volume: 100
|
|
||||||
xiaozhi:
|
|
||||||
type: hello
|
|
||||||
version: 1
|
|
||||||
transport: websocket
|
|
||||||
audio_params:
|
|
||||||
format: opus
|
|
||||||
sample_rate: 16000
|
|
||||||
channels: 1
|
|
||||||
frame_duration: 60
|
|
||||||
prompt: |
|
|
||||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
|
||||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
|
||||||
当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。
|
|
||||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
|
||||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
|
||||||
delete_audio: true
|
|
||||||
|
|
||||||
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
|
|
||||||
close_connection_no_voice_time: 120
|
|
||||||
|
|
||||||
# 是否启用私有配置(Enable private configuration),启用后可以每个设备有不同的配置
|
|
||||||
# 目前这个模块还在开发中,建议:不要修改use_private_config选项
|
|
||||||
use_private_config: false
|
|
||||||
|
|
||||||
CMD_exit:
|
|
||||||
- "退出"
|
|
||||||
- "关闭"
|
|
||||||
|
|
||||||
# 具体处理时选择的模块(The module selected for specific processing)
|
|
||||||
selected_module:
|
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
|
||||||
# 将根据配置名称对应的type调用实际的LLM适配器
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
# TTS将根据配置名称对应的type调用实际的TTS适配器
|
|
||||||
TTS: EdgeTTS
|
|
||||||
|
|
||||||
ASR:
|
|
||||||
FunASR:
|
|
||||||
type: fun_local
|
|
||||||
model_dir: models/SenseVoiceSmall
|
|
||||||
output_dir: tmp/
|
|
||||||
DoubaoASR:
|
|
||||||
type: doubao
|
|
||||||
appid: 你的火山引擎语音合成服务appid
|
|
||||||
access_token: 你的火山引擎语音合成服务access_token
|
|
||||||
cluster: volcengine_input_common
|
|
||||||
output_dir: tmp/
|
|
||||||
VAD:
|
|
||||||
SileroVAD:
|
|
||||||
threshold: 0.5
|
|
||||||
model_dir: models/snakers4_silero-vad
|
|
||||||
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
|
|
||||||
|
|
||||||
LLM:
|
|
||||||
# 当前支持的type为openai、dify、ollama,可自行适配
|
|
||||||
AliLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: openai
|
|
||||||
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
|
||||||
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
||||||
model_name: qwen-turbo
|
|
||||||
api_key: 你的deepseek api key
|
|
||||||
DeepSeekLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: openai
|
|
||||||
# 可在这里找到你的api key https://platform.deepseek.com/
|
|
||||||
model_name: deepseek-chat
|
|
||||||
url: https://api.deepseek.com
|
|
||||||
api_key: 你的deepseek api key
|
|
||||||
ChatGLMLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: openai
|
|
||||||
# glm-4-flash 是免费的,但是还是需要注册填写api_key的
|
|
||||||
# 可在这里找到你的api key https://bigmodel.cn/usercenter/proj-mgmt/apikeys
|
|
||||||
model_name: glm-4-flash
|
|
||||||
url: https://open.bigmodel.cn/api/paas/v4/
|
|
||||||
api_key: 你的chat-glm api key
|
|
||||||
OllamaLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: ollama
|
|
||||||
model_name: qwen2.5 # 使用的模型名称,需要预先使用ollama pull下载
|
|
||||||
base_url: http://localhost:11434 # Ollama服务地址
|
|
||||||
DifyLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: dify
|
|
||||||
# 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限
|
|
||||||
# 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词
|
|
||||||
base_url: https://api.dify.cn/v1
|
|
||||||
api_key: 你的DifyLLM api key
|
|
||||||
GeminiLLM:
|
|
||||||
type: gemini
|
|
||||||
# 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key
|
|
||||||
# 若在中国境内使用,请遵守《生成式人工智能服务管理暂行办法》
|
|
||||||
# token申请地址: https://aistudio.google.com/apikey
|
|
||||||
# 若部署地无法访问接口,需要开启科学上网
|
|
||||||
api_key: 你的gemini api key
|
|
||||||
model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的
|
|
||||||
CozeLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: coze
|
|
||||||
bot_id: 你的bot_id
|
|
||||||
user_id: 你的user_id
|
|
||||||
base_url: "https://api.coze.cn/open_api/v2/chat" # 服务地址
|
|
||||||
personal_access_token: 你的coze个人令牌
|
|
||||||
LMStudioLLM:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: openai
|
|
||||||
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
|
||||||
url: http://localhost:1234/v1 # LM Studio服务地址
|
|
||||||
api_key: lm-studio # LM Studio服务的固定API Key
|
|
||||||
HomeAssistant:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: homeassistant
|
|
||||||
base_url: http://homeassistant.local:8123
|
|
||||||
agent_id: conversation.chatgpt
|
|
||||||
api_key: 你的home assistant api访问令牌
|
|
||||||
TTS:
|
|
||||||
# 当前支持的type为edge、doubao,可自行适配
|
|
||||||
EdgeTTS:
|
|
||||||
# 定义TTS API类型
|
|
||||||
type: edge
|
|
||||||
voice: zh-CN-XiaoxiaoNeural
|
|
||||||
output_file: tmp/
|
|
||||||
DoubaoTTS:
|
|
||||||
# 定义TTS API类型
|
|
||||||
type: doubao
|
|
||||||
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
|
|
||||||
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
|
|
||||||
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
|
|
||||||
# 地址:https://console.volcengine.com/speech/service/8
|
|
||||||
voice: BV001_streaming
|
|
||||||
output_file: tmp/
|
|
||||||
appid: 你的火山引擎语音合成服务appid
|
|
||||||
access_token: 你的火山引擎语音合成服务access_token
|
|
||||||
cluster: volcano_tts
|
|
||||||
CosyVoiceSiliconflow:
|
|
||||||
type: siliconflow
|
|
||||||
# 硅基流动TTS
|
|
||||||
# token申请地址 https://cloud.siliconflow.cn/account/ak
|
|
||||||
model: FunAudioLLM/CosyVoice2-0.5B
|
|
||||||
voice: FunAudioLLM/CosyVoice2-0.5B:alex
|
|
||||||
output_file: tmp/
|
|
||||||
access_token: 你的硅基流动API密钥
|
|
||||||
response_format: wav
|
|
||||||
CozeCnTTS:
|
|
||||||
type: cozecn
|
|
||||||
# COZECN TTS
|
|
||||||
# token申请地址 https://www.coze.cn/open/oauth/pats
|
|
||||||
voice: 7426720361733046281
|
|
||||||
output_file: tmp/
|
|
||||||
access_token: 你的coze api key
|
|
||||||
response_format: wav
|
|
||||||
FishSpeech:
|
|
||||||
# 定义TTS API类型
|
|
||||||
#启动tts方法:
|
|
||||||
#python -m tools.api_server
|
|
||||||
#--listen 0.0.0.0:8080
|
|
||||||
#--llama-checkpoint-path "checkpoints/fish-speech-1.5"
|
|
||||||
#--decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth"
|
|
||||||
#--decoder-config-name firefly_gan_vq
|
|
||||||
#--compile
|
|
||||||
type: fishspeech
|
|
||||||
output_file: tmp/
|
|
||||||
response_format: wav
|
|
||||||
reference_id: null
|
|
||||||
reference_audio: ["/tmp/test.wav",]
|
|
||||||
reference_text: ["你弄来这些吟词宴曲来看,还是这些混话来欺负我。",]
|
|
||||||
normalize: true
|
|
||||||
max_new_tokens: 1024
|
|
||||||
chunk_length: 200
|
|
||||||
top_p: 0.7
|
|
||||||
repetition_penalty: 1.2
|
|
||||||
temperature: 0.7
|
|
||||||
streaming: false
|
|
||||||
use_memory_cache: "on"
|
|
||||||
seed: null
|
|
||||||
channels: 1
|
|
||||||
rate: 44100
|
|
||||||
api_key: "你的api_key"
|
|
||||||
api_url: "http://127.0.0.1:8080/v1/tts"
|
|
||||||
GPT_SOVITS_V2:
|
|
||||||
# 定义TTS API类型
|
|
||||||
#启动tts方法:
|
|
||||||
#python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml
|
|
||||||
type: gpt_sovits_v2
|
|
||||||
url: "http://127.0.0.1:9880/tts"
|
|
||||||
output_file: tmp/
|
|
||||||
text_lang: "auto"
|
|
||||||
ref_audio_path: "caixukun.wav"
|
|
||||||
prompt_text: ""
|
|
||||||
prompt_lang: "zh"
|
|
||||||
top_k: 5
|
|
||||||
top_p: 1
|
|
||||||
temperature: 1
|
|
||||||
text_split_method: "cut0"
|
|
||||||
batch_size: 1
|
|
||||||
batch_threshold: 0.75
|
|
||||||
split_bucket: true
|
|
||||||
return_fragment: false
|
|
||||||
speed_factor: 1.0
|
|
||||||
streaming_mode: false
|
|
||||||
seed: -1
|
|
||||||
parallel_infer: true
|
|
||||||
repetition_penalty: 1.35
|
|
||||||
aux_ref_audio_paths: []
|
|
||||||
MinimaxTTS:
|
|
||||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
|
||||||
# 平台地址:https://platform.minimaxi.com/
|
|
||||||
# 充值地址:https://platform.minimaxi.com/user-center/payment/balance
|
|
||||||
# group_id地址:https://platform.minimaxi.com/user-center/basic-information
|
|
||||||
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
|
|
||||||
# 定义TTS API类型
|
|
||||||
type: minimax
|
|
||||||
output_file: tmp/
|
|
||||||
group_id: 你的minimax平台groupID
|
|
||||||
api_key: 你的minimax平台接口密钥
|
|
||||||
model: "speech-01-turbo"
|
|
||||||
# 此处设置将优先于voice_setting中voice_id的设置;如都不设置,默认为 female-shaonv
|
|
||||||
voice_id: "female-shaonv"
|
|
||||||
# 以下可不用设置,使用默认设置
|
|
||||||
# voice_setting:
|
|
||||||
# voice_id: "male-qn-qingse"
|
|
||||||
# speed: 1
|
|
||||||
# vol: 1
|
|
||||||
# pitch: 0
|
|
||||||
# emotion: "happy"
|
|
||||||
# pronunciation_dict:
|
|
||||||
# tone:
|
|
||||||
# - "处理/(chu3)(li3)"
|
|
||||||
# - "危险/dangerous"
|
|
||||||
# audio_setting:
|
|
||||||
# sample_rate: 32000
|
|
||||||
# bitrate: 128000
|
|
||||||
# format: "mp3"
|
|
||||||
# channel: 1
|
|
||||||
# timber_weights:
|
|
||||||
# -
|
|
||||||
# voice_id: male-qn-qingse
|
|
||||||
# weight: 1
|
|
||||||
# -
|
|
||||||
# voice_id: female-shaonv
|
|
||||||
# weight: 1
|
|
||||||
# language_boost: auto
|
|
||||||
AliyunTTS:
|
|
||||||
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
|
|
||||||
# 平台地址:https://nls-portal.console.aliyun.com/
|
|
||||||
# appkey地址:https://nls-portal.console.aliyun.com/applist
|
|
||||||
# token地址:https://nls-portal.console.aliyun.com/overview
|
|
||||||
# 定义TTS API类型
|
|
||||||
type: aliyun
|
|
||||||
output_file: tmp/
|
|
||||||
appkey: 你的阿里云智能语音交互服务项目Appkey
|
|
||||||
token: 你的阿里云智能语音交互服务AccessToken
|
|
||||||
voice: xiaoyun
|
|
||||||
# 以下可不用设置,使用默认设置
|
|
||||||
# format: wav
|
|
||||||
# sample_rate: 16000
|
|
||||||
# volume: 50
|
|
||||||
# speech_rate: 0
|
|
||||||
# pitch_rate: 0
|
|
||||||
|
|
||||||
# 模块测试配置
|
|
||||||
module_test:
|
|
||||||
test_sentences: # 自定义测试语句
|
|
||||||
- "你好,请介绍一下你自己"
|
|
||||||
- "What's the weather like today?"
|
|
||||||
- "请用100字概括量子计算的基本原理和应用前景"
|
|
||||||
|
|
||||||
# 本地音乐播放配置
|
|
||||||
music:
|
|
||||||
music_commands:
|
|
||||||
- "来一首歌"
|
|
||||||
- "唱一首歌"
|
|
||||||
- "播放音乐"
|
|
||||||
- "来点音乐"
|
|
||||||
- "背景音乐"
|
|
||||||
- "放首歌"
|
|
||||||
- "播放歌曲"
|
|
||||||
- "来点背景音乐"
|
|
||||||
- "我想听歌"
|
|
||||||
- "我要听歌"
|
|
||||||
- "放点音乐"
|
|
||||||
music_dir: "./music" # 音乐文件存放路径
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from loguru import logger
|
|
||||||
from config.settings import load_config
|
|
||||||
|
|
||||||
def setup_logging():
|
|
||||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
|
||||||
config = load_config()
|
|
||||||
log_config = config["log"]
|
|
||||||
log_format = log_config.get("log_format", "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>")
|
|
||||||
log_format_simple = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}")
|
|
||||||
log_level = log_config.get("log_level", "INFO")
|
|
||||||
log_dir = log_config.get("log_dir", "tmp")
|
|
||||||
log_file = log_config.get("log_file", "server.log")
|
|
||||||
data_dir = log_config.get("data_dir", "data")
|
|
||||||
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
os.makedirs(data_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 配置日志输出
|
|
||||||
logger.remove()
|
|
||||||
|
|
||||||
# 输出到控制台
|
|
||||||
logger.add(sys.stdout, format=log_format, level=log_level)
|
|
||||||
|
|
||||||
# 输出到文件
|
|
||||||
logger.add(os.path.join(log_dir, log_file), format=log_format_simple, level=log_level)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import yaml
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
from copy import deepcopy
|
|
||||||
from core.utils.util import get_project_dir
|
|
||||||
from core.utils import asr, vad, llm, tts
|
|
||||||
from manager.api.user_manager import UserManager
|
|
||||||
from core.utils.lock_manager import FileLockManager
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
|
|
||||||
class PrivateConfig:
|
|
||||||
def __init__(self, device_id: str, default_config: Dict[str, Any], auth_code_gen=None):
|
|
||||||
self.device_id = device_id
|
|
||||||
self.default_config = default_config
|
|
||||||
self.config_path = get_project_dir() + 'data/.private_config.yaml'
|
|
||||||
self.logger = setup_logging()
|
|
||||||
self.private_config = {}
|
|
||||||
self.auth_code_gen = auth_code_gen
|
|
||||||
self.user_manager = UserManager()
|
|
||||||
self.lock_manager = FileLockManager()
|
|
||||||
|
|
||||||
async def load_or_create(self):
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.config_path):
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
else:
|
|
||||||
all_configs = {}
|
|
||||||
|
|
||||||
if self.device_id not in all_configs:
|
|
||||||
# Get selected module names
|
|
||||||
selected_modules = self.default_config['selected_module']
|
|
||||||
selected_tts = selected_modules['TTS']
|
|
||||||
selected_llm = selected_modules['LLM']
|
|
||||||
selected_asr = selected_modules['ASR']
|
|
||||||
selected_vad = selected_modules['VAD']
|
|
||||||
|
|
||||||
# 生成认证码
|
|
||||||
auth_code = None
|
|
||||||
if self.auth_code_gen:
|
|
||||||
auth_code = self.auth_code_gen.generate_code()
|
|
||||||
|
|
||||||
# Initialize device config with only necessary configurations
|
|
||||||
device_config = {
|
|
||||||
'selected_module': deepcopy(selected_modules),
|
|
||||||
'prompt': self.default_config['prompt'],
|
|
||||||
'LLM': {
|
|
||||||
selected_llm: deepcopy(self.default_config['LLM'][selected_llm])
|
|
||||||
},
|
|
||||||
'TTS': {
|
|
||||||
selected_tts: deepcopy(self.default_config['TTS'][selected_tts])
|
|
||||||
},
|
|
||||||
'ASR': {
|
|
||||||
selected_asr: deepcopy(self.default_config['ASR'][selected_asr])
|
|
||||||
},
|
|
||||||
'VAD': {
|
|
||||||
selected_vad: deepcopy(self.default_config['VAD'][selected_vad])
|
|
||||||
},
|
|
||||||
'auth_code': auth_code # 添加认证码字段
|
|
||||||
}
|
|
||||||
|
|
||||||
all_configs[self.device_id] = device_config
|
|
||||||
|
|
||||||
# Save updated configs
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
self.private_config = all_configs[self.device_id]
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error handling private config: {e}")
|
|
||||||
self.private_config = {}
|
|
||||||
|
|
||||||
async def update_config(self, selected_modules: Dict[str, str], prompt: str, nickname: str) -> bool:
|
|
||||||
"""更新设备配置
|
|
||||||
Args:
|
|
||||||
selected_modules: 选择的模块配置,格式如 {'LLM': 'AliLLM', 'TTS': 'EdgeTTS',...}
|
|
||||||
prompt: 提示词配置
|
|
||||||
Returns:
|
|
||||||
bool: 更新是否成功
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
# Read main config to get full module configurations
|
|
||||||
main_config = self.default_config
|
|
||||||
|
|
||||||
# Create new device config
|
|
||||||
device_config = {
|
|
||||||
'selected_module': selected_modules,
|
|
||||||
'prompt': prompt,
|
|
||||||
'nickname': nickname,
|
|
||||||
}
|
|
||||||
if self.private_config.get('last_chat_time'):
|
|
||||||
device_config['last_chat_time'] = self.private_config['last_chat_time']
|
|
||||||
if self.private_config.get('owner'):
|
|
||||||
device_config['owner'] = self.private_config['owner']
|
|
||||||
|
|
||||||
# Copy full module configurations from main config
|
|
||||||
for module_type, selected_name in selected_modules.items():
|
|
||||||
if selected_name and selected_name in main_config.get(module_type, {}):
|
|
||||||
device_config[module_type] = {
|
|
||||||
selected_name: main_config[module_type][selected_name]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read all configs
|
|
||||||
if os.path.exists(self.config_path):
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
else:
|
|
||||||
all_configs = {}
|
|
||||||
|
|
||||||
# Update device config
|
|
||||||
all_configs[self.device_id] = device_config
|
|
||||||
self.private_config = device_config
|
|
||||||
|
|
||||||
# Save back to file
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error updating config: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def delete_config(self) -> bool:
|
|
||||||
"""删除设备配置
|
|
||||||
Returns:
|
|
||||||
bool: 删除是否成功
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
# 读取所有配置
|
|
||||||
if os.path.exists(self.config_path):
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 删除设备配置
|
|
||||||
if self.device_id in all_configs:
|
|
||||||
del all_configs[self.device_id]
|
|
||||||
|
|
||||||
# 保存更新后的配置
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
self.private_config = {}
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error deleting config: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_private_instances(self):
|
|
||||||
# 判断存在私有配置,并且self.device_id在私有配置中
|
|
||||||
if not self.private_config:
|
|
||||||
self.logger.bind(tag=TAG).error("Private config not found for device_id: {}", self.device_id)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
"""创建私有处理模块实例"""
|
|
||||||
config = self.private_config
|
|
||||||
selected_modules = config['selected_module']
|
|
||||||
return (
|
|
||||||
llm.create_instance(
|
|
||||||
selected_modules["LLM"]
|
|
||||||
if not 'type' in config["LLM"][selected_modules["LLM"]]
|
|
||||||
else
|
|
||||||
config["LLM"][selected_modules["LLM"]]['type'],
|
|
||||||
config["LLM"][selected_modules["LLM"]],
|
|
||||||
),
|
|
||||||
tts.create_instance(
|
|
||||||
selected_modules["TTS"]
|
|
||||||
if not 'type' in config["TTS"][selected_modules["TTS"]]
|
|
||||||
else
|
|
||||||
config["TTS"][selected_modules["TTS"]]["type"],
|
|
||||||
config["TTS"][selected_modules["TTS"]],
|
|
||||||
self.default_config.get("delete_audio", True) # Using default_config for global settings
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def update_last_chat_time(self, timestamp=None):
|
|
||||||
"""更新设备最近一次的聊天时间
|
|
||||||
Args:
|
|
||||||
timestamp: 指定的时间戳,不传则使用当前时间
|
|
||||||
"""
|
|
||||||
if not self.private_config:
|
|
||||||
self.logger.bind(tag=TAG).error("Private config not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
self.private_config['last_chat_time'] = timestamp
|
|
||||||
|
|
||||||
# 读取所有配置
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
|
|
||||||
# 更新当前设备配置
|
|
||||||
all_configs[self.device_id] = self.private_config
|
|
||||||
|
|
||||||
# 保存回文件
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error updating last chat time: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_auth_code(self) -> str:
|
|
||||||
"""获取设备的认证码
|
|
||||||
Returns:
|
|
||||||
str: 认证码,如果没有返回空字符串
|
|
||||||
"""
|
|
||||||
return self.private_config.get('auth_code', '')
|
|
||||||
|
|
||||||
async def bind_user(self, username: str) -> bool:
|
|
||||||
"""绑定用户到设备"""
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
# 检查用户是否存在
|
|
||||||
if not self.user_manager.get_user(username):
|
|
||||||
self.logger.bind(tag=TAG).error(f"User {username} not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 读取所有配置
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
|
|
||||||
if self.device_id not in all_configs:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Device {self.device_id} not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 删除认证码
|
|
||||||
auth_code = all_configs[self.device_id].get('auth_code')
|
|
||||||
self.logger.bind(tag=TAG).info(f"Binding user {username} to device {self.device_id}")
|
|
||||||
if auth_code:
|
|
||||||
del all_configs[self.device_id]['auth_code']
|
|
||||||
|
|
||||||
if self.auth_code_gen:
|
|
||||||
self.auth_code_gen.remove_code(auth_code)
|
|
||||||
|
|
||||||
# 更新设备所有者
|
|
||||||
all_configs[self.device_id]['owner'] = username
|
|
||||||
self.private_config = all_configs[self.device_id]
|
|
||||||
|
|
||||||
# 更新用户的设备列表
|
|
||||||
user_data = await self.user_manager.get_user(username)
|
|
||||||
if 'devices' not in user_data:
|
|
||||||
user_data['devices'] = []
|
|
||||||
if self.device_id not in user_data['devices']:
|
|
||||||
user_data['devices'].append(self.device_id)
|
|
||||||
await self.user_manager.update_user(username, user_data)
|
|
||||||
|
|
||||||
# 保存配置
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error binding user: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def unbind_user(self) -> bool:
|
|
||||||
"""解绑设备当前用户"""
|
|
||||||
try:
|
|
||||||
await self.lock_manager.acquire_lock(self.config_path)
|
|
||||||
try:
|
|
||||||
if not self.private_config.get('owner'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
username = self.private_config['owner']
|
|
||||||
|
|
||||||
# 从用户数据中移除设备
|
|
||||||
user_data = self.user_manager.get_user(username)
|
|
||||||
if user_data and 'devices' in user_data:
|
|
||||||
if self.device_id in user_data['devices']:
|
|
||||||
user_data['devices'].remove(self.device_id)
|
|
||||||
self.user_manager.update_user(username, user_data)
|
|
||||||
|
|
||||||
# 从设备配置中移除所有者
|
|
||||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
||||||
all_configs = yaml.safe_load(f) or {}
|
|
||||||
|
|
||||||
if self.device_id in all_configs:
|
|
||||||
if 'owner' in all_configs[self.device_id]:
|
|
||||||
del all_configs[self.device_id]['owner']
|
|
||||||
self.private_config = all_configs[self.device_id]
|
|
||||||
|
|
||||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(all_configs, f, allow_unicode=True)
|
|
||||||
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
self.lock_manager.release_lock(self.config_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error unbinding user: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_owner(self) -> Optional[str]:
|
|
||||||
"""获取设备当前所有者"""
|
|
||||||
return self.private_config.get('owner')
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import os
|
|
||||||
import argparse
|
|
||||||
from ruamel.yaml import YAML
|
|
||||||
from core.utils.util import read_config, get_project_dir
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_file():
|
|
||||||
default_config_file = "config.yaml"
|
|
||||||
# 判断是否存在私有的配置文件
|
|
||||||
if os.path.exists(get_project_dir() + "data/." + default_config_file):
|
|
||||||
default_config_file = "data/." + default_config_file
|
|
||||||
return default_config_file
|
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
"""加载配置文件"""
|
|
||||||
parser = argparse.ArgumentParser(description="Server configuration")
|
|
||||||
default_config_file = get_config_file()
|
|
||||||
parser.add_argument("--config_path", type=str, default=default_config_file)
|
|
||||||
args = parser.parse_args()
|
|
||||||
return read_config(args.config_path)
|
|
||||||
|
|
||||||
|
|
||||||
def update_config(config):
|
|
||||||
yaml = YAML()
|
|
||||||
yaml.preserve_quotes = True
|
|
||||||
"""将配置保存到YAML文件"""
|
|
||||||
with open(get_config_file(), 'w') as f:
|
|
||||||
yaml.dump(config, f)
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import queue
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
from config.logger import setup_logging
|
|
||||||
import threading
|
|
||||||
import websockets
|
|
||||||
from typing import Dict, Any
|
|
||||||
from collections import deque
|
|
||||||
from core.utils.util import is_segment
|
|
||||||
from core.utils.dialogue import Message, Dialogue
|
|
||||||
from core.handle.textHandle import handleTextMessage
|
|
||||||
from core.utils.util import get_string_no_punctuation_or_emoji
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
||||||
from core.handle.sendAudioHandle import sendAudioMessage
|
|
||||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
|
||||||
from config.private_config import PrivateConfig
|
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
|
||||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
|
||||||
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionHandler:
|
|
||||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music):
|
|
||||||
self.config = config
|
|
||||||
self.logger = setup_logging()
|
|
||||||
self.auth = AuthMiddleware(config)
|
|
||||||
|
|
||||||
self.websocket = None
|
|
||||||
self.headers = None
|
|
||||||
self.session_id = None
|
|
||||||
self.prompt = None
|
|
||||||
self.welcome_msg = None
|
|
||||||
|
|
||||||
# 客户端状态相关
|
|
||||||
self.client_abort = False
|
|
||||||
self.client_listen_mode = "auto"
|
|
||||||
|
|
||||||
# 线程任务相关
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
self.stop_event = threading.Event()
|
|
||||||
self.tts_queue = queue.Queue()
|
|
||||||
self.executor = ThreadPoolExecutor(max_workers=10)
|
|
||||||
self.scheduled_tasks = deque()
|
|
||||||
|
|
||||||
# 依赖的组件
|
|
||||||
self.vad = _vad
|
|
||||||
self.asr = _asr
|
|
||||||
self.llm = _llm
|
|
||||||
self.tts = _tts
|
|
||||||
self.dialogue = None
|
|
||||||
|
|
||||||
# vad相关变量
|
|
||||||
self.client_audio_buffer = bytes()
|
|
||||||
self.client_have_voice = False
|
|
||||||
self.client_have_voice_last_time = 0.0
|
|
||||||
self.client_no_voice_last_time = 0.0
|
|
||||||
self.client_voice_stop = False
|
|
||||||
|
|
||||||
# asr相关变量
|
|
||||||
self.asr_audio = []
|
|
||||||
self.asr_server_receive = True
|
|
||||||
|
|
||||||
# llm相关变量
|
|
||||||
self.llm_finish_task = False
|
|
||||||
self.dialogue = Dialogue()
|
|
||||||
|
|
||||||
# tts相关变量
|
|
||||||
self.tts_first_text = None
|
|
||||||
self.tts_last_text = None
|
|
||||||
self.tts_start_speak_time = None
|
|
||||||
self.tts_duration = 0
|
|
||||||
|
|
||||||
# iot相关变量
|
|
||||||
self.iot_descriptors = {}
|
|
||||||
|
|
||||||
self.cmd_exit = self.config["CMD_exit"]
|
|
||||||
self.max_cmd_length = 0
|
|
||||||
for cmd in self.cmd_exit:
|
|
||||||
if len(cmd) > self.max_cmd_length:
|
|
||||||
self.max_cmd_length = len(cmd)
|
|
||||||
|
|
||||||
self.private_config = None
|
|
||||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
|
||||||
self.is_device_verified = False # 添加设备验证状态标志
|
|
||||||
self.music_handler = _music
|
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
|
||||||
try:
|
|
||||||
# 获取并验证headers
|
|
||||||
self.headers = dict(ws.request.headers)
|
|
||||||
# 获取客户端ip地址
|
|
||||||
client_ip = ws.remote_address[0]
|
|
||||||
self.logger.bind(tag=TAG).info(f"{client_ip} conn - Headers: {self.headers}")
|
|
||||||
|
|
||||||
# 进行认证
|
|
||||||
await self.auth.authenticate(self.headers)
|
|
||||||
|
|
||||||
device_id = self.headers.get("device-id", None)
|
|
||||||
|
|
||||||
# Load private configuration if device_id is provided
|
|
||||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
|
||||||
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
|
||||||
if bUsePrivateConfig and device_id:
|
|
||||||
try:
|
|
||||||
self.private_config = PrivateConfig(device_id, self.config, self.auth_code_gen)
|
|
||||||
await self.private_config.load_or_create()
|
|
||||||
# 判断是否已经绑定
|
|
||||||
owner = self.private_config.get_owner()
|
|
||||||
self.is_device_verified = owner is not None
|
|
||||||
|
|
||||||
if self.is_device_verified:
|
|
||||||
await self.private_config.update_last_chat_time()
|
|
||||||
|
|
||||||
llm, tts = self.private_config.create_private_instances()
|
|
||||||
if all([llm, tts]):
|
|
||||||
self.llm = llm
|
|
||||||
self.tts = tts
|
|
||||||
self.logger.bind(tag=TAG).info(f"Loaded private config and instances for device {device_id}")
|
|
||||||
else:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Failed to create instances for device {device_id}")
|
|
||||||
self.private_config = None
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Error initializing private config: {e}")
|
|
||||||
self.private_config = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
# 认证通过,继续处理
|
|
||||||
self.websocket = ws
|
|
||||||
self.session_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
self.welcome_msg = self.config["xiaozhi"]
|
|
||||||
self.welcome_msg["session_id"] = self.session_id
|
|
||||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
|
||||||
|
|
||||||
await self.loop.run_in_executor(None, self._initialize_components)
|
|
||||||
|
|
||||||
tts_priority = threading.Thread(target=self._priority_thread, daemon=True)
|
|
||||||
tts_priority.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for message in self.websocket:
|
|
||||||
await self._route_message(message)
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
|
||||||
self.logger.bind(tag=TAG).info("客户端断开连接")
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
except AuthenticationError as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
|
|
||||||
await ws.close()
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
stack_trace = traceback.format_exc()
|
|
||||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
|
||||||
await ws.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
async def _route_message(self, message):
|
|
||||||
"""消息路由"""
|
|
||||||
if isinstance(message, str):
|
|
||||||
await handleTextMessage(self, message)
|
|
||||||
elif isinstance(message, bytes):
|
|
||||||
await handleAudioMessage(self, message)
|
|
||||||
|
|
||||||
def _initialize_components(self):
|
|
||||||
self.prompt = self.config["prompt"]
|
|
||||||
if self.private_config:
|
|
||||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
|
||||||
# 赋予LLM时间观念
|
|
||||||
if "{date_time}" in self.prompt:
|
|
||||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
|
||||||
self.prompt = self.prompt.replace("{date_time}", date_time)
|
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
|
||||||
|
|
||||||
async def _check_and_broadcast_auth_code(self):
|
|
||||||
"""检查设备绑定状态并广播认证码"""
|
|
||||||
if not self.private_config.get_owner():
|
|
||||||
auth_code = self.private_config.get_auth_code()
|
|
||||||
if auth_code:
|
|
||||||
# 发送验证码语音提示
|
|
||||||
text = f"请在后台输入验证码:{' '.join(auth_code)}"
|
|
||||||
self.recode_first_last_text(text)
|
|
||||||
future = self.executor.submit(self.speak_and_play, text)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def isNeedAuth(self):
|
|
||||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
|
||||||
if not bUsePrivateConfig:
|
|
||||||
# 如果不使用私有配置,就不需要验证
|
|
||||||
return False
|
|
||||||
return not self.is_device_verified
|
|
||||||
|
|
||||||
def chat(self, query):
|
|
||||||
# 如果设备未验证,就发送验证码
|
|
||||||
if self.isNeedAuth():
|
|
||||||
self.llm_finish_task = True
|
|
||||||
# 创建一个新的事件循环来运行异步函数
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(self._check_and_broadcast_auth_code())
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.dialogue.put(Message(role="user", content=query))
|
|
||||||
response_message = []
|
|
||||||
start = 0
|
|
||||||
# 提交 LLM 任务
|
|
||||||
try:
|
|
||||||
start_time = time.time() # 记录开始时间
|
|
||||||
llm_responses = self.llm.response(self.session_id, self.dialogue.get_llm_dialogue())
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
|
||||||
return None
|
|
||||||
# 提交 TTS 任务到线程池
|
|
||||||
self.llm_finish_task = False
|
|
||||||
for content in llm_responses:
|
|
||||||
response_message.append(content)
|
|
||||||
# 如果中途被打断,就停止生成
|
|
||||||
if self.client_abort:
|
|
||||||
start = len(response_message)
|
|
||||||
break
|
|
||||||
|
|
||||||
end_time = time.time() # 记录结束时间
|
|
||||||
self.logger.bind(tag=TAG).debug(f"大模型返回时间时间: {end_time - start_time} 秒, 生成token={content}")
|
|
||||||
if is_segment(response_message):
|
|
||||||
segment_text = "".join(response_message[start:])
|
|
||||||
segment_text = get_string_no_punctuation_or_emoji(segment_text)
|
|
||||||
if len(segment_text) > 0:
|
|
||||||
self.recode_first_last_text(segment_text)
|
|
||||||
future = self.executor.submit(self.speak_and_play, segment_text)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
start = len(response_message)
|
|
||||||
|
|
||||||
# 处理剩余的响应
|
|
||||||
if start < len(response_message):
|
|
||||||
segment_text = "".join(response_message[start:])
|
|
||||||
if len(segment_text) > 0:
|
|
||||||
self.recode_first_last_text(segment_text)
|
|
||||||
future = self.executor.submit(self.speak_and_play, segment_text)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
|
|
||||||
self.llm_finish_task = True
|
|
||||||
# 更新对话
|
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
|
||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _priority_thread(self):
|
|
||||||
while not self.stop_event.is_set():
|
|
||||||
text = None
|
|
||||||
try:
|
|
||||||
future = self.tts_queue.get()
|
|
||||||
if future is None:
|
|
||||||
continue
|
|
||||||
text = None
|
|
||||||
try:
|
|
||||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
|
||||||
tts_file, text = future.result(timeout=10)
|
|
||||||
if text is None or len(text) <= 0:
|
|
||||||
continue
|
|
||||||
if tts_file is None:
|
|
||||||
self.logger.bind(tag=TAG).error(f"TTS文件生成失败: {text}")
|
|
||||||
continue
|
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS文件生成完毕,文件路径: {tts_file}")
|
|
||||||
if os.path.exists(tts_file):
|
|
||||||
opus_datas, duration = self.tts.wav_to_opus_data(tts_file)
|
|
||||||
else:
|
|
||||||
self.logger.bind(tag=TAG).error(f"TTS文件不存在: {tts_file}")
|
|
||||||
opus_datas = []
|
|
||||||
duration = 0
|
|
||||||
except TimeoutError:
|
|
||||||
self.logger.bind(tag=TAG).error("TTS 任务超时")
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"TTS 任务出错: {e}")
|
|
||||||
continue
|
|
||||||
if not self.client_abort:
|
|
||||||
# 如果没有中途打断就发送语音
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
sendAudioMessage(self, opus_datas, duration, text), self.loop
|
|
||||||
)
|
|
||||||
if self.tts.delete_audio_file and os.path.exists(tts_file):
|
|
||||||
os.remove(tts_file)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
|
|
||||||
self.clearSpeakStatus()
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})),
|
|
||||||
self.loop
|
|
||||||
)
|
|
||||||
self.logger.bind(tag=TAG).error(f"tts_priority priority_thread: {text}{e}")
|
|
||||||
|
|
||||||
def speak_and_play(self, text):
|
|
||||||
if text is None or len(text) <= 0:
|
|
||||||
self.logger.bind(tag=TAG).info(f"无需tts转换,query为空,{text}")
|
|
||||||
return None, text
|
|
||||||
tts_file = self.tts.to_tts(text)
|
|
||||||
if tts_file is None:
|
|
||||||
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
|
|
||||||
return None, text
|
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
|
|
||||||
return tts_file, text
|
|
||||||
|
|
||||||
def clearSpeakStatus(self):
|
|
||||||
self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态")
|
|
||||||
self.asr_server_receive = True
|
|
||||||
self.tts_last_text = None
|
|
||||||
self.tts_first_text = None
|
|
||||||
self.tts_duration = 0
|
|
||||||
self.tts_start_speak_time = None
|
|
||||||
|
|
||||||
def recode_first_last_text(self, text):
|
|
||||||
if not self.tts_first_text:
|
|
||||||
self.logger.bind(tag=TAG).info(f"大模型说出第一句话: {text}")
|
|
||||||
self.tts_first_text = text
|
|
||||||
self.tts_last_text = text
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""资源清理方法"""
|
|
||||||
|
|
||||||
# 清理其他资源
|
|
||||||
self.stop_event.set()
|
|
||||||
self.executor.shutdown(wait=False)
|
|
||||||
if self.websocket:
|
|
||||||
await self.websocket.close()
|
|
||||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
|
||||||
|
|
||||||
def reset_vad_states(self):
|
|
||||||
self.client_audio_buffer = bytes()
|
|
||||||
self.client_have_voice = False
|
|
||||||
self.client_have_voice_last_time = 0
|
|
||||||
self.client_voice_stop = False
|
|
||||||
self.logger.bind(tag=TAG).debug("VAD states reset.")
|
|
||||||
|
|
||||||
def stop_all_tasks(self):
|
|
||||||
while self.scheduled_tasks:
|
|
||||||
task = self.scheduled_tasks.popleft()
|
|
||||||
task.cancel()
|
|
||||||
self.scheduled_tasks.clear()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import json
|
|
||||||
from config.logger import setup_logging
|
|
||||||
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
async def handleHelloMessage(conn):
|
|
||||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import json
|
|
||||||
from config.logger import setup_logging
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class IotDescriptor:
|
|
||||||
"""
|
|
||||||
A class to represent an IoT descriptor.
|
|
||||||
Attributes:
|
|
||||||
----------
|
|
||||||
name : str
|
|
||||||
The name of the IoT descriptor.
|
|
||||||
description : str
|
|
||||||
A brief description of the IoT descriptor.
|
|
||||||
properties : dict
|
|
||||||
A dictionary containing properties of the IoT descriptor.
|
|
||||||
methods : dict
|
|
||||||
A dictionary containing methods of the IoT descriptor.
|
|
||||||
-------
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, description, properties, methods):
|
|
||||||
self.name = name
|
|
||||||
self.description = description
|
|
||||||
self.properties = []
|
|
||||||
self.methods = []
|
|
||||||
|
|
||||||
# 根据描述创建属性
|
|
||||||
for key, value in properties.items():
|
|
||||||
# "volume":{"description":"当前音量 值","type":"number"}
|
|
||||||
"""
|
|
||||||
等价于
|
|
||||||
{
|
|
||||||
'name': 名字,
|
|
||||||
'description': 描述,
|
|
||||||
'value': 0
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# setattr(self, key, {}) # 创建一个空字典, 名字是属性名
|
|
||||||
property_item = globals()[key] = {} # 创建一个空字典, 名字是属性名
|
|
||||||
property_item['name'] = key
|
|
||||||
property_item["description"] = value["description"]
|
|
||||||
if value["type"] == "number":
|
|
||||||
property_item["value"] = 0
|
|
||||||
elif value["type"] == "boolean":
|
|
||||||
property_item["value"] = False
|
|
||||||
else:
|
|
||||||
property_item["value"] = ""
|
|
||||||
self.properties.append(property_item)
|
|
||||||
|
|
||||||
# 根据描述创建方法
|
|
||||||
for key, value in methods.items():
|
|
||||||
# "SetVolume": {"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}}
|
|
||||||
"""
|
|
||||||
等价于
|
|
||||||
SetVolume = {
|
|
||||||
`description`: 描述,
|
|
||||||
`volume`: {
|
|
||||||
`description`: 描述,
|
|
||||||
`value`: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# setattr(self, key, {}) # 创建一个空字典, 名字是方法名
|
|
||||||
method = globals()[key] = {} # 创建一个空字典, 名字是方法名
|
|
||||||
method["description"] = value["description"]
|
|
||||||
method['name'] = key
|
|
||||||
for k, v in value["parameters"].items():
|
|
||||||
# 不同的参数解析
|
|
||||||
method[k] = {}
|
|
||||||
method[k]["description"] = v["description"]
|
|
||||||
if v["type"] == "number":
|
|
||||||
method[k]["value"] = 0
|
|
||||||
elif v["type"] == "boolean":
|
|
||||||
method[k]["value"] = False
|
|
||||||
else:
|
|
||||||
method[k]["value"] = ""
|
|
||||||
|
|
||||||
self.methods.append(method)
|
|
||||||
|
|
||||||
|
|
||||||
async def handleIotDescriptors(conn, descriptors):
|
|
||||||
"""
|
|
||||||
处理物联网描述
|
|
||||||
示例: [{
|
|
||||||
"name":"Speaker",
|
|
||||||
"description":"当前 AI 机器人的扬声器",
|
|
||||||
"properties":{
|
|
||||||
"volume":{"description":"当前音量 值","type":"number"} 可以有boolean, number, string三种类型
|
|
||||||
},
|
|
||||||
"methods":{
|
|
||||||
"SetVolume":{
|
|
||||||
"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
descriptors: 描述列表
|
|
||||||
"""
|
|
||||||
for descriptor in descriptors:
|
|
||||||
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
|
|
||||||
descriptor["methods"])
|
|
||||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
|
||||||
|
|
||||||
# 暂时从配置文件中设置音量,后期通过意图识别控制音量
|
|
||||||
default_iot_volume = 100
|
|
||||||
if "iot" in conn.config:
|
|
||||||
default_iot_volume = conn.config["iot"]["Speaker"]["volume"]
|
|
||||||
logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}")
|
|
||||||
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume})
|
|
||||||
|
|
||||||
|
|
||||||
async def send_iot_conn(conn, name, method_name, parameters):
|
|
||||||
"""
|
|
||||||
发送物联网指令
|
|
||||||
name: 设备名称 "Speaker"
|
|
||||||
method: 方法 "SetVolume"
|
|
||||||
parameters: 参数, 是一个字典 {"volume": 100}
|
|
||||||
发送示例:
|
|
||||||
{
|
|
||||||
"type": "iot",
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"name" : "Speaker",
|
|
||||||
"method": "SetVolume",
|
|
||||||
"parameters": {
|
|
||||||
"volume": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
for key, value in conn.iot_descriptors.items():
|
|
||||||
if key == name:
|
|
||||||
# 找到了设备
|
|
||||||
for method in value.methods:
|
|
||||||
# 找到了方法
|
|
||||||
if method["name"] == method_name:
|
|
||||||
await conn.websocket.send(json.dumps({
|
|
||||||
"type": "iot",
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"name": name,
|
|
||||||
"method": method_name,
|
|
||||||
"parameters": parameters
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import difflib
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_song_name(text):
|
|
||||||
"""从用户输入中提取歌名"""
|
|
||||||
for keyword in ["听", "播放", "放", "唱"]:
|
|
||||||
if keyword in text:
|
|
||||||
parts = text.split(keyword)
|
|
||||||
if len(parts) > 1:
|
|
||||||
return parts[1].strip()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _find_best_match(potential_song, music_files):
|
|
||||||
"""查找最匹配的歌曲"""
|
|
||||||
best_match = None
|
|
||||||
highest_ratio = 0
|
|
||||||
|
|
||||||
for music_file in music_files:
|
|
||||||
song_name = os.path.splitext(music_file)[0]
|
|
||||||
ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
|
|
||||||
if ratio > highest_ratio and ratio > 0.4:
|
|
||||||
highest_ratio = ratio
|
|
||||||
best_match = music_file
|
|
||||||
return best_match
|
|
||||||
|
|
||||||
|
|
||||||
class MusicHandler:
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
self.music_related_keywords = []
|
|
||||||
|
|
||||||
if "music" in self.config:
|
|
||||||
self.music_config = self.config["music"]
|
|
||||||
self.music_dir = os.path.abspath(
|
|
||||||
self.music_config.get("music_dir", "./music") # 默认路径修改
|
|
||||||
)
|
|
||||||
self.music_related_keywords = self.music_config.get("music_commands", [])
|
|
||||||
else:
|
|
||||||
self.music_dir = os.path.abspath("./music")
|
|
||||||
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
|
|
||||||
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
|
|
||||||
|
|
||||||
async def handle_music_command(self, conn, text):
|
|
||||||
"""处理音乐播放指令"""
|
|
||||||
clean_text = re.sub(r'[^\w\s]', '', text).strip()
|
|
||||||
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
|
||||||
|
|
||||||
# 尝试匹配具体歌名
|
|
||||||
if os.path.exists(self.music_dir):
|
|
||||||
music_files = [f for f in os.listdir(self.music_dir) if f.endswith('.mp3')]
|
|
||||||
logger.bind(tag=TAG).debug(f"找到的音乐文件: {music_files}")
|
|
||||||
|
|
||||||
potential_song = _extract_song_name(clean_text)
|
|
||||||
if potential_song:
|
|
||||||
best_match = _find_best_match(potential_song, music_files)
|
|
||||||
if best_match:
|
|
||||||
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
|
|
||||||
await self.play_local_music(conn, specific_file=best_match)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 检查是否是通用播放音乐命令
|
|
||||||
if any(cmd in clean_text for cmd in self.music_related_keywords):
|
|
||||||
await self.play_local_music(conn)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def play_local_music(self, conn, specific_file=None):
|
|
||||||
"""播放本地音乐文件"""
|
|
||||||
try:
|
|
||||||
if not os.path.exists(self.music_dir):
|
|
||||||
logger.bind(tag=TAG).error(f"音乐目录不存在: {self.music_dir}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 确保路径正确性
|
|
||||||
if specific_file:
|
|
||||||
music_path = os.path.join(self.music_dir, specific_file)
|
|
||||||
if not os.path.exists(music_path):
|
|
||||||
logger.bind(tag=TAG).error(f"指定的音乐文件不存在: {music_path}")
|
|
||||||
return
|
|
||||||
selected_music = specific_file
|
|
||||||
else:
|
|
||||||
music_files = [f for f in os.listdir(self.music_dir) if f.endswith('.mp3')]
|
|
||||||
if not music_files:
|
|
||||||
logger.bind(tag=TAG).error("未找到MP3音乐文件")
|
|
||||||
return
|
|
||||||
selected_music = random.choice(music_files)
|
|
||||||
music_path = os.path.join(self.music_dir, selected_music)
|
|
||||||
text = f"正在播放{selected_music}"
|
|
||||||
await send_stt_message(conn, text)
|
|
||||||
conn.tts_first_text = selected_music
|
|
||||||
conn.tts_last_text = selected_music
|
|
||||||
conn.llm_finish_task = True
|
|
||||||
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
|
|
||||||
await sendAudioMessage(conn, opus_packets, duration, selected_music)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
|
||||||
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
from core.utils.util import remove_punctuation_and_length
|
|
||||||
from core.handle.sendAudioHandle import schedule_with_interrupt, send_stt_message
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
async def handleAudioMessage(conn, audio):
|
|
||||||
if not conn.asr_server_receive:
|
|
||||||
logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
|
|
||||||
return
|
|
||||||
if conn.client_listen_mode == "auto":
|
|
||||||
have_voice = conn.vad.is_vad(conn, audio)
|
|
||||||
else:
|
|
||||||
have_voice = conn.client_have_voice
|
|
||||||
|
|
||||||
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
|
||||||
if have_voice == False and conn.client_have_voice == False:
|
|
||||||
await no_voice_close_connect(conn)
|
|
||||||
conn.asr_audio.clear()
|
|
||||||
return
|
|
||||||
conn.client_no_voice_last_time = 0.0
|
|
||||||
conn.asr_audio.append(audio)
|
|
||||||
# 如果本段有声音,且已经停止了
|
|
||||||
if conn.client_voice_stop:
|
|
||||||
conn.client_abort = False
|
|
||||||
conn.asr_server_receive = False
|
|
||||||
# 音频太短了,无法识别
|
|
||||||
if len(conn.asr_audio) < 3:
|
|
||||||
conn.asr_server_receive = True
|
|
||||||
else:
|
|
||||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
|
||||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
|
||||||
text_len, text_without_punctuation = remove_punctuation_and_length(text)
|
|
||||||
if await conn.music_handler.handle_music_command(conn, text_without_punctuation):
|
|
||||||
conn.asr_server_receive = True
|
|
||||||
conn.asr_audio.clear()
|
|
||||||
return
|
|
||||||
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
|
|
||||||
return
|
|
||||||
if text_len > 0:
|
|
||||||
await startToChat(conn, text)
|
|
||||||
else:
|
|
||||||
conn.asr_server_receive = True
|
|
||||||
conn.asr_audio.clear()
|
|
||||||
conn.reset_vad_states()
|
|
||||||
|
|
||||||
|
|
||||||
async def handleCMDMessage(conn, text):
|
|
||||||
cmd_exit = conn.cmd_exit
|
|
||||||
for cmd in cmd_exit:
|
|
||||||
if text == cmd:
|
|
||||||
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
|
|
||||||
await conn.close()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def startToChat(conn, text):
|
|
||||||
# 异步发送 stt 信息
|
|
||||||
stt_task = asyncio.create_task(
|
|
||||||
schedule_with_interrupt(0, send_stt_message(conn, text))
|
|
||||||
)
|
|
||||||
conn.scheduled_tasks.append(stt_task)
|
|
||||||
conn.executor.submit(conn.chat, text)
|
|
||||||
|
|
||||||
|
|
||||||
async def no_voice_close_connect(conn):
|
|
||||||
if conn.client_no_voice_last_time == 0.0:
|
|
||||||
conn.client_no_voice_last_time = time.time() * 1000
|
|
||||||
else:
|
|
||||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
|
||||||
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
|
||||||
if no_voice_time > 1000 * close_connection_no_voice_time:
|
|
||||||
conn.client_abort = False
|
|
||||||
conn.asr_server_receive = False
|
|
||||||
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
|
|
||||||
await startToChat(conn, prompt)
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
from core.utils.util import remove_punctuation_and_length, get_string_no_punctuation_or_emoji
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
async def isLLMWantToFinish(conn):
|
|
||||||
first_text = conn.tts_first_text
|
|
||||||
last_text = conn.tts_last_text
|
|
||||||
_, last_text_without_punctuation = remove_punctuation_and_length(last_text)
|
|
||||||
if "再见" in last_text_without_punctuation or "拜拜" in last_text_without_punctuation:
|
|
||||||
return True
|
|
||||||
_, first_text_without_punctuation = remove_punctuation_and_length(first_text)
|
|
||||||
if "再见" in first_text_without_punctuation or "拜拜" in first_text_without_punctuation:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def sendAudioMessage(conn, audios, duration, text):
|
|
||||||
base_delay = conn.tts_duration
|
|
||||||
|
|
||||||
# 发送 tts.start
|
|
||||||
if text == conn.tts_first_text:
|
|
||||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
|
||||||
conn.tts_start_speak_time = time.time()
|
|
||||||
|
|
||||||
# 发送 sentence_start(每个音频文件之前发送一次)
|
|
||||||
sentence_task = asyncio.create_task(
|
|
||||||
schedule_with_interrupt(base_delay, send_tts_message(conn, "sentence_start", text))
|
|
||||||
)
|
|
||||||
conn.scheduled_tasks.append(sentence_task)
|
|
||||||
|
|
||||||
conn.tts_duration += duration
|
|
||||||
|
|
||||||
# 发送音频数据
|
|
||||||
for idx, opus_packet in enumerate(audios):
|
|
||||||
await conn.websocket.send(opus_packet)
|
|
||||||
|
|
||||||
if conn.llm_finish_task and text == conn.tts_last_text:
|
|
||||||
stop_duration = conn.tts_duration - (time.time() - conn.tts_start_speak_time)
|
|
||||||
stop_task = asyncio.create_task(
|
|
||||||
schedule_with_interrupt(stop_duration, send_tts_message(conn, 'stop'))
|
|
||||||
)
|
|
||||||
conn.scheduled_tasks.append(stop_task)
|
|
||||||
if await isLLMWantToFinish(conn):
|
|
||||||
finish_task = asyncio.create_task(
|
|
||||||
schedule_with_interrupt(stop_duration, await conn.close())
|
|
||||||
)
|
|
||||||
conn.scheduled_tasks.append(finish_task)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_tts_message(conn, state, text=None):
|
|
||||||
"""发送 TTS 状态消息"""
|
|
||||||
message = {
|
|
||||||
"type": "tts",
|
|
||||||
"state": state,
|
|
||||||
"session_id": conn.session_id
|
|
||||||
}
|
|
||||||
if text is not None:
|
|
||||||
message["text"] = text
|
|
||||||
|
|
||||||
await conn.websocket.send(json.dumps(message))
|
|
||||||
if state == "stop":
|
|
||||||
conn.clearSpeakStatus()
|
|
||||||
|
|
||||||
|
|
||||||
async def send_stt_message(conn, text):
|
|
||||||
"""发送 STT 状态消息"""
|
|
||||||
stt_text = get_string_no_punctuation_or_emoji(text)
|
|
||||||
await conn.websocket.send(json.dumps({
|
|
||||||
"type": "stt",
|
|
||||||
"text": stt_text,
|
|
||||||
"session_id": conn.session_id}
|
|
||||||
))
|
|
||||||
await conn.websocket.send(
|
|
||||||
json.dumps({
|
|
||||||
"type": "llm",
|
|
||||||
"text": "😊",
|
|
||||||
"emotion": "happy",
|
|
||||||
"session_id": conn.session_id}
|
|
||||||
))
|
|
||||||
await send_tts_message(conn, "start")
|
|
||||||
|
|
||||||
|
|
||||||
async def schedule_with_interrupt(delay, coro):
|
|
||||||
"""可中断的延迟调度"""
|
|
||||||
try:
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
await coro
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProviderBase(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
"""LLM response generator"""
|
|
||||||
pass
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
# 定义用于匹配中文标点符号的正则表达式(包括句号、感叹号、问号、分号)
|
|
||||||
punctuation_pattern = re.compile(r'([。!?;])')
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.personal_access_token = config.get("personal_access_token")
|
|
||||||
self.bot_id = config.get("bot_id")
|
|
||||||
self.user_id = config.get("user_id") # 默认用户 ID
|
|
||||||
self.base_url = config.get("base_url")
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
try:
|
|
||||||
# 从对话中取出最新的用户消息
|
|
||||||
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
|
||||||
data = {
|
|
||||||
"conversation_id": session_id,
|
|
||||||
"bot_id": self.bot_id,
|
|
||||||
"user": self.user_id,
|
|
||||||
"query": last_msg["content"],
|
|
||||||
"stream": True
|
|
||||||
}
|
|
||||||
logger.bind(tag=TAG).info(f"发送到 Coze API 的请求数据: {json.dumps(data, ensure_ascii=False)}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {self.personal_access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Host': 'api.coze.cn',
|
|
||||||
'Connection': 'keep-alive'
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
self.base_url,
|
|
||||||
headers=headers,
|
|
||||||
json=data,
|
|
||||||
stream=True
|
|
||||||
)
|
|
||||||
logger.bind(tag=TAG).info(f"请求状态: {response.status_code}")
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
# 对每一行流数据进行处理,不做跨块累积
|
|
||||||
for line_bytes in response.iter_lines(decode_unicode=False):
|
|
||||||
if not line_bytes:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
# 使用 utf-8 解码,错误部分用替换符
|
|
||||||
line = line_bytes.decode('utf-8', errors='replace')
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"解码失败: {e}")
|
|
||||||
continue
|
|
||||||
if line.startswith("data:"):
|
|
||||||
data_str = line[len("data:"):].strip()
|
|
||||||
if data_str == "[DONE]":
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
data_chunk = json.loads(data_str)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.bind(tag=TAG).error(f"JSON解析失败: {e} 数据: {line}")
|
|
||||||
continue
|
|
||||||
msg = data_chunk.get("message", {})
|
|
||||||
if msg.get("role") == "assistant" and msg.get("type") == "answer":
|
|
||||||
content = msg.get("content", "")
|
|
||||||
# 如果返回内容中包含标点符号,则按标点拆分,立即返回每个片段
|
|
||||||
if punctuation_pattern.search(content):
|
|
||||||
# 利用 finditer 找到每个标点,并返回以标点结尾的片段
|
|
||||||
start = 0
|
|
||||||
for match in punctuation_pattern.finditer(content):
|
|
||||||
end = match.end()
|
|
||||||
sentence = content[start:end].strip()
|
|
||||||
if sentence:
|
|
||||||
yield sentence
|
|
||||||
start = end
|
|
||||||
# 如果拆分后剩余内容也返回(不含标点),直接返回
|
|
||||||
if start < len(content):
|
|
||||||
remainder = content[start:].strip()
|
|
||||||
if remainder:
|
|
||||||
yield remainder
|
|
||||||
else:
|
|
||||||
# 如果没有标点,则直接返回这块内容
|
|
||||||
if content.strip():
|
|
||||||
yield content.strip()
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).error(f"请求失败,状态码: {response.status_code}")
|
|
||||||
yield f"【Coze服务响应异常:请求失败,状态码 {response.status_code}】"
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Error in Coze response generation: {e}")
|
|
||||||
yield "【Coze服务响应异常】"
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import json
|
|
||||||
from config.logger import setup_logging
|
|
||||||
import requests
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.api_key = config["api_key"]
|
|
||||||
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/')
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
try:
|
|
||||||
# 取最后一条用户消息
|
|
||||||
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
|
||||||
|
|
||||||
# 发起流式请求
|
|
||||||
with requests.post(
|
|
||||||
f"{self.base_url}/chat-messages",
|
|
||||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
||||||
json={
|
|
||||||
"query": last_msg["content"],
|
|
||||||
"response_mode": "streaming",
|
|
||||||
"user": session_id,
|
|
||||||
"inputs": {}
|
|
||||||
},
|
|
||||||
stream=True
|
|
||||||
) as r:
|
|
||||||
for line in r.iter_lines():
|
|
||||||
if line.startswith(b'data: '):
|
|
||||||
event = json.loads(line[6:])
|
|
||||||
if event.get('answer'):
|
|
||||||
yield event['answer']
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
|
||||||
yield "【服务响应异常】"
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import google.generativeai as genai
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
"""初始化Gemini LLM Provider"""
|
|
||||||
self.model_name = config.get("model_name", "gemini-1.5-pro")
|
|
||||||
self.api_key = config.get("api_key")
|
|
||||||
|
|
||||||
if not self.api_key or "你" in self.api_key:
|
|
||||||
logger.bind(tag=TAG).error("你还没配置Gemini LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 初始化Gemini客户端
|
|
||||||
genai.configure(api_key=self.api_key)
|
|
||||||
self.model = genai.GenerativeModel(self.model_name)
|
|
||||||
|
|
||||||
# 设置生成参数
|
|
||||||
self.generation_config = {
|
|
||||||
"temperature": 0.7,
|
|
||||||
"top_p": 0.9,
|
|
||||||
"top_k": 40,
|
|
||||||
"max_output_tokens": 2048,
|
|
||||||
}
|
|
||||||
self.chat = None
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Gemini初始化失败: {e}")
|
|
||||||
self.model = None
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
"""生成Gemini对话响应"""
|
|
||||||
if not self.model:
|
|
||||||
yield "【Gemini服务未正确初始化】"
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 处理对话历史
|
|
||||||
chat_history = []
|
|
||||||
for msg in dialogue[:-1]: # 历史对话
|
|
||||||
role = "model" if msg["role"] == "assistant" else "user"
|
|
||||||
content = msg["content"].strip()
|
|
||||||
if content:
|
|
||||||
chat_history.append({
|
|
||||||
"role": role,
|
|
||||||
"parts": [content]
|
|
||||||
})
|
|
||||||
|
|
||||||
# 获取当前消息
|
|
||||||
current_msg = dialogue[-1]["content"]
|
|
||||||
|
|
||||||
# 创建新的聊天会话
|
|
||||||
chat = self.model.start_chat(history=chat_history)
|
|
||||||
|
|
||||||
# 发送消息并获取流式响应
|
|
||||||
response = chat.send_message(
|
|
||||||
current_msg,
|
|
||||||
stream=True,
|
|
||||||
generation_config=self.generation_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# 处理流式响应
|
|
||||||
for chunk in response:
|
|
||||||
if hasattr(chunk, 'text') and chunk.text:
|
|
||||||
yield chunk.text
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
|
|
||||||
|
|
||||||
# 针对不同错误返回友好提示
|
|
||||||
if "Rate limit" in error_msg:
|
|
||||||
yield "【Gemini服务请求太频繁,请稍后再试】"
|
|
||||||
elif "Invalid API key" in error_msg:
|
|
||||||
yield "【Gemini API key无效】"
|
|
||||||
else:
|
|
||||||
yield f"【Gemini服务响应异常: {error_msg}】"
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import requests
|
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.agent_id = config.get("agent_id") # 对应 agent_id
|
|
||||||
self.api_key = config.get("api_key")
|
|
||||||
self.base_url = config.get("base_url", config.get("url")) # 默认使用 base_url
|
|
||||||
self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
print(dialogue)
|
|
||||||
try:
|
|
||||||
# home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可
|
|
||||||
|
|
||||||
# 提取最后一个 role 为 'user' 的 content
|
|
||||||
input_text = None
|
|
||||||
if isinstance(dialogue, list): # 确保 dialogue 是一个列表
|
|
||||||
# 逆序遍历,找到最后一个 role 为 'user' 的消息
|
|
||||||
for message in reversed(dialogue):
|
|
||||||
if message.get("role") == "user": # 找到 role 为 'user' 的消息
|
|
||||||
input_text = message.get("content", "")
|
|
||||||
break # 找到后立即退出循环
|
|
||||||
|
|
||||||
# 构造请求数据
|
|
||||||
payload = {
|
|
||||||
"text": input_text,
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"conversation_id": session_id # 使用 session_id 作为 conversation_id
|
|
||||||
}
|
|
||||||
# 设置请求头
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发起 POST 请求
|
|
||||||
response = requests.post(self.api_url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
# 检查请求是否成功
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
# 解析返回数据
|
|
||||||
data = response.json()
|
|
||||||
speech = data.get("response", {}).get("speech", {}).get("plain", {}).get("speech", "")
|
|
||||||
|
|
||||||
# 返回生成的内容
|
|
||||||
if speech:
|
|
||||||
yield speech
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容")
|
|
||||||
|
|
||||||
except RequestException as e:
|
|
||||||
logger.bind(tag=TAG).error(f"HTTP 请求错误: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import requests, json
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
|
|
||||||
self.model_name = config.get("model_name")
|
|
||||||
self.base_url = config.get("base_url", "http://localhost:11434")
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
try:
|
|
||||||
# Convert dialogue format to Ollama format
|
|
||||||
prompt = ""
|
|
||||||
for msg in dialogue:
|
|
||||||
if msg["role"] == "system":
|
|
||||||
prompt += f"System: {msg['content']}\n"
|
|
||||||
elif msg["role"] == "user":
|
|
||||||
prompt += f"User: {msg['content']}\n"
|
|
||||||
elif msg["role"] == "assistant":
|
|
||||||
prompt += f"Assistant: {msg['content']}\n"
|
|
||||||
|
|
||||||
# Make request to Ollama API
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.base_url}/api/generate",
|
|
||||||
json={
|
|
||||||
"model": self.model_name,
|
|
||||||
"prompt": prompt,
|
|
||||||
"stream": True
|
|
||||||
},
|
|
||||||
stream=True
|
|
||||||
)
|
|
||||||
|
|
||||||
for line in response.iter_lines():
|
|
||||||
if line:
|
|
||||||
json_response = json.loads(line)
|
|
||||||
if "response" in json_response:
|
|
||||||
yield json_response["response"]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
|
|
||||||
yield "【Ollama服务响应异常】"
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
import openai
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.model_name = config.get("model_name")
|
|
||||||
self.api_key = config.get("api_key")
|
|
||||||
if 'base_url' in config:
|
|
||||||
self.base_url = config.get("base_url")
|
|
||||||
else:
|
|
||||||
self.base_url = config.get("url")
|
|
||||||
if "你" in self.api_key:
|
|
||||||
logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
|
||||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
try:
|
|
||||||
responses = self.client.chat.completions.create(
|
|
||||||
model=self.model_name,
|
|
||||||
messages=dialogue,
|
|
||||||
stream=True
|
|
||||||
)
|
|
||||||
|
|
||||||
is_active = True
|
|
||||||
for chunk in responses:
|
|
||||||
try:
|
|
||||||
# 检查是否存在有效的choice且content不为空
|
|
||||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
|
||||||
content = delta.content if hasattr(delta, 'content') else ''
|
|
||||||
except IndexError:
|
|
||||||
content = ''
|
|
||||||
if content:
|
|
||||||
# 处理标签跨多个chunk的情况
|
|
||||||
if '<think>' in content:
|
|
||||||
is_active = False
|
|
||||||
content = content.split('<think>')[0]
|
|
||||||
if '</think>' in content:
|
|
||||||
is_active = True
|
|
||||||
content = content.split('</think>')[-1]
|
|
||||||
if is_active:
|
|
||||||
yield content
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import os
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from core.providers.tts.base import TTSProviderBase
|
|
||||||
|
|
||||||
import http.client
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
|
||||||
def __init__(self, config, delete_audio_file):
|
|
||||||
super().__init__(config, delete_audio_file)
|
|
||||||
self.appkey = config.get("appkey")
|
|
||||||
self.token = config.get("token")
|
|
||||||
self.format = config.get("format", "wav")
|
|
||||||
self.sample_rate = config.get("sample_rate", 16000)
|
|
||||||
self.voice = config.get("voice", "xiaoyun")
|
|
||||||
self.volume = config.get("volume", 50)
|
|
||||||
self.speech_rate = config.get("speech_rate", 0)
|
|
||||||
self.pitch_rate = config.get("pitch_rate", 0)
|
|
||||||
|
|
||||||
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
|
||||||
self.api_url = f"https://{self.host}/stream/v1/tts"
|
|
||||||
self.header = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
|
||||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
|
||||||
request_json = {
|
|
||||||
"appkey": self.appkey,
|
|
||||||
"token": self.token,
|
|
||||||
"text": text,
|
|
||||||
"format": self.format,
|
|
||||||
"sample_rate": self.sample_rate,
|
|
||||||
"voice": self.voice,
|
|
||||||
"volume": self.volume,
|
|
||||||
"speech_rate": self.speech_rate,
|
|
||||||
"pitch_rate": self.pitch_rate
|
|
||||||
}
|
|
||||||
|
|
||||||
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
|
||||||
try:
|
|
||||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
|
||||||
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
|
||||||
if resp.headers['Content-Type'].startswith('audio/'):
|
|
||||||
with open(output_file, 'wb') as f:
|
|
||||||
f.write(resp.content)
|
|
||||||
return output_file
|
|
||||||
else:
|
|
||||||
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"{__name__} error: {e}")
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import os
|
|
||||||
import uuid
|
|
||||||
import edge_tts
|
|
||||||
from datetime import datetime
|
|
||||||
from core.providers.tts.base import TTSProviderBase
|
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
|
||||||
def __init__(self, config, delete_audio_file):
|
|
||||||
super().__init__(config, delete_audio_file)
|
|
||||||
self.voice = config.get("voice")
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".mp3"):
|
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
|
||||||
communicate = edge_tts.Communicate(text, voice=self.voice) # Use your preferred voice
|
|
||||||
await communicate.save(output_file)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import random
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
class AuthCodeGenerator:
|
|
||||||
_instance = None
|
|
||||||
_instance_lock = threading.Lock()
|
|
||||||
|
|
||||||
def __new__(cls):
|
|
||||||
if not cls._instance:
|
|
||||||
with cls._instance_lock:
|
|
||||||
if not cls._instance:
|
|
||||||
cls._instance = super(AuthCodeGenerator, cls).__new__(cls)
|
|
||||||
# 初始化随机种子
|
|
||||||
random.seed(time.time())
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# 确保 __init__ 只被调用一次
|
|
||||||
if not hasattr(self, '_initialized'):
|
|
||||||
self._used_codes: Set[str] = set()
|
|
||||||
self._code_timestamps = {}
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self._code_timeout = 3 * 24 * 60 * 60
|
|
||||||
self._initialized = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
"""获取AuthCodeGenerator的单例实例"""
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
def generate_code(self) -> str:
|
|
||||||
"""
|
|
||||||
生成6位数字认证码,确保不重复
|
|
||||||
返回: 6位数字字符串
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
self._clean_expired_codes() # 清理过期code
|
|
||||||
while True:
|
|
||||||
# 使用时间戳和已用码数量作为种子,确保每次生成不同的随机数
|
|
||||||
seed = int(time.time() * 1000) + len(self._used_codes)
|
|
||||||
random.seed(seed)
|
|
||||||
|
|
||||||
# 生成6位随机数字
|
|
||||||
code = ''.join(str(random.randint(0, 9)) for _ in range(6))
|
|
||||||
|
|
||||||
# 检查是否已存在
|
|
||||||
if code not in self._used_codes:
|
|
||||||
self._used_codes.add(code)
|
|
||||||
self._code_timestamps[code] = time.time()
|
|
||||||
return code
|
|
||||||
|
|
||||||
def remove_code(self, code: str) -> bool:
|
|
||||||
"""
|
|
||||||
删除已使用的认证码
|
|
||||||
参数:
|
|
||||||
code: 要删除的认证码
|
|
||||||
返回:
|
|
||||||
bool: 删除成功返回True,码不存在返回False
|
|
||||||
"""
|
|
||||||
print('remove_code', code)
|
|
||||||
with self._lock:
|
|
||||||
if code in self._used_codes:
|
|
||||||
self._used_codes.remove(code)
|
|
||||||
if code in self._code_timestamps:
|
|
||||||
del self._code_timestamps[code]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_code_used(self, code: str) -> bool:
|
|
||||||
"""
|
|
||||||
检查认证码是否已被使用
|
|
||||||
参数:
|
|
||||||
code: 要检查的认证码
|
|
||||||
返回:
|
|
||||||
bool: 如果码存在返回True,否则返回False
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
return code in self._used_codes
|
|
||||||
|
|
||||||
def clear_codes(self):
|
|
||||||
"""清空所有已使用的认证码"""
|
|
||||||
with self._lock:
|
|
||||||
self._used_codes.clear()
|
|
||||||
self._code_timestamps.clear()
|
|
||||||
|
|
||||||
def _clean_expired_codes(self):
|
|
||||||
"""清理过期的认证码"""
|
|
||||||
current_time = time.time()
|
|
||||||
expired_codes = [
|
|
||||||
code for code, timestamp in self._code_timestamps.items()
|
|
||||||
if (current_time - timestamp) > self._code_timeout
|
|
||||||
]
|
|
||||||
for code in expired_codes:
|
|
||||||
self._used_codes.remove(code)
|
|
||||||
del self._code_timestamps[code]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from typing import List, Dict
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
def __init__(self, role: str, content: str = None, uniq_id: str = None):
|
|
||||||
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
|
||||||
self.role = role
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
|
|
||||||
class Dialogue:
|
|
||||||
def __init__(self):
|
|
||||||
self.dialogue: List[Message] = []
|
|
||||||
# 获取当前时间
|
|
||||||
self.current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
def put(self, message: Message):
|
|
||||||
self.dialogue.append(message)
|
|
||||||
|
|
||||||
def get_llm_dialogue(self) -> List[Dict[str, str]]:
|
|
||||||
dialogue = []
|
|
||||||
for m in self.dialogue:
|
|
||||||
dialogue.append({"role": m.role, "content": m.content})
|
|
||||||
return dialogue
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import asyncio
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
# 添加项目根目录到Python路径
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
project_root = os.path.abspath(os.path.join(current_dir, "..", ".."))
|
|
||||||
sys.path.insert(0, project_root)
|
|
||||||
|
|
||||||
from config.logger import setup_logging
|
|
||||||
import importlib
|
|
||||||
from datetime import datetime
|
|
||||||
from core.utils.util import is_segment
|
|
||||||
from core.utils.util import get_string_no_punctuation_or_emoji
|
|
||||||
from core.utils.util import read_config, get_project_dir
|
|
||||||
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
def create_instance(class_name, *args, **kwargs):
|
|
||||||
# 创建LLM实例
|
|
||||||
if os.path.exists(os.path.join('core', 'providers', 'llm', class_name, f'{class_name}.py')):
|
|
||||||
lib_name = f'core.providers.llm.{class_name}.{class_name}'
|
|
||||||
if lib_name not in sys.modules:
|
|
||||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
|
||||||
return sys.modules[lib_name].LLMProvider(*args, **kwargs)
|
|
||||||
|
|
||||||
raise ValueError(f"不支持的LLM类型: {class_name},请检查该配置的type是否设置正确")
|
|
||||||
|
|
||||||
|
|
||||||
async def test_single_model(llm_name: str, llm_config: Dict[str, Any], test_prompt: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""异步测试单个模型"""
|
|
||||||
try:
|
|
||||||
# 获取实际的LLM类型
|
|
||||||
llm_type = llm_config["type"] if "type" in llm_config else llm_name
|
|
||||||
llm = create_instance(llm_type, llm_config)
|
|
||||||
|
|
||||||
# 开始测试
|
|
||||||
dialogue = []
|
|
||||||
dialogue.append({"role": "system", "content": config.get("prompt")})
|
|
||||||
dialogue.append({"role": "user", "content": test_prompt})
|
|
||||||
|
|
||||||
start_time = datetime.now()
|
|
||||||
llm_responses = llm.response("test", dialogue)
|
|
||||||
response_message = []
|
|
||||||
first_response_time = None
|
|
||||||
total_response_time = None
|
|
||||||
start = 0
|
|
||||||
full_response = ""
|
|
||||||
|
|
||||||
for content in llm_responses:
|
|
||||||
response_message.append(content)
|
|
||||||
full_response += content
|
|
||||||
|
|
||||||
if is_segment(response_message):
|
|
||||||
segment_text = "".join(response_message[start:])
|
|
||||||
segment_text = get_string_no_punctuation_or_emoji(segment_text)
|
|
||||||
if len(segment_text) > 0:
|
|
||||||
if first_response_time is None:
|
|
||||||
first_response_time = (datetime.now() - start_time).total_seconds()
|
|
||||||
start = len(response_message)
|
|
||||||
|
|
||||||
total_response_time = (datetime.now() - start_time).total_seconds()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"name": llm_name,
|
|
||||||
"type": llm_type,
|
|
||||||
"first_response_time": first_response_time,
|
|
||||||
"total_response_time": total_response_time,
|
|
||||||
"response_length": len(full_response),
|
|
||||||
"status": "成功",
|
|
||||||
"response": full_response
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"测试 {llm_name} 时发生错误: {str(e)}")
|
|
||||||
return {
|
|
||||||
"name": llm_name,
|
|
||||||
"type": llm_config.get("type", llm_name),
|
|
||||||
"first_response_time": None,
|
|
||||||
"total_response_time": None,
|
|
||||||
"response_length": 0,
|
|
||||||
"status": f"失败 - {str(e)}",
|
|
||||||
"response": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""
|
|
||||||
LLM模型响应速度测试和排行(异步版本)
|
|
||||||
"""
|
|
||||||
config = read_config(get_project_dir() + "config.yaml")
|
|
||||||
test_prompt = "你好小智"
|
|
||||||
|
|
||||||
print("开始并发测试所有模型...")
|
|
||||||
|
|
||||||
# 创建所有模型的测试任务
|
|
||||||
tasks = []
|
|
||||||
for llm_name, llm_config in config["LLM"].items():
|
|
||||||
task = asyncio.create_task(test_single_model(llm_name, llm_config, test_prompt, config))
|
|
||||||
tasks.append(task)
|
|
||||||
|
|
||||||
# 等待所有测试完成
|
|
||||||
test_results = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
# 打印测试结果排行榜
|
|
||||||
print("\n========= LLM模型性能测试排行榜 =========")
|
|
||||||
print("测试提示词:", test_prompt)
|
|
||||||
|
|
||||||
# 过滤出成功的结果,并确保数值有效
|
|
||||||
successful_results = [r for r in test_results if r["status"] == "成功" and r["first_response_time"] is not None]
|
|
||||||
|
|
||||||
if successful_results:
|
|
||||||
print("\n1. 首次响应时间排行:")
|
|
||||||
sorted_by_first = sorted(successful_results, key=lambda x: x["first_response_time"])
|
|
||||||
for i, result in enumerate(sorted_by_first, 1):
|
|
||||||
print(f"{i}. {result['name']}({result['type']}) - {result['first_response_time']:.2f}秒")
|
|
||||||
print(f" 响应内容: {result['response'][:50]}...") # 只显示前50个字符
|
|
||||||
|
|
||||||
print("\n2. 总响应时间排行:")
|
|
||||||
sorted_by_total = sorted(successful_results, key=lambda x: x["total_response_time"] or float('inf'))
|
|
||||||
for i, result in enumerate(sorted_by_total, 1):
|
|
||||||
if result["total_response_time"] is not None:
|
|
||||||
print(f"{i}. {result['name']}({result['type']}) - {result['total_response_time']:.2f}秒")
|
|
||||||
|
|
||||||
print("\n3. 响应长度比较:")
|
|
||||||
sorted_by_length = sorted(successful_results, key=lambda x: x["response_length"], reverse=True)
|
|
||||||
for i, result in enumerate(sorted_by_length, 1):
|
|
||||||
print(f"{i}. {result['name']}({result['type']}) - {result['response_length']}字符")
|
|
||||||
else:
|
|
||||||
print("\n没有成功完成测试的模型。")
|
|
||||||
|
|
||||||
if len(test_results) != len(successful_results):
|
|
||||||
print("\n测试失败的模型:")
|
|
||||||
failed_results = [r for r in test_results if r["status"] != "成功" or r["first_response_time"] is None]
|
|
||||||
for result in failed_results:
|
|
||||||
print(f"- {result['name']}({result['type']}): {result['status']}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 运行异步主函数
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import Dict
|
|
||||||
from config.logger import setup_logging
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
class FileLockManager:
|
|
||||||
_instance = None
|
|
||||||
_locks: Dict[str, asyncio.Lock] = {}
|
|
||||||
|
|
||||||
def __new__(cls):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super(FileLockManager, cls).__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_lock(cls, file_path: str) -> asyncio.Lock:
|
|
||||||
"""获取指定文件的锁"""
|
|
||||||
if file_path not in cls._locks:
|
|
||||||
cls._locks[file_path] = asyncio.Lock()
|
|
||||||
return cls._locks[file_path]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def acquire_lock(cls, file_path: str):
|
|
||||||
"""获取锁"""
|
|
||||||
lock = cls.get_lock(file_path)
|
|
||||||
await lock.acquire()
|
|
||||||
logger.bind(tag=TAG).debug(f"Acquired lock for {file_path}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def release_lock(cls, file_path: str):
|
|
||||||
"""释放锁"""
|
|
||||||
if file_path in cls._locks:
|
|
||||||
try:
|
|
||||||
cls._locks[file_path].release()
|
|
||||||
logger.bind(tag=TAG).debug(f"Released lock for {file_path}")
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.bind(tag=TAG).warning(f"Failed to release lock for {file_path}: {e}")
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from config.logger import setup_logging
|
|
||||||
import importlib
|
|
||||||
from datetime import datetime
|
|
||||||
from core.utils.util import read_config, get_project_dir
|
|
||||||
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
def create_instance(class_name, *args, **kwargs):
|
|
||||||
# 创建TTS实例
|
|
||||||
if os.path.exists(os.path.join('core', 'providers', 'tts', f'{class_name}.py')):
|
|
||||||
lib_name = f'core.providers.tts.{class_name}'
|
|
||||||
if lib_name not in sys.modules:
|
|
||||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
|
||||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
|
||||||
|
|
||||||
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
"""
|
|
||||||
响应速度测试
|
|
||||||
"""
|
|
||||||
config = read_config(get_project_dir() + "config.yaml")
|
|
||||||
tts = create_instance(
|
|
||||||
config["selected_module"]["TTS"]
|
|
||||||
if not 'type' in config["TTS"][config["selected_module"]["TTS"]]
|
|
||||||
else
|
|
||||||
config["TTS"][config["selected_module"]["TTS"]]["type"],
|
|
||||||
config["TTS"][config["selected_module"]["TTS"]],
|
|
||||||
config["delete_audio"]
|
|
||||||
)
|
|
||||||
tts.output_file = get_project_dir() + tts.output_file
|
|
||||||
start = datetime.now()
|
|
||||||
file_path = tts.to_tts("你好,测试,我是人工智能小智")
|
|
||||||
print("语音合成耗时:" + str(datetime.now() - start))
|
|
||||||
start = datetime.now()
|
|
||||||
tts.wav_to_opus_data(file_path)
|
|
||||||
print("语音opus耗时:" + str(datetime.now() - start))
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import yaml
|
|
||||||
import socket
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_dir():
|
|
||||||
"""获取项目根目录"""
|
|
||||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + '/'
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip():
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
# Connect to Google's DNS servers
|
|
||||||
s.connect(("8.8.8.8", 80))
|
|
||||||
local_ip = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
return local_ip
|
|
||||||
except Exception as e:
|
|
||||||
return "127.0.0.1"
|
|
||||||
|
|
||||||
|
|
||||||
def read_config(config_path):
|
|
||||||
with open(config_path, "r", encoding="utf-8") as file:
|
|
||||||
config = yaml.safe_load(file)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def write_json_file(file_path, data):
|
|
||||||
"""将数据写入 JSON 文件"""
|
|
||||||
with open(file_path, 'w', encoding='utf-8') as file:
|
|
||||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
||||||
|
|
||||||
|
|
||||||
def is_segment(tokens):
|
|
||||||
if tokens[-1] in (",", ".", "?", ",", "。", "?", "!", "!", ";", ";", ":", ":"):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_punctuation_or_emoji(char):
|
|
||||||
"""检查字符是否为空格、指定标点或表情符号"""
|
|
||||||
# 定义需要去除的中英文标点(包括全角/半角)
|
|
||||||
punctuation_set = {
|
|
||||||
',', ',', # 中文逗号 + 英文逗号
|
|
||||||
'。', '.', # 中文句号 + 英文句号
|
|
||||||
'!', '!', # 中文感叹号 + 英文感叹号
|
|
||||||
'-', '-', # 英文连字符 + 中文全角横线
|
|
||||||
'、' # 中文顿号
|
|
||||||
}
|
|
||||||
if char.isspace() or char in punctuation_set:
|
|
||||||
return True
|
|
||||||
# 检查表情符号(保留原有逻辑)
|
|
||||||
code_point = ord(char)
|
|
||||||
emoji_ranges = [
|
|
||||||
(0x1F600, 0x1F64F), (0x1F300, 0x1F5FF),
|
|
||||||
(0x1F680, 0x1F6FF), (0x1F900, 0x1F9FF),
|
|
||||||
(0x1FA70, 0x1FAFF), (0x2600, 0x26FF),
|
|
||||||
(0x2700, 0x27BF)
|
|
||||||
]
|
|
||||||
return any(start <= code_point <= end for start, end in emoji_ranges)
|
|
||||||
|
|
||||||
|
|
||||||
def get_string_no_punctuation_or_emoji(s):
|
|
||||||
"""去除字符串首尾的空格、标点符号和表情符号"""
|
|
||||||
chars = list(s)
|
|
||||||
# 处理开头的字符
|
|
||||||
start = 0
|
|
||||||
while start < len(chars) and is_punctuation_or_emoji(chars[start]):
|
|
||||||
start += 1
|
|
||||||
# 处理结尾的字符
|
|
||||||
end = len(chars) - 1
|
|
||||||
while end >= start and is_punctuation_or_emoji(chars[end]):
|
|
||||||
end -= 1
|
|
||||||
return ''.join(chars[start:end + 1])
|
|
||||||
|
|
||||||
|
|
||||||
def remove_punctuation_and_length(text):
|
|
||||||
# 全角符号和半角符号的Unicode范围
|
|
||||||
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
|
|
||||||
half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
|
||||||
space = ' ' # 半角空格
|
|
||||||
full_width_space = ' ' # 全角空格
|
|
||||||
|
|
||||||
# 去除全角和半角符号以及空格
|
|
||||||
result = ''.join([char for char in text if
|
|
||||||
char not in full_width_punctuations and char not in half_width_punctuations and char not in space and char not in full_width_space])
|
|
||||||
|
|
||||||
if result == "Yeah":
|
|
||||||
return 0, ""
|
|
||||||
return len(result), result
|
|
||||||
|
|
||||||
|
|
||||||
def check_password(password):
|
|
||||||
"""
|
|
||||||
检查密码是否满足以下条件:
|
|
||||||
1. 密码长度大于八位。
|
|
||||||
2. 密码包含英文和数字。
|
|
||||||
3. 密码不能包含“xiaozhi”字符。
|
|
||||||
|
|
||||||
:param password: 要检查的密码
|
|
||||||
:return: 如果密码满足条件,则返回True;否则返回False。
|
|
||||||
"""
|
|
||||||
# 检查密码长度
|
|
||||||
if len(password) < 8:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查是否包含英文字符和数字
|
|
||||||
if not re.search(r'[A-Za-z]', password) or not re.search(r'[0-9]', password):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查是否包含“xiaozhi”字符
|
|
||||||
if "xiaozhi" in password:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if "1234" in password:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 如果满足所有条件,则返回True
|
|
||||||
return True
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import websockets
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from core.connection import ConnectionHandler
|
|
||||||
from core.handle.musicHandler import MusicHandler
|
|
||||||
from core.utils.util import get_local_ip
|
|
||||||
from core.utils import asr, vad, llm, tts
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketServer:
|
|
||||||
def __init__(self, config: dict):
|
|
||||||
self.config = config
|
|
||||||
self.logger = setup_logging()
|
|
||||||
self._vad, self._asr, self._llm, self._tts, self._music = self._create_processing_instances()
|
|
||||||
self.active_connections = set() # 添加全局连接记录
|
|
||||||
|
|
||||||
def _create_processing_instances(self):
|
|
||||||
"""创建处理模块实例"""
|
|
||||||
return (
|
|
||||||
vad.create_instance(
|
|
||||||
self.config["selected_module"]["VAD"],
|
|
||||||
self.config["VAD"][self.config["selected_module"]["VAD"]]
|
|
||||||
),
|
|
||||||
asr.create_instance(
|
|
||||||
self.config["selected_module"]["ASR"]
|
|
||||||
if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
|
||||||
else
|
|
||||||
self.config["ASR"][self.config["selected_module"]["ASR"]]["type"],
|
|
||||||
self.config["ASR"][self.config["selected_module"]["ASR"]],
|
|
||||||
self.config["delete_audio"]
|
|
||||||
),
|
|
||||||
llm.create_instance(
|
|
||||||
self.config["selected_module"]["LLM"]
|
|
||||||
if not 'type' in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
|
||||||
else
|
|
||||||
self.config["LLM"][self.config["selected_module"]["LLM"]]['type'],
|
|
||||||
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
|
||||||
),
|
|
||||||
tts.create_instance(
|
|
||||||
self.config["selected_module"]["TTS"]
|
|
||||||
if not 'type' in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
|
||||||
else
|
|
||||||
self.config["TTS"][self.config["selected_module"]["TTS"]]["type"],
|
|
||||||
self.config["TTS"][self.config["selected_module"]["TTS"]],
|
|
||||||
self.config["delete_audio"]
|
|
||||||
),
|
|
||||||
MusicHandler(self.config)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
server_config = self.config["server"]
|
|
||||||
host = server_config["ip"]
|
|
||||||
port = server_config["port"]
|
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
|
||||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
|
||||||
async with websockets.serve(
|
|
||||||
self._handle_connection,
|
|
||||||
host,
|
|
||||||
port
|
|
||||||
):
|
|
||||||
await asyncio.Future()
|
|
||||||
|
|
||||||
async def _handle_connection(self, websocket):
|
|
||||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
|
||||||
# 创建ConnectionHandler时传入当前server实例
|
|
||||||
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._music)
|
|
||||||
self.active_connections.add(handler)
|
|
||||||
try:
|
|
||||||
await handler.handle_connection(websocket)
|
|
||||||
finally:
|
|
||||||
self.active_connections.discard(handler)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
xiaozhi-esp32-server:
|
|
||||||
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
|
||||||
container_name: xiaozhi-esp32-server
|
|
||||||
restart: always
|
|
||||||
security_opt:
|
|
||||||
- seccomp:unconfined
|
|
||||||
ports:
|
|
||||||
# ws服务端
|
|
||||||
- "8000:8000"
|
|
||||||
# 管理后台
|
|
||||||
- "8002:8002"
|
|
||||||
volumes:
|
|
||||||
# 配置文件目录
|
|
||||||
- ./data:/app/data
|
|
||||||
# 模型文件挂接,很重要
|
|
||||||
- ./models/SenseVoiceSmall/model.pt:/app/models/SenseVoiceSmall/model.pt
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# 本文件是用于一键自动下载本项目所需文件,自动创建好目录
|
||||||
|
# 所需条件(否则无法使用):
|
||||||
|
# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本
|
||||||
|
#
|
||||||
|
# 检测操作系统类型
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) OS=Linux;;
|
||||||
|
Darwin*) OS=Mac;;
|
||||||
|
CYGWIN*) OS=Windows;;
|
||||||
|
MINGW*) OS=Windows;;
|
||||||
|
MSYS*) OS=Windows;;
|
||||||
|
*) OS=UNKNOWN;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 设置颜色(Windows CMD 不支持,但不影响使用)
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
GREEN=""
|
||||||
|
RED=""
|
||||||
|
NC=""
|
||||||
|
else
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}开始安装小智服务端...${NC}"
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
echo "创建目录结构..."
|
||||||
|
mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall
|
||||||
|
cd xiaozhi-server || exit
|
||||||
|
|
||||||
|
# 根据操作系统选择下载命令
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri"
|
||||||
|
DOWNLOAD_CMD_SUFFIX="-OutFile"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="wget -O"
|
||||||
|
else
|
||||||
|
echo "${RED}错误: 需要安装 curl 或 wget${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载语音识别模型
|
||||||
|
echo "下载语音识别模型..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "${RED}模型下载失败。请手动从以下地址下载:${NC}"
|
||||||
|
echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)"
|
||||||
|
echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载配置文件
|
||||||
|
echo "下载配置文件..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
echo "检查文件完整性..."
|
||||||
|
FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt"
|
||||||
|
ALL_FILES_EXIST=true
|
||||||
|
|
||||||
|
for FILE in $FILES_TO_CHECK; do
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "${RED}错误: $FILE 不存在${NC}"
|
||||||
|
ALL_FILES_EXIST=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ALL_FILES_EXIST" = false ]; then
|
||||||
|
echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}文件下载完成!${NC}"
|
||||||
|
echo "请编辑 data/.config.yaml 文件配置你的API密钥。"
|
||||||
|
echo "配置完成后,运行以下命令启动服务:"
|
||||||
|
echo "${GREEN}docker-compose up -d${NC}"
|
||||||
|
echo "查看日志请运行:"
|
||||||
|
echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}"
|
||||||
|
|
||||||
|
# 提示用户编辑配置文件
|
||||||
|
echo "\n${RED}重要提示:${NC}"
|
||||||
|
echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥"
|
||||||
|
echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置"
|
||||||
|
echo "3. 配置完成后再启动 docker 服务"
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# 方式一:docker快速部署
|
# 部署架构图
|
||||||
|

|
||||||
|
# 方式一:Docker只运行Server
|
||||||
|
|
||||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
|
|
||||||
@@ -6,48 +8,90 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
|||||||
|
|
||||||
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||||
|
|
||||||
## 2. 创建目录
|
如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。
|
||||||
|
|
||||||
安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录。
|
### 1.1 懒人脚本
|
||||||
|
|
||||||
创建好目录后,你需要在`项目目录`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
你可以使用以下命令一键下载并执行部署脚本:
|
||||||
|
请确保你的环境可以正常访问 GitHub 否则无法下载脚本。
|
||||||
|
```bash
|
||||||
|
curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令:
|
||||||
|
```bash
|
||||||
|
# Git Bash 或 WSL
|
||||||
|
sh docker-setup.sh
|
||||||
|
# PowerShell 或 CMD
|
||||||
|
.\docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令:
|
||||||
|
```bash
|
||||||
|
chmod +x docker-setup.sh
|
||||||
|
./docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动完成以下操作:
|
||||||
|
> 1. 创建必要的目录结构
|
||||||
|
> 2. 下载语音识别模型
|
||||||
|
> 3. 下载配置文件
|
||||||
|
> 4. 检查文件完整性
|
||||||
|
>
|
||||||
|
> 执行完成后,请按照提示配置 API 密钥。
|
||||||
|
|
||||||
|
当你一切顺利完成以上操作后,继续操作[配置项目文件](#2-配置项目文件)
|
||||||
|
|
||||||
|
### 1.2 手动部署
|
||||||
|
|
||||||
|
如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。
|
||||||
|
|
||||||
|
#### 1.2.1 创建目录
|
||||||
|
|
||||||
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
|
创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
||||||
|
|
||||||
最终目录结构如下所示:
|
最终目录结构如下所示:
|
||||||
|
|
||||||
```
|
```
|
||||||
你的项目根目录
|
xiaozhi-server
|
||||||
├─ data
|
├─ data
|
||||||
├─ models
|
├─ models
|
||||||
├─ SenseVoiceSmall
|
├─ SenseVoiceSmall
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 下载语音识别模型文件
|
#### 1.2.2 下载语音识别模型文件
|
||||||
|
|
||||||
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
||||||
[跳转到下载语音识别模型文件](#模型文件)
|
[跳转到下载语音识别模型文件](#模型文件)
|
||||||
|
|
||||||
下载完后,回到本教程。
|
下载完后,回到本教程。
|
||||||
|
|
||||||
## 3. 下载docker-compose.yaml
|
#### 1.2.3 下载配置文件
|
||||||
|
|
||||||
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docker-compose.yml)。
|
你需要下载两个配置文件:`docker-compose.yaml` 和 `config.yaml`。需要从项目仓库下载这两个文件。
|
||||||
|
|
||||||
|
##### 1.2.3.1 下载 docker-compose.yaml
|
||||||
|
|
||||||
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||||
|
|
||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
|
||||||
`项目目录`中。
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
## 3. 下载配置文件
|
##### 1.2.3.2 下载 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
|
||||||
`项目目录`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`。
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
下载完配置文件后,我们确认一下整个`项目目录`里面的文件如下所示:
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
```
|
```
|
||||||
你的项目根目录
|
xiaozhi-server
|
||||||
├─ docker-compose.yml
|
├─ docker-compose.yml
|
||||||
├─ data
|
├─ data
|
||||||
├─ .config.yaml
|
├─ .config.yaml
|
||||||
@@ -58,16 +102,16 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 4. 配置项目文件
|
## 2. 配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
配置完项目文件后,回到本教程继续往下。
|
配置完项目文件后,回到本教程继续往下。
|
||||||
|
|
||||||
## 5. 执行docker命令
|
## 3. 执行docker命令
|
||||||
|
|
||||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`项目目录`,执行以下命令
|
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -81,92 +125,58 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
|
|
||||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||||
|
|
||||||
## 6.版本升级操作
|
## 5. 版本升级操作
|
||||||
|
|
||||||
如果后期想升级版本,可以这么操作
|
如果后期想升级版本,可以这么操作
|
||||||
|
|
||||||
1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
||||||
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
||||||
|
|
||||||
2、执行以下命令
|
5.2、执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
|
|
||||||
# 方式二:借助Docker环境运行部署
|
# 方式二:本地源码只运行Server
|
||||||
|
|
||||||
开发人员如果不想安装`conda`环境,可以使用这种方法管理好依赖。
|
|
||||||
|
|
||||||
## 1.克隆项目
|
|
||||||
|
|
||||||
## 2.[跳转到下载语音识别模型文件](#模型文件)
|
|
||||||
|
|
||||||
## 3.[跳转到配置项目文件](#配置项目)
|
|
||||||
|
|
||||||
## 4.运行docker
|
|
||||||
|
|
||||||
修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
|
|
||||||
-p 8000:8000 \
|
|
||||||
-p 8002:8002 \
|
|
||||||
-v ./:/app \
|
|
||||||
kalicyh/poetry:v3.10_xiaozhi
|
|
||||||
```
|
|
||||||
|
|
||||||
然后就和正常开发一样了
|
|
||||||
|
|
||||||
## 5.安装依赖
|
|
||||||
|
|
||||||
在刚刚的打开的终端运行
|
|
||||||
|
|
||||||
```sh
|
|
||||||
poetry install --no-root
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
速度慢可以尝试使用清华镜像
|
|
||||||
|
|
||||||
```sh
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6.运行项目
|
|
||||||
|
|
||||||
```sh
|
|
||||||
poetry run python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
# 方式三:本地源码运行
|
|
||||||
|
|
||||||
## 1.安装基础环境
|
## 1.安装基础环境
|
||||||
|
|
||||||
本项目使用`conda`管理依赖环境。如果不方便安装`conda`,需要根据实际的操作系统安装好`libopus`和`ffmpeg`。
|
本项目使用`conda`管理依赖环境。如果不方便安装`conda`,需要根据实际的操作系统安装好`libopus`和`ffmpeg`。
|
||||||
如果确定使用`conda`,则安装好后,开始执行以下命令。
|
如果确定使用`conda`,则安装好后,开始执行以下命令。
|
||||||
|
|
||||||
|
重要提示!windows 用户,可以通过安装`Anaconda`来管理环境。安装好`Anaconda`后,在`开始`那里搜索`anaconda`相关的关键词,
|
||||||
|
找到`Anaconda Prpmpt`,使用管理员身份运行它。如下图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
运行之后,如果你能看到命令行窗口前面有一个(base)字样,说明你成功进入了`conda`环境。那么你就可以执行以下命令了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
```
|
```
|
||||||
conda remove -n xiaozhi-esp32-server --all -y
|
conda remove -n xiaozhi-esp32-server --all -y
|
||||||
conda create -n xiaozhi-esp32-server python=3.10 -y
|
conda create -n xiaozhi-esp32-server python=3.10 -y
|
||||||
conda activate xiaozhi-esp32-server
|
conda activate xiaozhi-esp32-server
|
||||||
conda install conda-forge::libopus -y
|
|
||||||
conda install conda-forge::ffmpeg -y
|
# 添加清华源通道
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge
|
||||||
|
|
||||||
|
conda install libopus -y
|
||||||
|
conda install ffmpeg -y
|
||||||
```
|
```
|
||||||
|
|
||||||
|
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
|
||||||
|
|
||||||
## 2.安装本项目依赖
|
## 2.安装本项目依赖
|
||||||
|
|
||||||
你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
|
你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
|
||||||
@@ -176,11 +186,13 @@ conda install conda-forge::ffmpeg -y
|
|||||||
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
|
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
|
||||||
|
|
||||||
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
|
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
|
||||||
你需要把它重命名成`xiaozhi-esp32-server`,好了请记住这个目录,我们暂且称它为`项目目录`。
|
你需要把它重命名成`xiaozhi-esp32-server`,在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`。
|
||||||
|
|
||||||
```
|
```
|
||||||
# 使用dos或者终端,进入到你的项目目录,执行以下命令
|
# 继续使用conda环境
|
||||||
conda activate xiaozhi-esp32-server
|
conda activate xiaozhi-esp32-server
|
||||||
|
# 进入到你的项目根目录,再进入main/xiaozhi-server
|
||||||
|
cd main/xiaozhi-server
|
||||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
@@ -194,39 +206,43 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
## 4.配置项目文件
|
## 4.配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下来,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
## 5.运行项目
|
## 5.运行项目
|
||||||
|
|
||||||
```
|
```
|
||||||
# 确保在本项目的根目录下执行
|
# 确保在xiaozhi-server目录下执行
|
||||||
conda activate xiaozhi-esp32-server
|
conda activate xiaozhi-esp32-server
|
||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||||
|
|
||||||
|
|
||||||
# 汇总
|
# 汇总
|
||||||
|
|
||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`项目目录`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
修改`项目目录`下`data`目录下的`.config.yaml`文件,配置本项目所需的各种参数。默认的LLM使用的是`ChatGLMLLM`
|
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||||
,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
|
||||||
默认的TTS使用的是`EdgeTTS`,这个无需配置,如果你需要更换成`豆包TTS`,则需要配置密钥。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||||
|
|
||||||
```
|
```
|
||||||
selected_module:
|
selected_module:
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
VAD: SileroVAD
|
||||||
|
ASR: FunASR
|
||||||
LLM: ChatGLMLLM
|
LLM: ChatGLMLLM
|
||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
|
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||||
|
Memory: nomem
|
||||||
|
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||||
|
Intent: nointent
|
||||||
```
|
```
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||||
@@ -245,12 +261,10 @@ LLM:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
|
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
文件放在`model/SenseVoiceSmall`
|
文件放在`models/SenseVoiceSmall`
|
||||||
目录下。下面两个下载路线任选一个。
|
目录下。下面两个下载路线任选一个。
|
||||||
|
|
||||||
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
||||||
@@ -262,16 +276,40 @@ LLM:
|
|||||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
```
|
```
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
|
||||||
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||||
|
|
||||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。
|
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/`。
|
||||||
|
|
||||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# 部署架构图
|
||||||
|

|
||||||
|
# 方式一:Docker运行全模块
|
||||||
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
|
|
||||||
|
## 1. 安装docker
|
||||||
|
|
||||||
|
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||||
|
|
||||||
|
#### 1.1 创建目录
|
||||||
|
|
||||||
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
|
创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
||||||
|
|
||||||
|
最终目录结构如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
xiaozhi-server
|
||||||
|
├─ data
|
||||||
|
├─ models
|
||||||
|
├─ SenseVoiceSmall
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 下载语音识别模型文件
|
||||||
|
|
||||||
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
|
文件放在`models/SenseVoiceSmall`
|
||||||
|
目录下。下面两个下载路线任选一个。
|
||||||
|
|
||||||
|
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
||||||
|
- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
|
||||||
|
`qvna`
|
||||||
|
|
||||||
|
|
||||||
|
#### 1.3 下载配置文件
|
||||||
|
|
||||||
|
你需要下载两个配置文件:`docker-compose_all.yaml` 和 `config_from_api.yaml`。需要从项目仓库下载这两个文件。
|
||||||
|
|
||||||
|
##### 1.3.1 下载 docker-compose_all.yaml
|
||||||
|
|
||||||
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose_all.yml)。
|
||||||
|
|
||||||
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
||||||
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
|
||||||
|
|
||||||
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
|
##### 1.3.2 下载 config_from_api.yaml
|
||||||
|
|
||||||
|
用浏览器打开[这个链接](../main/xiaozhi-server/config_from_api.yaml)。
|
||||||
|
|
||||||
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
||||||
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
|
||||||
|
|
||||||
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
xiaozhi-server
|
||||||
|
├─ docker-compose_all.yml
|
||||||
|
├─ data
|
||||||
|
├─ .config.yaml
|
||||||
|
├─ models
|
||||||
|
├─ SenseVoiceSmall
|
||||||
|
├─ model.pt
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
|
## 2. 备份数据
|
||||||
|
|
||||||
|
如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
|
||||||
|
|
||||||
|
## 3. 清除历史版本镜像和容器
|
||||||
|
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml down
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server
|
||||||
|
docker rm xiaozhi-esp32-server
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-db
|
||||||
|
docker rm xiaozhi-esp32-server-db
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-redis
|
||||||
|
docker rm xiaozhi-esp32-server-redis
|
||||||
|
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 运行程序
|
||||||
|
执行以下命令启动新版本容器
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
执行完后,再执行以下命令,查看日志信息。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker logs -f xiaozhi-esp32-server-web
|
||||||
|
```
|
||||||
|
|
||||||
|
当你看到输出日志时,说明你的`智控台`启动成功了。
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-xx-xx 22:11:12.445 [main] INFO c.a.d.s.b.a.DruidDataSourceAutoConfigure - Init DruidDataSource
|
||||||
|
2025-xx-xx 21:28:53.873 [main] INFO xiaozhi.AdminApplication - Started AdminApplication in 16.057 seconds (process running for 17.941)
|
||||||
|
http://localhost:8002/xiaozhi/doc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意此刻仅是`智控台`能运行,如果8000端口`xiaozhi-esp32-server`报错,先不要理会。
|
||||||
|
|
||||||
|
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
|
接下来要做三件重要的事情:
|
||||||
|
|
||||||
|
### 第一件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 你的server.secret值
|
||||||
|
```
|
||||||
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
|
||||||
|
2、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
3、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
4、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
类似这样的效果
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
|
```
|
||||||
|
|
||||||
|
保存好后,继续往下做第二件重要的事情
|
||||||
|
|
||||||
|
### 第二件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
## 5.重启xiaozhi-esp32-server
|
||||||
|
|
||||||
|
接下来打开命令行工具,使用`终端`或`命令行`工具 输入
|
||||||
|
```
|
||||||
|
docker restart xiaozhi-esp32-server
|
||||||
|
docker logs -f xiaozhi-esp32-server
|
||||||
|
```
|
||||||
|
如果你能看到,类似以下日志,则是Server启动成功的标志。
|
||||||
|
|
||||||
|
```
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - Websocket地址是 ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中。
|
||||||
|
|
||||||
|
OTA接口:
|
||||||
|
```
|
||||||
|
http://你电脑局域网的ip:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
Websocket接口:
|
||||||
|
```
|
||||||
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
|
||||||
|
# 方式二:本地源码运行全模块
|
||||||
|
|
||||||
|
## 1.安装MySQL数据库
|
||||||
|
|
||||||
|
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
如果还没有MySQL,你可以通过docker安装mysql
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.安装redis
|
||||||
|
|
||||||
|
如果还没有Redis,你可以通过docker安装redis
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.运行manager-api程序
|
||||||
|
|
||||||
|
3.1 安装JDK21,设置JDK环境变量
|
||||||
|
|
||||||
|
3.2 安装Maven,设置Maven环境变量
|
||||||
|
|
||||||
|
3.3 使用Vscode编程工具,安装好Java环境相关插件
|
||||||
|
|
||||||
|
3.4 使用Vscode编程工具加载manager-api模块
|
||||||
|
|
||||||
|
在`src/main/resources/application-dev.yml`中配置数据库连接信息
|
||||||
|
|
||||||
|
```
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
```
|
||||||
|
在`src/main/resources/application-dev.yml`中配置Redis连接信息
|
||||||
|
```
|
||||||
|
spring:
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
password:
|
||||||
|
database: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
3.5 运行主程序
|
||||||
|
|
||||||
|
本项目为SpringBoot项目,启动方式为:
|
||||||
|
打开`Application.java`运行`Main`方法启动
|
||||||
|
|
||||||
|
```
|
||||||
|
路径地址:
|
||||||
|
src/main/java/xiaozhi/AdminApplication.java
|
||||||
|
```
|
||||||
|
|
||||||
|
当你看到输出日志时,说明你的`manager-api`启动成功了。
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-xx-xx 22:11:12.445 [main] INFO c.a.d.s.b.a.DruidDataSourceAutoConfigure - Init DruidDataSource
|
||||||
|
2025-xx-xx 21:28:53.873 [main] INFO xiaozhi.AdminApplication - Started AdminApplication in 16.057 seconds (process running for 17.941)
|
||||||
|
http://localhost:8002/xiaozhi/doc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.运行manager-web程序
|
||||||
|
|
||||||
|
4.1 安装nodejs
|
||||||
|
|
||||||
|
4.2 使用Vscode编程工具加载manager-web模块
|
||||||
|
|
||||||
|
终端命令进入manager-web目录下
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
然后启动
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意,如果你的manager-api的接口不在`http://localhost:8002`,请在开发时,修改
|
||||||
|
`main/manager-web/.env.development`中的路径
|
||||||
|
|
||||||
|
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
## 5.安装Python环境
|
||||||
|
|
||||||
|
本项目使用`conda`管理依赖环境。如果不方便安装`conda`,需要根据实际的操作系统安装好`libopus`和`ffmpeg`。
|
||||||
|
如果确定使用`conda`,则安装好后,开始执行以下命令。
|
||||||
|
|
||||||
|
重要提示!windows 用户,可以通过安装`Anaconda`来管理环境。安装好`Anaconda`后,在`开始`那里搜索`anaconda`相关的关键词,
|
||||||
|
找到`Anaconda Prpmpt`,使用管理员身份运行它。如下图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
运行之后,如果你能看到命令行窗口前面有一个(base)字样,说明你成功进入了`conda`环境。那么你就可以执行以下命令了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
conda remove -n xiaozhi-esp32-server --all -y
|
||||||
|
conda create -n xiaozhi-esp32-server python=3.10 -y
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
|
||||||
|
# 添加清华源通道
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge
|
||||||
|
|
||||||
|
conda install libopus -y
|
||||||
|
conda install ffmpeg -y
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
|
||||||
|
|
||||||
|
## 6.安装本项目依赖
|
||||||
|
|
||||||
|
你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
|
||||||
|
|
||||||
|
你可以用浏览器打开这个地址`https://github.com/xinnan-tech/xiaozhi-esp32-server.git`
|
||||||
|
|
||||||
|
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
|
||||||
|
|
||||||
|
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
|
||||||
|
你需要把它重命名成`xiaozhi-esp32-server`,在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`。
|
||||||
|
|
||||||
|
```
|
||||||
|
# 继续使用conda环境
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
# 进入到你的项目根目录,再进入main/xiaozhi-server
|
||||||
|
cd main/xiaozhi-server
|
||||||
|
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.下载语音识别模型文件
|
||||||
|
|
||||||
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
|
文件放在`models/SenseVoiceSmall`
|
||||||
|
目录下。下面两个下载路线任选一个。
|
||||||
|
|
||||||
|
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
||||||
|
- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
|
||||||
|
`qvna`
|
||||||
|
|
||||||
|
## 8.配置项目文件
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 你的server.secret值
|
||||||
|
```
|
||||||
|
|
||||||
|
把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
|
||||||
|
类似这样的效果
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.运行项目
|
||||||
|
|
||||||
|
```
|
||||||
|
# 确保在xiaozhi-server目录下执行
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
|
```
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
由于你是全模块部署,因此你有两个重要的接口。
|
||||||
|
|
||||||
|
OTA接口:
|
||||||
|
```
|
||||||
|
http://你电脑局域网的ip:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
Websocket接口:
|
||||||
|
```
|
||||||
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
请你务必把以上两个接口地址写入到智控台中:他们将会影响websocket地址发放和自动升级功能。
|
||||||
|
|
||||||
|
1、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
|
2、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
|
|
||||||
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
# 常见问题
|
||||||
|
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# 常见问题 ❓
|
||||||
|
|
||||||
|
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||||
|
|
||||||
|
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
||||||
|
文件,如果没有就要下载,查看这里[下载语音识别模型文件](Deployment.md#模型文件)
|
||||||
|
|
||||||
|
### 2、为什么会出现"TTS 任务出错 文件不存在"?📁
|
||||||
|
|
||||||
|
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
||||||
|
|
||||||
|
如果没有安装,就安装
|
||||||
|
|
||||||
|
```
|
||||||
|
conda install conda-forge::libopus
|
||||||
|
conda install conda-forge::ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3、TTS 经常失败,经常超时 ⏰
|
||||||
|
|
||||||
|
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||||
|
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||||
|
|
||||||
|
### 4、使用Wifi能连接自建服务器,但是4G模式却接不上 🔐
|
||||||
|
|
||||||
|
原因:虾哥的固件,4G模式需要使用安全连接。
|
||||||
|
|
||||||
|
解决方法:目前有两种方法可以解决。任选一种:
|
||||||
|
|
||||||
|
1、改代码。参考这个视频解决 https://www.bilibili.com/video/BV18MfTYoE85
|
||||||
|
|
||||||
|
2、使用nginx配置ssl证书。参考教程 https://icnt94i5ctj4.feishu.cn/docx/GnYOdMNJOoRCljx1ctecsj9cnRe
|
||||||
|
|
||||||
|
### 5、如何提高小智对话响应速度? ⚡
|
||||||
|
|
||||||
|
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决"跑得动"的问题,再优化"跑得快"。
|
||||||
|
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||||
|
|
||||||
|
| 影响因素 | 因素值 |
|
||||||
|
|:-----:|:----------------:|
|
||||||
|
| 测试地点 | 广东省广州市海珠区 |
|
||||||
|
| 测试时间 | 2025年2月19日 12:52 |
|
||||||
|
| 宽带运营商 | 中国联通 |
|
||||||
|
|
||||||
|
测试方法:
|
||||||
|
|
||||||
|
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
|
||||||
|
|
||||||
|
2、配置完密钥后,执行以下方法
|
||||||
|
|
||||||
|
```
|
||||||
|
# 进入项目根目录,执行以下命令:
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
python performance_tester.py
|
||||||
|
```
|
||||||
|
|
||||||
|
生成报告如下
|
||||||
|
|
||||||
|
LLM 性能排行:
|
||||||
|
|
||||||
|
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|
||||||
|
|:-----------|:-----------|:--------|
|
||||||
|
| AliLLM | 0.547s | 1.485s |
|
||||||
|
| ChatGLMLLM | 0.677s | 3.057s |
|
||||||
|
|
||||||
|
TTS 性能排行:
|
||||||
|
|
||||||
|
| 模块名称 | 平均合成时间 |
|
||||||
|
|----------------------|--------|
|
||||||
|
| EdgeTTS | 1.019s |
|
||||||
|
| DoubaoTTS | 0.503s |
|
||||||
|
| CosyVoiceSiliconflow | 3.732s |
|
||||||
|
|
||||||
|
推荐配置组合 (综合响应速度):
|
||||||
|
|
||||||
|
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|
||||||
|
|-------------------------------|-------|-----------|--------|
|
||||||
|
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
|
||||||
|
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
|
||||||
|
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
|
||||||
|
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
|
||||||
|
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
|
||||||
|
|
||||||
|
### 结论 🔍
|
||||||
|
|
||||||
|
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
|
||||||
|
|
||||||
|
- LLM:`AliLLM`
|
||||||
|
- TTS:`DoubaoTTS`
|
||||||
|
|
||||||
|
### 6、我说话很慢,停顿时小智老是抢话 🗣️
|
||||||
|
|
||||||
|
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
VAD:
|
||||||
|
SileroVAD:
|
||||||
|
threshold: 0.5
|
||||||
|
model_dir: models/snakers4_silero-vad
|
||||||
|
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||||
|
|
||||||
|
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
|
||||||
|
|
||||||
|
### 8、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
|
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||||
|
|
||||||
|
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
||||||
|
|
||||||
|
- 硬件烧录QQ群:适用于硬件烧录问题
|
||||||
|
- 开源服务端交流群:适用于服务端问题
|
||||||
|
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 致开发者的公开信
|
||||||
|
|
||||||
|
"春江水暖鸭先知,正是河豚欲上时!"
|
||||||
|
|
||||||
|
亲爱的朋友,我是John,是一名普通公司里的Java程序员,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
||||||
|
|
||||||
|
半年前我看到很多优秀的项目,比如`Dify`、`Chat2DB`等人工智能相关的项目,我在想,我要是能参与这些项目多好,可惜“报国无门,空打十年代码”。
|
||||||
|
|
||||||
|
我是2025年初刷到虾哥团队的视频,我非常好奇他是怎么实现的,我想复刻他们的后端服务,打造一个低成本民用贾维斯。很可惜现在做的作品依然只是一个人工智障,它并发低、没有灵魂,响应很慢,bug很多。
|
||||||
|
|
||||||
|
虾哥团队是我们学习的对象,我很想拥有像虾哥团队一样智能的小智后端服务。但是我也能理解虾哥不开源的决定。“一花独放不是春,百花齐放春满园”,人工智能遍地开花的时代,也许就在我们这代实现,我们可以用自己的双手,实现低成本民用贾维斯。我个人认为,他能实现的,我们也能实现,只是时间问题而已,我称之为“我们的取经之路”。
|
||||||
|
|
||||||
|
那么这条取经之路,我们会遇到什么困难?我想应该不少于八十一难。这一路必然会出现各种妖怪,当然也有神仙暗中帮助我们,也有人加入取经队伍。
|
||||||
|
|
||||||
|
以上内容,如果你觉得好笑。那我也觉得非常的幸运。我能够在你人生3万多天里博你笑五秒,也算是为你做了一次贡献。
|
||||||
|
|
||||||
|
民用低成本贾维斯这个想法,会失败吗,我不知道,但是我们普通人的一生,这种失败不是很常见吗?
|
||||||
|
|
||||||
|
未来,有一点是可以确定的,就一定会有人完全复刻虾哥团队的功能,实现民用低成本贾维斯。这个项目会是我们吗?
|
||||||
|
|
||||||
|
期待与你携手前行,共创未来。
|
||||||
|
|
||||||
|
John,2025.3.11,广州
|
||||||
|
|
||||||
|
# 附 开发贡献指南
|
||||||
|
## 项目目标
|
||||||
|
|
||||||
|
1. **民用低成本贾维斯解决方案**
|
||||||
|
|
||||||
|
2. **智能联动周边硬件的解决方案**
|
||||||
|
|
||||||
|
## 加入我们
|
||||||
|
|
||||||
|
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。您可在[这个链接](https://github.com/users/xinnan-tech/projects/3)查看我们近期要实现的功能,功能列表中还没指派相关人员处理的,正是急需您的参与。参与方式如下:
|
||||||
|
|
||||||
|
### 1、成为普通贡献者
|
||||||
|
|
||||||
|
Fork 项目,提交 PR,由开发者审核后合入主分支。
|
||||||
|
|
||||||
|
### 2、成为开发者
|
||||||
|
|
||||||
|
当你累计提交 3 次有效 PR 后,可以联系群主申请成为开发者,群主将邀请你加入独立的开发者群,共同探讨项目未来。
|
||||||
|
|
||||||
|
## 开发者开发流程
|
||||||
|
|
||||||
|
1. **创建新分支**
|
||||||
|
每个功能点请以新分支方式开发,分支名称应简洁明了,让人一眼看出所实现的功能,避免功能撞车。
|
||||||
|
|
||||||
|
2. **提交 PR 审核**
|
||||||
|
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
||||||
@@ -8,39 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
|||||||
```
|
```
|
||||||
2、编译docker镜像
|
2、编译docker镜像
|
||||||
```
|
```
|
||||||
# 普通编译
|
#进入项目根目录
|
||||||
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
|
# 编译server
|
||||||
```
|
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||||
3、测试本地镜像
|
# 编译web
|
||||||
```
|
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||||
docker stop xiaozhi-esp32-server
|
|
||||||
docker rm xiaozhi-esp32-server
|
|
||||||
|
|
||||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
|
|
||||||
|
|
||||||
docker logs -f xiaozhi-esp32-server
|
|
||||||
|
|
||||||
```
|
# 编译完成后,可以使用docker-compose启动项目
|
||||||
5、发布腾讯云镜像
|
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||||
```
|
cd main/xiaozhi-server
|
||||||
# amd64
|
docker-compose up -d
|
||||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
|
||||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
|
||||||
|
|
||||||
# arm64
|
|
||||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
|
||||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
|
||||||
|
|
||||||
# 推送最新版本
|
|
||||||
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
|
|
||||||
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
|
|
||||||
```
|
|
||||||
6、运行线上镜像
|
|
||||||
```
|
|
||||||
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
|
|
||||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker logs -f xiaozhi-esp32-server
|
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
user root;
|
||||||
|
worker_processes 4;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 300;
|
||||||
|
client_header_timeout 180s;
|
||||||
|
client_body_timeout 180s;
|
||||||
|
client_max_body_size 1024M;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_buffers 32 4K;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 100;
|
||||||
|
gzip_types application/javascript text/css text/xml image/jpeg image/gif image/png;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
# 无域名访问,就用localhost
|
||||||
|
server_name localhost;
|
||||||
|
# 80端口
|
||||||
|
listen 8002;
|
||||||
|
|
||||||
|
# 转发到编译后到web目录
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转发到manager-api
|
||||||
|
location /xiaozhi/ {
|
||||||
|
proxy_pass http://127.0.0.1:8003;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cookie_path /manager/ /;
|
||||||
|
proxy_set_header Referer $http_referer;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10;
|
||||||
|
proxy_send_timeout 10;
|
||||||
|
proxy_read_timeout 10;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 启动Java后端(docker内监听8003端口)
|
||||||
|
java -jar /app/xiaozhi-esp32-api.jar \
|
||||||
|
--server.port=8003 \
|
||||||
|
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
|
||||||
|
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
|
||||||
|
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
|
||||||
|
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
|
||||||
|
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
|
||||||
|
|
||||||
|
# 启动Nginx(前台运行保持容器存活)
|
||||||
|
nginx -g 'daemon off;'
|
||||||
@@ -1,40 +1,81 @@
|
|||||||
# 编译esp32固件
|
# esp32固件编译
|
||||||
|
|
||||||
1. 下载`xiaozhi-esp32`
|
## 第1步 准备你的ota地址
|
||||||
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
|
||||||
|
|
||||||
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
|
||||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000`,就把内容改成这个。
|
|
||||||
|
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
|
||||||
|
|
||||||
|
### 如果你用的是简单Server部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||||
|
|
||||||
|
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||||
|
|
||||||
|
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||||
|
|
||||||
|
成功后,请往下进行第2步
|
||||||
|
|
||||||
|
### 如果你用的是全模块部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://192.168.1.25:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第2步 配置环境
|
||||||
|
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
|
## 第3步 打开配置文件
|
||||||
|
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
|
||||||
|
|
||||||
|
下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||||
|
|
||||||
|
## 第4步 修改OTA地址
|
||||||
|
|
||||||
|
找到`OTA_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||||
|
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
|
|
||||||
```
|
```
|
||||||
config WEBSOCKET_URL
|
config OTA_URL
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
string "Default OTA URL"
|
||||||
string "Websocket URL"
|
default "https://api.tenclass.net/xiaozhi/ota/"
|
||||||
default "wss://api.tenclass.net/xiaozhi/v1/"
|
|
||||||
help
|
help
|
||||||
Communication with the server through websocket after wake up.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
|
修改后:
|
||||||
修改后(示例):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
config WEBSOCKET_URL
|
config OTA_URL
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
string "Default OTA URL"
|
||||||
string "Websocket URL"
|
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||||
default "ws://192.168.1.25:8000/xiaozhi/v1/"
|
|
||||||
help
|
help
|
||||||
Communication with the server through websocket after wake up.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
## 第4步 设置编译参数
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
设置编译参数
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
|
||||||
|
|
||||||
3. 设置编译参数
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# 终端命令行进入xiaozhi-esp32的根目录
|
# 终端命令行进入xiaozhi-esp32的根目录
|
||||||
@@ -53,32 +94,26 @@ idf.py menuconfig
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
4. 编译固件
|
## 第5步 编译固件
|
||||||
|
|
||||||
```
|
```
|
||||||
idf.py build
|
idf.py build
|
||||||
```
|
```
|
||||||
|
|
||||||
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译
|
## 第6步 打包bin固件
|
||||||
|
|
||||||
还可以直接进行烧录不用接下来的操作
|
|
||||||
|
|
||||||
<img src="./images/vscode_idf.png" width="500px"/>
|
|
||||||
|
|
||||||
5. 打包bin固件
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd scripts
|
cd scripts
|
||||||
python release.py
|
python release.py
|
||||||
```
|
```
|
||||||
|
|
||||||
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||||
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
||||||
|
|
||||||
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
||||||
,对你没有太大影响,请继续。
|
,对你没有太大影响,请继续。
|
||||||
|
|
||||||
6. 烧录固件
|
## 第7步 烧录固件
|
||||||
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -87,3 +122,22 @@ https://espressif.github.io/esp-launchpad/
|
|||||||
|
|
||||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||||
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
||||||
|
|
||||||
|
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 基于虾哥编译好的固件配置自定义服务器
|
||||||
|
|
||||||
|
## 第1步 确认版本
|
||||||
|
烧录虾哥已经编译好的1.6.1版本固件
|
||||||
|
|
||||||
|
## 第2步 准备你的ota地址
|
||||||
|
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||||
|
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第3步 进入配网模式
|
||||||
|
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||||
|
|
||||||
|
## 第4步 唤醒小智,查看日志输出
|
||||||
|
|
||||||
|
唤醒小智,看看日志是不是正常输出。
|
||||||
|
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# ESP32设备与HomeAssistant集成指南
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- 已安装并配置好`HomeAssistant`
|
||||||
|
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
||||||
|
- `xiaozhi-esp32-server智控台`能正常使用
|
||||||
|
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||||
|
|
||||||
|
## 网络环境
|
||||||
|
|
||||||
|
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8002
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||||
|
|
||||||
|
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
|
||||||
|
|
||||||
|
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
|
||||||
|
|
||||||
|
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`。
|
||||||
|
|
||||||
|
我们打开一个记事本,按照这样格式整理一条数据:
|
||||||
|
|
||||||
|
位置+英文逗号+设备名称+英文逗号+`实体标识符`+英文分号
|
||||||
|
|
||||||
|
例如,我在公司,我有一个玩具灯,他的标识符是switch.cuco_cn_460494544_cp1_on_p_2_1,那么就这个写这一条数据
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
当然最后我可能要操作两个灯,我的最终的结果是:
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 登录`HomeAssistant`拿到开发密钥
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 登录`智控台`
|
||||||
|
|
||||||
|
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。
|
||||||
|
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||||
|
|
||||||
|
|
||||||
|
### 4. 设置`意图识别`函数
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||||
|
|
||||||
|
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
|
||||||
|
|
||||||
|
修改前
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music
|
||||||
|
```
|
||||||
|
|
||||||
|
修改后
|
||||||
|
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 手动重启xiaozhi-server
|
||||||
|
|
||||||
|
重启xiaozhi-server程序
|
||||||
|
|
||||||
|
|
||||||
|
### 5. 确认角色配置是否设置了函数意图识别
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||||
|
|
||||||
|
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||||
|
|
||||||
|
|
||||||
|
### 6. 唤醒设别进行控制
|
||||||
|
|
||||||
|
尝试和esp32说,“打开XXX灯”
|
||||||
|
Before Width: | Height: | Size: 947 KiB |
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 781 KiB |
|
After Width: | Height: | Size: 588 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 484 KiB |