xiaogpt 2.50__py3-none-any.whl → 2.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xiaogpt/cli.py +10 -22
- xiaogpt/config.py +2 -25
- xiaogpt/tts/__init__.py +4 -6
- xiaogpt/tts/base.py +4 -4
- xiaogpt/tts/mi.py +1 -1
- xiaogpt/tts/tetos.py +56 -0
- xiaogpt/xiaogpt.py +31 -19
- {xiaogpt-2.50.dist-info → xiaogpt-2.60.dist-info}/METADATA +48 -61
- {xiaogpt-2.50.dist-info → xiaogpt-2.60.dist-info}/RECORD +12 -15
- xiaogpt/tts/azure.py +0 -98
- xiaogpt/tts/edge.py +0 -32
- xiaogpt/tts/openai.py +0 -46
- xiaogpt/tts/volc.py +0 -130
- {xiaogpt-2.50.dist-info → xiaogpt-2.60.dist-info}/WHEEL +0 -0
- {xiaogpt-2.50.dist-info → xiaogpt-2.60.dist-info}/entry_points.txt +0 -0
- {xiaogpt-2.50.dist-info → xiaogpt-2.60.dist-info}/licenses/LICENSE +0 -0
xiaogpt/cli.py
CHANGED
@@ -86,27 +86,9 @@ def main():
|
|
86
86
|
help="show info",
|
87
87
|
)
|
88
88
|
parser.add_argument(
|
89
|
-
"--azure_tts_speech_key",
|
90
|
-
dest="azure_tts_speech_key",
|
91
|
-
help="if use azure tts",
|
92
|
-
)
|
93
|
-
parser.add_argument(
|
94
|
-
"--azure_tts_service_region",
|
95
|
-
dest="azure_tts_service_region",
|
96
|
-
help="if use azure tts",
|
97
|
-
)
|
98
|
-
tts_group = parser.add_mutually_exclusive_group()
|
99
|
-
tts_group.add_argument(
|
100
|
-
"--enable_edge_tts",
|
101
|
-
dest="tts",
|
102
|
-
action="store_const",
|
103
|
-
const="edge",
|
104
|
-
help="if use edge tts",
|
105
|
-
)
|
106
|
-
tts_group.add_argument(
|
107
89
|
"--tts",
|
108
|
-
help="
|
109
|
-
choices=["mi", "edge", "openai", "azure"],
|
90
|
+
help="TTS provider",
|
91
|
+
choices=["mi", "edge", "openai", "azure", "google", "baidu", "volc"],
|
110
92
|
)
|
111
93
|
bot_group = parser.add_mutually_exclusive_group()
|
112
94
|
bot_group.add_argument(
|
@@ -190,9 +172,15 @@ def main():
|
|
190
172
|
options = parser.parse_args()
|
191
173
|
config = Config.from_options(options)
|
192
174
|
|
193
|
-
|
175
|
+
async def main(config: Config) -> None:
|
176
|
+
miboy = MiGPT(config)
|
177
|
+
try:
|
178
|
+
await miboy.run_forever()
|
179
|
+
finally:
|
180
|
+
await miboy.close()
|
181
|
+
|
194
182
|
loop = asyncio.get_event_loop()
|
195
|
-
loop.run_until_complete(
|
183
|
+
loop.run_until_complete(main(config))
|
196
184
|
|
197
185
|
|
198
186
|
if __name__ == "__main__":
|
xiaogpt/config.py
CHANGED
@@ -33,15 +33,6 @@ HARDWARE_COMMAND_DICT = {
|
|
33
33
|
# add more here
|
34
34
|
}
|
35
35
|
|
36
|
-
EDGE_TTS_DICT = {
|
37
|
-
"用英语": "en-US-AriaNeural",
|
38
|
-
"用日语": "ja-JP-NanamiNeural",
|
39
|
-
"用法语": "fr-BE-CharlineNeural",
|
40
|
-
"用韩语": "ko-KR-SunHiNeural",
|
41
|
-
"用德语": "de-AT-JonasNeural",
|
42
|
-
# add more here
|
43
|
-
}
|
44
|
-
|
45
36
|
DEFAULT_COMMAND = ("5-1", "5-5")
|
46
37
|
|
47
38
|
KEY_WORD = ("帮我", "请")
|
@@ -80,23 +71,11 @@ class Config:
|
|
80
71
|
start_conversation: str = "开始持续对话"
|
81
72
|
end_conversation: str = "结束持续对话"
|
82
73
|
stream: bool = False
|
83
|
-
tts: Literal["mi", "edge", "azure", "openai"] = "mi"
|
84
|
-
|
74
|
+
tts: Literal["mi", "edge", "azure", "openai", "baidu", "google", "volc"] = "mi"
|
75
|
+
tts_options: dict[str, Any] = field(default_factory=dict)
|
85
76
|
gpt_options: dict[str, Any] = field(default_factory=dict)
|
86
77
|
bing_cookie_path: str = ""
|
87
78
|
bing_cookies: dict | None = None
|
88
|
-
azure_tts_speech_key: str | None = None
|
89
|
-
azure_tts_service_region: str = "eastasia"
|
90
|
-
volc_accesskey: str = os.getenv(
|
91
|
-
"VOLC_ACCESSKEY", ""
|
92
|
-
) # https://console.volcengine.com/iam/keymanage/
|
93
|
-
volc_secretkey: str = os.getenv("VOLC_SECRETKEY", "")
|
94
|
-
volc_tts_app: str = os.getenv(
|
95
|
-
"VOLC_TTS_APP", ""
|
96
|
-
) # https://console.volcengine.com/sami
|
97
|
-
volc_tts_speaker: str = os.getenv(
|
98
|
-
"VOLC_TTS_SPEAPER", "zh_female_qingxin"
|
99
|
-
) # https://www.volcengine.com/docs/6489/93478
|
100
79
|
|
101
80
|
def __post_init__(self) -> None:
|
102
81
|
if self.proxy:
|
@@ -121,8 +100,6 @@ class Config:
|
|
121
100
|
raise Exception(
|
122
101
|
"Using GPT api needs openai API key, please google how to"
|
123
102
|
)
|
124
|
-
if self.tts == "azure" and not self.azure_tts_speech_key:
|
125
|
-
raise Exception("Using Azure TTS needs azure speech key")
|
126
103
|
|
127
104
|
@property
|
128
105
|
def tts_command(self) -> str:
|
xiaogpt/tts/__init__.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
from xiaogpt.tts.base import TTS
|
2
|
-
from xiaogpt.tts.
|
3
|
-
from xiaogpt.tts.
|
4
|
-
from xiaogpt.tts.volc import VolcTTS as VolcTTS
|
5
|
-
from xiaogpt.tts.azure import AzureTTS
|
1
|
+
from xiaogpt.tts.base import TTS
|
2
|
+
from xiaogpt.tts.mi import MiTTS
|
3
|
+
from xiaogpt.tts.tetos import TetosTTS
|
6
4
|
|
7
|
-
__all__ = ["TTS", "
|
5
|
+
__all__ = ["TTS", "TetosTTS", "MiTTS"]
|
xiaogpt/tts/base.py
CHANGED
@@ -56,7 +56,7 @@ class TTS(abc.ABC):
|
|
56
56
|
return is_playing
|
57
57
|
|
58
58
|
@abc.abstractmethod
|
59
|
-
async def synthesize(self,
|
59
|
+
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
60
60
|
"""Synthesize speech from a stream of text."""
|
61
61
|
raise NotImplementedError
|
62
62
|
|
@@ -87,20 +87,20 @@ class AudioFileTTS(TTS):
|
|
87
87
|
self._start_http_server()
|
88
88
|
|
89
89
|
@abc.abstractmethod
|
90
|
-
async def make_audio_file(self,
|
90
|
+
async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
|
91
91
|
"""Synthesize speech from text and save it to a file.
|
92
92
|
Return the file path and the duration of the audio in seconds.
|
93
93
|
The file path must be relative to the self.dirname.
|
94
94
|
"""
|
95
95
|
raise NotImplementedError
|
96
96
|
|
97
|
-
async def synthesize(self,
|
97
|
+
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
98
98
|
queue: asyncio.Queue[tuple[str, float]] = asyncio.Queue()
|
99
99
|
finished = asyncio.Event()
|
100
100
|
|
101
101
|
async def worker():
|
102
102
|
async for text in text_stream:
|
103
|
-
path, duration = await self.make_audio_file(
|
103
|
+
path, duration = await self.make_audio_file(lang, text)
|
104
104
|
url = f"http://{self.hostname}:{self.port}/{path.name}"
|
105
105
|
await queue.put((url, duration))
|
106
106
|
finished.set()
|
xiaogpt/tts/mi.py
CHANGED
@@ -27,7 +27,7 @@ class MiTTS(TTS):
|
|
27
27
|
f"{self.config.tts_command} {text}",
|
28
28
|
)
|
29
29
|
|
30
|
-
async def synthesize(self,
|
30
|
+
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
31
31
|
async for text in text_stream:
|
32
32
|
await self.say(text)
|
33
33
|
await self.wait_for_duration(calculate_tts_elapse(text))
|
xiaogpt/tts/tetos.py
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import tempfile
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from miservice import MiNAService
|
7
|
+
from tetos.base import Speaker
|
8
|
+
|
9
|
+
from xiaogpt.config import Config
|
10
|
+
from xiaogpt.tts.base import AudioFileTTS
|
11
|
+
|
12
|
+
|
13
|
+
class TetosTTS(AudioFileTTS):
|
14
|
+
def __init__(
|
15
|
+
self, mina_service: MiNAService, device_id: str, config: Config
|
16
|
+
) -> None:
|
17
|
+
super().__init__(mina_service, device_id, config)
|
18
|
+
self.speaker = self._get_speaker()
|
19
|
+
|
20
|
+
def _get_speaker(self) -> Speaker:
|
21
|
+
from tetos.azure import AzureSpeaker
|
22
|
+
from tetos.baidu import BaiduSpeaker
|
23
|
+
from tetos.edge import EdgeSpeaker
|
24
|
+
from tetos.google import GoogleSpeaker
|
25
|
+
from tetos.openai import OpenAISpeaker
|
26
|
+
from tetos.volc import VolcSpeaker
|
27
|
+
|
28
|
+
options = self.config.tts_options
|
29
|
+
allowed_speakers: list[str] = []
|
30
|
+
for speaker in (
|
31
|
+
OpenAISpeaker,
|
32
|
+
EdgeSpeaker,
|
33
|
+
AzureSpeaker,
|
34
|
+
VolcSpeaker,
|
35
|
+
GoogleSpeaker,
|
36
|
+
BaiduSpeaker,
|
37
|
+
):
|
38
|
+
if (name := speaker.__name__[:-7].lower()) == self.config.tts:
|
39
|
+
try:
|
40
|
+
return speaker(**options)
|
41
|
+
except TypeError as e:
|
42
|
+
raise ValueError(
|
43
|
+
f"{e}. Please add them via `tts_options` config"
|
44
|
+
) from e
|
45
|
+
else:
|
46
|
+
allowed_speakers.append(name)
|
47
|
+
raise ValueError(
|
48
|
+
f"Unsupported TTS: {self.config.tts}, allowed: {','.join(allowed_speakers)}"
|
49
|
+
)
|
50
|
+
|
51
|
+
async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
|
52
|
+
output_file = tempfile.NamedTemporaryFile(
|
53
|
+
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
54
|
+
)
|
55
|
+
duration = await self.speaker.synthesize(text, output_file.name, lang=lang)
|
56
|
+
return Path(output_file.name), duration
|
xiaogpt/xiaogpt.py
CHANGED
@@ -23,8 +23,7 @@ from xiaogpt.config import (
|
|
23
23
|
WAKEUP_KEYWORD,
|
24
24
|
Config,
|
25
25
|
)
|
26
|
-
from xiaogpt.tts import TTS,
|
27
|
-
from xiaogpt.tts.openai import OpenAITTS
|
26
|
+
from xiaogpt.tts import TTS, MiTTS, TetosTTS
|
28
27
|
from xiaogpt.utils import (
|
29
28
|
parse_cookie_string,
|
30
29
|
)
|
@@ -53,6 +52,9 @@ class MiGPT:
|
|
53
52
|
self.log.debug(config)
|
54
53
|
self.mi_session = ClientSession()
|
55
54
|
|
55
|
+
async def close(self):
|
56
|
+
await self.mi_session.close()
|
57
|
+
|
56
58
|
async def poll_latest_ask(self):
|
57
59
|
async with ClientSession() as session:
|
58
60
|
session._cookie_jar = self.cookie_jar
|
@@ -78,16 +80,16 @@ class MiGPT:
|
|
78
80
|
# if you want force mute xiaoai, comment this line below.
|
79
81
|
await asyncio.sleep(1 - d)
|
80
82
|
|
81
|
-
async def init_all_data(self
|
82
|
-
await self.login_miboy(
|
83
|
+
async def init_all_data(self):
|
84
|
+
await self.login_miboy()
|
83
85
|
await self._init_data_hardware()
|
84
86
|
self.mi_session.cookie_jar.update_cookies(self.get_cookie())
|
85
87
|
self.cookie_jar = self.mi_session.cookie_jar
|
86
88
|
self.tts # init tts
|
87
89
|
|
88
|
-
async def login_miboy(self
|
90
|
+
async def login_miboy(self):
|
89
91
|
account = MiAccount(
|
90
|
-
|
92
|
+
self.mi_session,
|
91
93
|
self.config.account,
|
92
94
|
self.config.password,
|
93
95
|
str(self.mi_token_home),
|
@@ -179,7 +181,7 @@ class MiGPT:
|
|
179
181
|
return (
|
180
182
|
self.in_conversation
|
181
183
|
and not query.startswith(WAKEUP_KEYWORD)
|
182
|
-
or query.startswith(tuple(self.config.keyword))
|
184
|
+
or query.lower().startswith(tuple(w.lower() for w in self.config.keyword))
|
183
185
|
)
|
184
186
|
|
185
187
|
def need_change_prompt(self, record):
|
@@ -225,7 +227,7 @@ class MiGPT:
|
|
225
227
|
return None
|
226
228
|
|
227
229
|
async def _retry(self):
|
228
|
-
await self.init_all_data(
|
230
|
+
await self.init_all_data()
|
229
231
|
|
230
232
|
def _get_last_query(self, data: dict) -> dict | None:
|
231
233
|
if d := data.get("data"):
|
@@ -258,16 +260,10 @@ class MiGPT:
|
|
258
260
|
|
259
261
|
@functools.cached_property
|
260
262
|
def tts(self) -> TTS:
|
261
|
-
if self.config.tts == "
|
262
|
-
return EdgeTTS(self.mina_service, self.device_id, self.config)
|
263
|
-
elif self.config.tts == "azure":
|
264
|
-
return AzureTTS(self.mina_service, self.device_id, self.config)
|
265
|
-
elif self.config.tts == "openai":
|
266
|
-
return OpenAITTS(self.mina_service, self.device_id, self.config)
|
267
|
-
elif self.config.tts == "volc":
|
268
|
-
return VolcTTS(self.mina_service, self.device_id, self.config)
|
269
|
-
else:
|
263
|
+
if self.config.tts == "mi":
|
270
264
|
return MiTTS(self.mina_service, self.device_id, self.config)
|
265
|
+
else:
|
266
|
+
return TetosTTS(self.mina_service, self.device_id, self.config)
|
271
267
|
|
272
268
|
async def wait_for_tts_finish(self):
|
273
269
|
while True:
|
@@ -347,7 +343,7 @@ class MiGPT:
|
|
347
343
|
)
|
348
344
|
|
349
345
|
async def run_forever(self):
|
350
|
-
await self.init_all_data(
|
346
|
+
await self.init_all_data()
|
351
347
|
task = asyncio.create_task(self.poll_latest_ask())
|
352
348
|
assert task is not None # to keep the reference to task, do not remove this
|
353
349
|
print(
|
@@ -389,6 +385,7 @@ class MiGPT:
|
|
389
385
|
print("问题:" + query + "?")
|
390
386
|
if not self.chatbot.has_history():
|
391
387
|
query = f"{query},{self.config.prompt}"
|
388
|
+
query += ",并用本段话的language code作为开头,用|分隔,如:en-US|你好……"
|
392
389
|
if self.config.mute_xiaoai:
|
393
390
|
await self.stop_if_xiaoai_is_playing()
|
394
391
|
else:
|
@@ -404,7 +401,7 @@ class MiGPT:
|
|
404
401
|
print("小爱没回")
|
405
402
|
print(f"以下是 {self.chatbot.name} 的回答: ", end="")
|
406
403
|
try:
|
407
|
-
await self.
|
404
|
+
await self.speak(self.ask_gpt(query))
|
408
405
|
except Exception as e:
|
409
406
|
print(f"{self.chatbot.name} 回答出错 {str(e)}")
|
410
407
|
else:
|
@@ -412,3 +409,18 @@ class MiGPT:
|
|
412
409
|
if self.in_conversation:
|
413
410
|
print(f"继续对话, 或用`{self.config.end_conversation}`结束对话")
|
414
411
|
await self.wakeup_xiaoai()
|
412
|
+
|
413
|
+
async def speak(self, text_stream: AsyncIterator[str]) -> None:
|
414
|
+
text = await anext(text_stream)
|
415
|
+
# See if the first part contains language code(e.g. en-US|Hello world)
|
416
|
+
lang, _, first_chunk = text.rpartition("|")
|
417
|
+
if len(lang) > 7:
|
418
|
+
# It is not a legal language code, discard it
|
419
|
+
lang, first_chunk = "", text
|
420
|
+
|
421
|
+
async def gen(): # reconstruct the generator
|
422
|
+
yield first_chunk
|
423
|
+
async for text in text_stream:
|
424
|
+
yield text
|
425
|
+
|
426
|
+
await self.tts.synthesize(lang or "zh-CN", gen())
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: xiaogpt
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.60
|
4
4
|
Summary: Play ChatGPT or other LLM with xiaomi AI speaker
|
5
5
|
Author-Email: yihong0618 <zouzou0208@gmail.com>
|
6
6
|
License: MIT
|
@@ -15,7 +15,6 @@ Requires-Dist: aiohttp
|
|
15
15
|
Requires-Dist: rich
|
16
16
|
Requires-Dist: zhipuai>=2.0.1
|
17
17
|
Requires-Dist: httpx[socks]
|
18
|
-
Requires-Dist: edge-tts>=6.1.3
|
19
18
|
Requires-Dist: EdgeGPT==0.1.26
|
20
19
|
Requires-Dist: langchain>=0.0.343
|
21
20
|
Requires-Dist: beautifulsoup4>=4.12.0
|
@@ -23,13 +22,11 @@ Requires-Dist: google-search-results>=2.4.2
|
|
23
22
|
Requires-Dist: google-generativeai
|
24
23
|
Requires-Dist: numexpr>=2.8.6
|
25
24
|
Requires-Dist: dashscope>=1.10.0
|
26
|
-
Requires-Dist:
|
27
|
-
Requires-Dist:
|
28
|
-
Requires-Dist: volcengine>=1.0.136
|
29
|
-
Requires-Dist: aiohttp==3.9.4; extra == "locked"
|
25
|
+
Requires-Dist: tetos>=0.1.0
|
26
|
+
Requires-Dist: aiohttp==3.9.5; extra == "locked"
|
30
27
|
Requires-Dist: aiosignal==1.3.1; extra == "locked"
|
31
28
|
Requires-Dist: annotated-types==0.6.0; extra == "locked"
|
32
|
-
Requires-Dist: anyio==3.
|
29
|
+
Requires-Dist: anyio==4.3.0; extra == "locked"
|
33
30
|
Requires-Dist: async-timeout==4.0.3; python_version < "3.11" and extra == "locked"
|
34
31
|
Requires-Dist: attrs==23.2.0; extra == "locked"
|
35
32
|
Requires-Dist: azure-cognitiveservices-speech==1.37.0; extra == "locked"
|
@@ -38,23 +35,23 @@ Requires-Dist: bingimagecreator==0.5.0; extra == "locked"
|
|
38
35
|
Requires-Dist: cachetools==5.3.2; extra == "locked"
|
39
36
|
Requires-Dist: certifi==2024.2.2; extra == "locked"
|
40
37
|
Requires-Dist: charset-normalizer==3.3.2; extra == "locked"
|
38
|
+
Requires-Dist: click==8.1.7; extra == "locked"
|
41
39
|
Requires-Dist: colorama==0.4.6; platform_system == "Windows" and extra == "locked"
|
42
|
-
Requires-Dist: dashscope==1.
|
40
|
+
Requires-Dist: dashscope==1.17.0; extra == "locked"
|
43
41
|
Requires-Dist: dataclasses-json==0.6.3; extra == "locked"
|
44
|
-
Requires-Dist: decorator==5.1.1; extra == "locked"
|
45
42
|
Requires-Dist: distro==1.9.0; extra == "locked"
|
46
43
|
Requires-Dist: edge-tts==6.1.10; extra == "locked"
|
47
44
|
Requires-Dist: edgegpt==0.1.26; extra == "locked"
|
48
45
|
Requires-Dist: exceptiongroup==1.2.0; python_version < "3.11" and extra == "locked"
|
49
46
|
Requires-Dist: frozenlist==1.4.1; extra == "locked"
|
50
|
-
Requires-Dist: google==
|
51
|
-
Requires-Dist: google-ai-generativelanguage==0.6.1; extra == "locked"
|
47
|
+
Requires-Dist: google-ai-generativelanguage==0.6.2; extra == "locked"
|
52
48
|
Requires-Dist: google-api-core==2.15.0; extra == "locked"
|
53
49
|
Requires-Dist: google-api-core[grpc]==2.15.0; extra == "locked"
|
54
50
|
Requires-Dist: google-api-python-client==2.125.0; extra == "locked"
|
55
51
|
Requires-Dist: google-auth==2.26.1; extra == "locked"
|
56
52
|
Requires-Dist: google-auth-httplib2==0.2.0; extra == "locked"
|
57
|
-
Requires-Dist: google-
|
53
|
+
Requires-Dist: google-cloud-texttospeech==2.16.3; extra == "locked"
|
54
|
+
Requires-Dist: google-generativeai==0.5.1; extra == "locked"
|
58
55
|
Requires-Dist: google-search-results==2.4.2; extra == "locked"
|
59
56
|
Requires-Dist: googleapis-common-protos==1.62.0; extra == "locked"
|
60
57
|
Requires-Dist: greenlet==3.0.3; (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64") and extra == "locked"
|
@@ -82,40 +79,35 @@ Requires-Dist: mutagen==1.47.0; extra == "locked"
|
|
82
79
|
Requires-Dist: mypy-extensions==1.0.0; extra == "locked"
|
83
80
|
Requires-Dist: numexpr==2.10.0; extra == "locked"
|
84
81
|
Requires-Dist: numpy==1.26.3; extra == "locked"
|
85
|
-
Requires-Dist: openai==1.
|
82
|
+
Requires-Dist: openai==1.21.2; extra == "locked"
|
86
83
|
Requires-Dist: orjson==3.10.0; extra == "locked"
|
87
84
|
Requires-Dist: packaging==23.2; extra == "locked"
|
88
85
|
Requires-Dist: prompt-toolkit==3.0.43; extra == "locked"
|
89
86
|
Requires-Dist: proto-plus==1.23.0; extra == "locked"
|
90
87
|
Requires-Dist: protobuf==4.25.1; extra == "locked"
|
91
|
-
Requires-Dist: py==1.11.0; extra == "locked"
|
92
88
|
Requires-Dist: pyasn1==0.5.1; extra == "locked"
|
93
89
|
Requires-Dist: pyasn1-modules==0.3.0; extra == "locked"
|
94
|
-
Requires-Dist: pycryptodome==3.9.9; extra == "locked"
|
95
90
|
Requires-Dist: pydantic==2.5.3; extra == "locked"
|
96
91
|
Requires-Dist: pydantic-core==2.14.6; extra == "locked"
|
97
92
|
Requires-Dist: pygments==2.17.2; extra == "locked"
|
98
93
|
Requires-Dist: pyjwt==2.8.0; extra == "locked"
|
99
94
|
Requires-Dist: pyparsing==3.1.2; python_version > "3.0" and extra == "locked"
|
100
|
-
Requires-Dist: pytz==2020.5; extra == "locked"
|
101
95
|
Requires-Dist: pyyaml==6.0.1; extra == "locked"
|
102
96
|
Requires-Dist: regex==2023.12.25; extra == "locked"
|
103
97
|
Requires-Dist: requests==2.31.0; extra == "locked"
|
104
|
-
Requires-Dist: retry==0.9.2; extra == "locked"
|
105
98
|
Requires-Dist: rich==13.7.1; extra == "locked"
|
106
99
|
Requires-Dist: rsa==4.9; extra == "locked"
|
107
|
-
Requires-Dist: six==1.16.0; extra == "locked"
|
108
100
|
Requires-Dist: sniffio==1.3.0; extra == "locked"
|
109
101
|
Requires-Dist: socksio==1.0.0; extra == "locked"
|
110
102
|
Requires-Dist: soupsieve==2.5; extra == "locked"
|
111
103
|
Requires-Dist: sqlalchemy==2.0.25; extra == "locked"
|
112
104
|
Requires-Dist: tenacity==8.2.3; extra == "locked"
|
105
|
+
Requires-Dist: tetos==0.1.0; extra == "locked"
|
113
106
|
Requires-Dist: tqdm==4.66.1; extra == "locked"
|
114
107
|
Requires-Dist: typing-extensions==4.9.0; extra == "locked"
|
115
108
|
Requires-Dist: typing-inspect==0.9.0; extra == "locked"
|
116
109
|
Requires-Dist: uritemplate==4.1.1; extra == "locked"
|
117
110
|
Requires-Dist: urllib3==2.1.0; extra == "locked"
|
118
|
-
Requires-Dist: volcengine==1.0.136; extra == "locked"
|
119
111
|
Requires-Dist: wcwidth==0.2.13; extra == "locked"
|
120
112
|
Requires-Dist: websockets==12.0; extra == "locked"
|
121
113
|
Requires-Dist: yarl==1.9.4; extra == "locked"
|
@@ -144,12 +136,12 @@ Play ChatGPT and other LLM with Xiaomi AI Speaker
|
|
144
136
|
- [通义千问](https://help.aliyun.com/zh/dashscope/developer-reference/api-details)
|
145
137
|
|
146
138
|
## 获取小米音响DID
|
147
|
-
系统和Shell|Linux *sh|Windows CMD
|
148
|
-
|
149
|
-
1
|
150
|
-
2
|
151
|
-
3、取得MI_DID
|
152
|
-
4、设置MI_DID
|
139
|
+
| 系统和Shell | Linux *sh | Windows CMD用户 | Windows PowerShell用户 |
|
140
|
+
| ------------- | ---------------------------------------------- | -------------------------------------- | ---------------------------------------------- |
|
141
|
+
| 1、安装包 | `pip install miservice_fork` | `pip install miservice_fork` | `pip install miservice_fork` |
|
142
|
+
| 2、设置变量 | `export MI_USER=xxx` <br> `export MI_PASS=xxx` | `set MI_USER=xxx`<br>`set MI_PASS=xxx` | `$env:MI_USER="xxx"` <br> `$env:MI_PASS="xxx"` |
|
143
|
+
| 3、取得MI_DID | `micli list` | `micli list` | `micli list` |
|
144
|
+
| 4、设置MI_DID | `export MI_DID=xxx` | `set MI_DID=xxx` | `$env:MI_DID="xxx"` |
|
153
145
|
|
154
146
|
- 注意不同shell 对环境变量的处理是不同的,尤其是powershell赋值时,可能需要双引号来包括值。
|
155
147
|
- 如果获取did报错时,请更换一下无线网络,有很大概率解决问题。
|
@@ -271,42 +263,36 @@ ChatGLM [文档](http://open.bigmodel.cn/doc/api#chatglm_130b)
|
|
271
263
|
|
272
264
|
## 配置项说明
|
273
265
|
|
274
|
-
| 参数
|
275
|
-
|
|
276
|
-
| hardware
|
277
|
-
| account
|
278
|
-
| password
|
279
|
-
| openai_key
|
280
|
-
| serpapi_api_key
|
281
|
-
| glm_key
|
282
|
-
| gemini_key
|
283
|
-
| qwen_key
|
284
|
-
| cookie
|
285
|
-
| mi_did
|
286
|
-
| use_command
|
287
|
-
| mute_xiaoai
|
288
|
-
| verbose
|
289
|
-
| bot
|
290
|
-
| tts
|
291
|
-
|
|
292
|
-
| prompt
|
293
|
-
| keyword
|
294
|
-
| change_prompt_keyword
|
295
|
-
| start_conversation
|
296
|
-
| end_conversation
|
297
|
-
| stream
|
298
|
-
| proxy
|
299
|
-
| gpt_options
|
300
|
-
| bing_cookie_path
|
301
|
-
| bing_cookies
|
302
|
-
| deployment_id
|
303
|
-
| api_base
|
304
|
-
| azure_tts_speech_key | Azure TTS key | null | |
|
305
|
-
| azure_tts_service_region | Azure TTS 服务地区 | `eastasia` | [Regions - Speech service - Azure AI services](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/regions) |
|
306
|
-
| volc_accesskey | 火山引擎accesskey [参考](https://console.volcengine.com/iam/keymanage/) | | |
|
307
|
-
| volc_secretkey | 火山引擎secretkey [参考](https://console.volcengine.com/iam/keymanage/) | | |
|
308
|
-
| volc_tts_app | 火山引擎 TTS app 服务 [参考]( https://console.volcengine.com/sami/) | | |
|
309
|
-
| volc_tts_speaker | 火山引擎 TTS speaker [参考]( https://www.volcengine.com/docs/6489/93478) | `zh_female_qingxin` | |
|
266
|
+
| 参数 | 说明 | 默认值 | 可选值 |
|
267
|
+
| --------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
268
|
+
| hardware | 设备型号 | | |
|
269
|
+
| account | 小爱账户 | | |
|
270
|
+
| password | 小爱账户密码 | | |
|
271
|
+
| openai_key | openai的apikey | | |
|
272
|
+
| serpapi_api_key | serpapi的key 参考 [SerpAPI](https://serpapi.com/) | | |
|
273
|
+
| glm_key | chatglm 的 apikey | | |
|
274
|
+
| gemini_key | gemini 的 apikey [参考](https://makersuite.google.com/app/apikey) | | |
|
275
|
+
| qwen_key | qwen 的 apikey [参考](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) | | |
|
276
|
+
| cookie | 小爱账户cookie (如果用上面密码登录可以不填) | | |
|
277
|
+
| mi_did | 设备did | | |
|
278
|
+
| use_command | 使用 MI command 与小爱交互 | `false` | |
|
279
|
+
| mute_xiaoai | 快速停掉小爱自己的回答 | `true` | |
|
280
|
+
| verbose | 是否打印详细日志 | `false` | |
|
281
|
+
| bot | 使用的 bot 类型,目前支持 chatgptapi,newbing, qwen, gemini | `chatgptapi` | |
|
282
|
+
| tts | 使用的 TTS 类型 | `mi` | `edge`、 `openai`、`azure`、`volc`、`baidu`、`google` |
|
283
|
+
| tts_options | TTS 参数字典,参考 [tetos](https://github.com/frostming/tetos) 获取可用参数 | | |
|
284
|
+
| prompt | 自定义prompt | `请用100字以内回答` | |
|
285
|
+
| keyword | 自定义请求词列表 | `["请"]` | |
|
286
|
+
| change_prompt_keyword | 更改提示词触发列表 | `["更改提示词"]` | |
|
287
|
+
| start_conversation | 开始持续对话关键词 | `开始持续对话` | |
|
288
|
+
| end_conversation | 结束持续对话关键词 | `结束持续对话` | |
|
289
|
+
| stream | 使用流式响应,获得更快的响应 | `false` | |
|
290
|
+
| proxy | 支持 HTTP 代理,传入 http proxy URL | "" | |
|
291
|
+
| gpt_options | OpenAI API 的参数字典 | `{}` | |
|
292
|
+
| bing_cookie_path | NewBing使用的cookie路径,参考[这里]获取 | 也可通过环境变量 `COOKIE_FILE` 设置 | |
|
293
|
+
| bing_cookies | NewBing使用的cookie字典,参考[这里]获取 | | |
|
294
|
+
| deployment_id | Azure OpenAI 服务的 deployment ID | 参考这个[如何找到deployment_id](https://github.com/yihong0618/xiaogpt/issues/347#issuecomment-1784410784) | |
|
295
|
+
| api_base | 如果需要替换默认的api,或者使用Azure OpenAI 服务 | 例如:`https://abc-def.openai.azure.com/` | |
|
310
296
|
|
311
297
|
|
312
298
|
[这里]: https://github.com/acheong08/EdgeGPT#getting-authentication-required
|
@@ -425,6 +411,7 @@ docker run -v <your-config-dir>:/config -p 9527:9527 -e XIAOGPT_HOSTNAME=<your i
|
|
425
411
|
|
426
412
|
- [xiaomi](https://www.mi.com/)
|
427
413
|
- [PDM](https://pdm.fming.dev/latest/)
|
414
|
+
- [Tetos](https://github.com/frostming/tetos) TTS 云服务支持
|
428
415
|
- @[Yonsm](https://github.com/Yonsm) 的 [MiService](https://github.com/Yonsm/MiService)
|
429
416
|
- @[pjq](https://github.com/pjq) 给了这个项目非常多的帮助
|
430
417
|
- @[frostming](https://github.com/frostming) 重构了一些代码,支持了`持续会话功能`
|
@@ -1,7 +1,7 @@
|
|
1
|
-
xiaogpt-2.
|
2
|
-
xiaogpt-2.
|
3
|
-
xiaogpt-2.
|
4
|
-
xiaogpt-2.
|
1
|
+
xiaogpt-2.60.dist-info/METADATA,sha256=WlspZJW0q2G9Zm4bA-bMBXiToOQpWIT6JafQnUEwZV4,26836
|
2
|
+
xiaogpt-2.60.dist-info/WHEEL,sha256=7sv5iXvIiTVJSnAxCz2tGBm9DHsb2vPSzeYeT7pvGUY,90
|
3
|
+
xiaogpt-2.60.dist-info/entry_points.txt,sha256=zLFzA72qQ_eWBepdA2YU5vdXFqORH8wXhv2Ox1vnYP8,46
|
4
|
+
xiaogpt-2.60.dist-info/licenses/LICENSE,sha256=XdClh516MvlnOf9749JZHCxSB7y6_fyXcWmLDz6IkZY,1063
|
5
5
|
xiaogpt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
xiaogpt/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
7
7
|
xiaogpt/bot/__init__.py,sha256=7K9v6j6xDkuvIrJwjby0Ec_1ywd4Si0Ww9S_R8M3LeU,919
|
@@ -12,19 +12,16 @@ xiaogpt/bot/glm_bot.py,sha256=QoMJbnu5_rHDz4tzwn7gh3IoAuw7E4hZQLAfziMAvNY,1825
|
|
12
12
|
xiaogpt/bot/langchain_bot.py,sha256=4Uz5iOYzA2ongCklS-9zBse2fw-7kEE_9wITH7wdVCc,1944
|
13
13
|
xiaogpt/bot/newbing_bot.py,sha256=afUmw6tyMXbgGZvfQQWaA5h0-e0V0isFolW-WGhd0Vs,2289
|
14
14
|
xiaogpt/bot/qwen_bot.py,sha256=325lMa4Z38rRh47HDa3J4XjvSs4SWOqMVhrMWzkGNo4,3657
|
15
|
-
xiaogpt/cli.py,sha256=
|
16
|
-
xiaogpt/config.py,sha256=
|
15
|
+
xiaogpt/cli.py,sha256=0ZmBmJ3dV748EaFYmKnxgGcSWy9LYoqLixKhVcRVCa4,4409
|
16
|
+
xiaogpt/config.py,sha256=5k7UqyALvUGF9H6QID9p_i3dIB8BZG1XJePjwtsf2Lg,5896
|
17
17
|
xiaogpt/langchain/callbacks.py,sha256=yR9AXQt9OHVYBWC47Q1I_BUT4Xg9iM44vnW2vv0BLpE,2616
|
18
18
|
xiaogpt/langchain/chain.py,sha256=z0cqRlL0ElWnf31ByxZBN7AKOT-svXQDt5_NDft_nYc,1495
|
19
19
|
xiaogpt/langchain/examples/email/mail_box.py,sha256=xauqrjE4-G4XPQnokUPE-MZgAaHQ_VrUDLlbfYTdCoo,6372
|
20
20
|
xiaogpt/langchain/examples/email/mail_summary_tools.py,sha256=6cWvBJUaA7iaywcHdbUoww8WiCtaNw3TmwyxyF4DY7E,1561
|
21
|
-
xiaogpt/tts/__init__.py,sha256=
|
22
|
-
xiaogpt/tts/
|
23
|
-
xiaogpt/tts/
|
24
|
-
xiaogpt/tts/
|
25
|
-
xiaogpt/tts/mi.py,sha256=9HkgGWByAs7k8sTpRdVlgJnnmjc44RNAccJa6tGDlXk,1096
|
26
|
-
xiaogpt/tts/openai.py,sha256=_Qk12zYY-UuXLKvQVe3PqIvCmoRW9OcVCqQRoGCXvNc,1533
|
27
|
-
xiaogpt/tts/volc.py,sha256=fLN7Z5HXx9TCkhGKWyTXasUcXn_fSlTNjVccvQunLUM,3984
|
21
|
+
xiaogpt/tts/__init__.py,sha256=xasHDrmgECirf1MSyrfURSaMBqtdZBi3cQNeDvPo_cQ,145
|
22
|
+
xiaogpt/tts/base.py,sha256=fljxdXy60HXqdLXyQlsJZtzJBo5VtTwLkkWTi58tzQc,4656
|
23
|
+
xiaogpt/tts/mi.py,sha256=1MzCB27DBohPQ_4Xz4W_FV9p-chJFDavOHB89NviLcM,1095
|
24
|
+
xiaogpt/tts/tetos.py,sha256=9DaSrfU8Pf_aa7mI6JXMvE70XYJogIPvbim4Yuhcc3k,1903
|
28
25
|
xiaogpt/utils.py,sha256=B7NCH7g19hcwHDXsnBJPTU6UcWnXoEntKWm-pgcet2I,2072
|
29
|
-
xiaogpt/xiaogpt.py,sha256=
|
30
|
-
xiaogpt-2.
|
26
|
+
xiaogpt/xiaogpt.py,sha256=Be5kUkn7H4lKU032dmaHP9LJAQKb65hMvulJMnVc5-g,15821
|
27
|
+
xiaogpt-2.60.dist-info/RECORD,,
|
xiaogpt/tts/azure.py
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import logging
|
4
|
-
import tempfile
|
5
|
-
from pathlib import Path
|
6
|
-
from typing import Optional
|
7
|
-
|
8
|
-
import azure.cognitiveservices.speech as speechsdk
|
9
|
-
|
10
|
-
from xiaogpt.tts.base import AudioFileTTS
|
11
|
-
from xiaogpt.utils import calculate_tts_elapse
|
12
|
-
|
13
|
-
logger = logging.getLogger(__name__)
|
14
|
-
|
15
|
-
|
16
|
-
class AzureTTS(AudioFileTTS):
|
17
|
-
voice_name = "zh-CN-XiaoxiaoMultilingualNeural"
|
18
|
-
|
19
|
-
async def make_audio_file(self, query: str, text: str) -> tuple[Path, float]:
|
20
|
-
output_file = tempfile.NamedTemporaryFile(
|
21
|
-
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
22
|
-
)
|
23
|
-
|
24
|
-
speech_synthesizer = self._build_speech_synthesizer(output_file.name)
|
25
|
-
result: Optional[speechsdk.SpeechSynthesisResult] = (
|
26
|
-
speech_synthesizer.speak_text_async(text).get()
|
27
|
-
)
|
28
|
-
if result is None:
|
29
|
-
raise RuntimeError(
|
30
|
-
f"Failed to get tts from azure with voice={self.voice_name}"
|
31
|
-
)
|
32
|
-
# Check result
|
33
|
-
if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
|
34
|
-
logger.debug("Speech synthesized for text [{}]".format(text))
|
35
|
-
|
36
|
-
return Path(output_file.name), calculate_tts_elapse(text)
|
37
|
-
elif result.reason == speechsdk.ResultReason.Canceled:
|
38
|
-
cancellation_details = result.cancellation_details
|
39
|
-
logger.warning(f"Speech synthesis canceled: {cancellation_details.reason}")
|
40
|
-
if cancellation_details.reason == speechsdk.CancellationReason.Error:
|
41
|
-
errmsg = f"Error details: {cancellation_details.error_details}"
|
42
|
-
logger.error(errmsg)
|
43
|
-
raise RuntimeError(errmsg)
|
44
|
-
raise RuntimeError(f"Failed to get tts from azure with voice={self.voice_name}")
|
45
|
-
|
46
|
-
def _build_speech_synthesizer(self, filename: str):
|
47
|
-
speech_key = self.config.azure_tts_speech_key
|
48
|
-
service_region = self.config.azure_tts_service_region
|
49
|
-
if not speech_key:
|
50
|
-
raise Exception("Azure tts need speech key")
|
51
|
-
speech_config = speechsdk.SpeechConfig(
|
52
|
-
subscription=speech_key, region=service_region
|
53
|
-
)
|
54
|
-
speech_config.set_speech_synthesis_output_format(
|
55
|
-
speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3
|
56
|
-
)
|
57
|
-
if self.config.proxy:
|
58
|
-
host, port, username, password = self._parse_proxy(self.config.proxy)
|
59
|
-
|
60
|
-
if username and password:
|
61
|
-
speech_config.set_proxy(
|
62
|
-
hostname=host, port=port, username=username, password=password
|
63
|
-
)
|
64
|
-
else:
|
65
|
-
speech_config.set_proxy(hostname=host, port=port)
|
66
|
-
|
67
|
-
speech_config.speech_synthesis_voice_name = (
|
68
|
-
self.config.tts_voice or self.voice_name
|
69
|
-
)
|
70
|
-
speech_synthesizer = speechsdk.SpeechSynthesizer(
|
71
|
-
speech_config=speech_config,
|
72
|
-
audio_config=speechsdk.audio.AudioOutputConfig(filename=filename), # type: ignore
|
73
|
-
)
|
74
|
-
return speech_synthesizer
|
75
|
-
|
76
|
-
def _parse_proxy(self, proxy_str: str):
|
77
|
-
proxy_str = proxy_str
|
78
|
-
proxy_str_splited = proxy_str.split("://")
|
79
|
-
proxy_type = proxy_str_splited[0]
|
80
|
-
proxy_addr = proxy_str_splited[1]
|
81
|
-
|
82
|
-
if proxy_type == "http":
|
83
|
-
if "@" in proxy_addr:
|
84
|
-
proxy_addr_splited = proxy_addr.split("@")
|
85
|
-
proxy_auth = proxy_addr_splited[0]
|
86
|
-
proxy_addr_netloc = proxy_addr_splited[1]
|
87
|
-
proxy_auth_splited = proxy_auth.split(":")
|
88
|
-
username = proxy_auth_splited[0]
|
89
|
-
password = proxy_auth_splited[1]
|
90
|
-
else:
|
91
|
-
proxy_addr_netloc = proxy_addr
|
92
|
-
username, password = None, None
|
93
|
-
|
94
|
-
proxy_addr_netloc_splited = proxy_addr_netloc.split(":")
|
95
|
-
host = proxy_addr_netloc_splited[0]
|
96
|
-
port = int(proxy_addr_netloc_splited[1])
|
97
|
-
return host, port, username, password
|
98
|
-
raise NotImplementedError
|
xiaogpt/tts/edge.py
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
import tempfile
|
2
|
-
from pathlib import Path
|
3
|
-
|
4
|
-
import edge_tts
|
5
|
-
|
6
|
-
from xiaogpt.config import EDGE_TTS_DICT
|
7
|
-
from xiaogpt.tts.base import AudioFileTTS
|
8
|
-
from xiaogpt.utils import find_key_by_partial_string
|
9
|
-
|
10
|
-
|
11
|
-
class EdgeTTS(AudioFileTTS):
|
12
|
-
default_voice = "zh-CN-XiaoxiaoNeural"
|
13
|
-
|
14
|
-
async def make_audio_file(self, query: str, text: str) -> tuple[Path, float]:
|
15
|
-
voice = (
|
16
|
-
find_key_by_partial_string(EDGE_TTS_DICT, query)
|
17
|
-
or self.config.tts_voice
|
18
|
-
or self.default_voice
|
19
|
-
)
|
20
|
-
communicate = edge_tts.Communicate(text, voice, proxy=self.config.proxy)
|
21
|
-
duration = 0
|
22
|
-
with tempfile.NamedTemporaryFile(
|
23
|
-
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
24
|
-
) as f:
|
25
|
-
async for chunk in communicate.stream():
|
26
|
-
if chunk["type"] == "audio":
|
27
|
-
f.write(chunk["data"])
|
28
|
-
elif chunk["type"] == "WordBoundary":
|
29
|
-
duration = (chunk["offset"] + chunk["duration"]) / 1e7
|
30
|
-
if duration == 0:
|
31
|
-
raise RuntimeError(f"Failed to get tts from edge with voice={voice}")
|
32
|
-
return (Path(f.name), duration)
|
xiaogpt/tts/openai.py
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import tempfile
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import TYPE_CHECKING
|
6
|
-
|
7
|
-
import httpx
|
8
|
-
|
9
|
-
from xiaogpt.tts.base import AudioFileTTS
|
10
|
-
from xiaogpt.utils import calculate_tts_elapse
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
import openai
|
14
|
-
|
15
|
-
|
16
|
-
class OpenAITTS(AudioFileTTS):
|
17
|
-
default_voice = "alloy"
|
18
|
-
|
19
|
-
async def make_audio_file(self, query: str, text: str) -> tuple[Path, float]:
|
20
|
-
output_file = tempfile.NamedTemporaryFile(
|
21
|
-
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
22
|
-
)
|
23
|
-
httpx_kwargs = {}
|
24
|
-
if self.config.proxy:
|
25
|
-
httpx_kwargs["proxies"] = self.config.proxy
|
26
|
-
async with httpx.AsyncClient(trust_env=True, **httpx_kwargs) as sess:
|
27
|
-
client = self._make_openai_client(sess)
|
28
|
-
|
29
|
-
resp = await client.audio.speech.create(
|
30
|
-
model="tts-1",
|
31
|
-
input=text,
|
32
|
-
voice=self.config.tts_voice or self.default_voice,
|
33
|
-
)
|
34
|
-
resp.stream_to_file(output_file.name)
|
35
|
-
return Path(output_file.name), calculate_tts_elapse(text)
|
36
|
-
|
37
|
-
def _make_openai_client(self, sess: httpx.AsyncClient) -> openai.AsyncOpenAI:
|
38
|
-
import openai
|
39
|
-
|
40
|
-
api_base = self.config.api_base
|
41
|
-
if api_base and api_base.rstrip("/").endswith("openai.azure.com"):
|
42
|
-
raise NotImplementedError("TTS is not supported for Azure OpenAI")
|
43
|
-
else:
|
44
|
-
return openai.AsyncOpenAI(
|
45
|
-
api_key=self.config.openai_key, http_client=sess, base_url=api_base
|
46
|
-
)
|
xiaogpt/tts/volc.py
DELETED
@@ -1,130 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import logging
|
4
|
-
import tempfile
|
5
|
-
from pathlib import Path
|
6
|
-
from typing import Optional
|
7
|
-
import json
|
8
|
-
import os
|
9
|
-
import time
|
10
|
-
import base64
|
11
|
-
import threading
|
12
|
-
import httpx
|
13
|
-
|
14
|
-
from volcengine.ApiInfo import ApiInfo
|
15
|
-
from volcengine.Credentials import Credentials
|
16
|
-
from volcengine.ServiceInfo import ServiceInfo
|
17
|
-
from volcengine.base.Service import Service
|
18
|
-
|
19
|
-
|
20
|
-
from xiaogpt.tts.base import AudioFileTTS
|
21
|
-
from xiaogpt.utils import calculate_tts_elapse
|
22
|
-
|
23
|
-
logger = logging.getLogger(__name__)
|
24
|
-
|
25
|
-
|
26
|
-
class VolcTTS(AudioFileTTS):
|
27
|
-
def __init__(self, mina_service, device_id, config):
|
28
|
-
super().__init__(mina_service, device_id, config)
|
29
|
-
self.token = get_token(config)
|
30
|
-
self.client = httpx.Client()
|
31
|
-
logger.info("Initializing VolcTTS {self.token}")
|
32
|
-
|
33
|
-
async def make_audio_file(self, query: str, text: str) -> tuple[Path, float]:
|
34
|
-
tts_payload = json.dumps(
|
35
|
-
{
|
36
|
-
"text": text,
|
37
|
-
"speaker": self.config.volc_tts_speaker,
|
38
|
-
"audio_config": {
|
39
|
-
"format": "mp3",
|
40
|
-
"sample_rate": 24000,
|
41
|
-
"speech_rate": 0,
|
42
|
-
},
|
43
|
-
}
|
44
|
-
)
|
45
|
-
|
46
|
-
req = {
|
47
|
-
"appkey": self.config.volc_tts_app,
|
48
|
-
"token": self.token,
|
49
|
-
"namespace": "TTS",
|
50
|
-
"payload": tts_payload,
|
51
|
-
}
|
52
|
-
|
53
|
-
resp = self.client.post("https://sami.bytedance.com/api/v1/invoke", json=req)
|
54
|
-
try:
|
55
|
-
sami_resp = resp.json()
|
56
|
-
logger.info(f"volc sami_resp {resp.status_code}")
|
57
|
-
if resp.status_code != 200:
|
58
|
-
print(sami_resp)
|
59
|
-
except:
|
60
|
-
logger.error(f"Failed to get tts from volcengine with voice=zh {text}")
|
61
|
-
|
62
|
-
if sami_resp["status_code"] == 20000000 and len(sami_resp["data"]) > 0:
|
63
|
-
audio_data = base64.b64decode(sami_resp["data"])
|
64
|
-
with tempfile.NamedTemporaryFile(
|
65
|
-
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
66
|
-
) as f:
|
67
|
-
f.write(audio_data)
|
68
|
-
|
69
|
-
return Path(f.name), calculate_tts_elapse(text)
|
70
|
-
|
71
|
-
|
72
|
-
## fetch token and save it to file
|
73
|
-
## it's aimed to reduce the request to volcengine
|
74
|
-
## it'll throw error if token is requested too frequently (more than 1 times per minute)
|
75
|
-
def get_token(config):
|
76
|
-
token_file = Path.home() / ".volc.token"
|
77
|
-
if not Path.exists(token_file):
|
78
|
-
token = request_token_data(config)
|
79
|
-
else:
|
80
|
-
with open(token_file, "r") as f:
|
81
|
-
token = json.load(f)
|
82
|
-
if token["expires_at"] < time.time():
|
83
|
-
token = request_token_data(config)
|
84
|
-
|
85
|
-
if not Path.exists(token_file):
|
86
|
-
with open(token_file, "w") as f:
|
87
|
-
json.dump(token, f)
|
88
|
-
return token["token"]
|
89
|
-
|
90
|
-
|
91
|
-
def request_token_data(config):
|
92
|
-
sami_service = SAMIService()
|
93
|
-
sami_service.set_ak(config.volc_accesskey)
|
94
|
-
sami_service.set_sk(config.volc_secretkey)
|
95
|
-
|
96
|
-
req = {
|
97
|
-
"appkey": config.volc_tts_app,
|
98
|
-
"token_version": "volc-auth-v1",
|
99
|
-
"expiration": 24 * 3600,
|
100
|
-
}
|
101
|
-
token = sami_service.common_json_handler("GetToken", req)
|
102
|
-
logger.info(f"Got token from volcengine {token}")
|
103
|
-
return token
|
104
|
-
|
105
|
-
|
106
|
-
class SAMIService(Service):
|
107
|
-
def __init__(self):
|
108
|
-
self.service_info = ServiceInfo(
|
109
|
-
"open.volcengineapi.com",
|
110
|
-
{},
|
111
|
-
Credentials("", "", "sami", "cn-north-1"),
|
112
|
-
10,
|
113
|
-
10,
|
114
|
-
)
|
115
|
-
self.api_info = {
|
116
|
-
"GetToken": ApiInfo(
|
117
|
-
"POST", "/", {"Action": "GetToken", "Version": "2021-07-27"}, {}, {}
|
118
|
-
),
|
119
|
-
}
|
120
|
-
super(SAMIService, self).__init__(self.service_info, self.api_info)
|
121
|
-
|
122
|
-
def common_json_handler(self, api, body):
|
123
|
-
params = dict()
|
124
|
-
try:
|
125
|
-
body = json.dumps(body)
|
126
|
-
res = self.json(api, params, body)
|
127
|
-
res_json = json.loads(res)
|
128
|
-
return res_json
|
129
|
-
except Exception as e:
|
130
|
-
raise Exception(str(e))
|
File without changes
|
File without changes
|
File without changes
|