xiaogpt 3.2__tar.gz → 3.4__tar.gz
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-3.2 → xiaogpt-3.4}/PKG-INFO +16 -13
- {xiaogpt-3.2 → xiaogpt-3.4}/pyproject.toml +16 -13
- xiaogpt-3.4/xiaogpt/tts/__init__.py +6 -0
- xiaogpt-3.4/xiaogpt/tts/base.py +51 -0
- xiaogpt-3.2/xiaogpt/tts/base.py → xiaogpt-3.4/xiaogpt/tts/file.py +20 -57
- xiaogpt-3.4/xiaogpt/tts/live.py +98 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/xiaogpt.py +6 -3
- xiaogpt-3.2/xiaogpt/tts/__init__.py +0 -5
- xiaogpt-3.2/xiaogpt/tts/tetos.py +0 -31
- {xiaogpt-3.2 → xiaogpt-3.4}/LICENSE +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/README.md +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/__init__.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/__main__.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/__init__.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/base_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/chatgptapi_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/doubao_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/gemini_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/glm_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/langchain_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/llama_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/moonshot_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/qwen_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/bot/yi_bot.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/cli.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/config.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/langchain/callbacks.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/langchain/chain.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/langchain/examples/email/mail_box.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/langchain/examples/email/mail_summary_tools.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/tts/mi.py +0 -0
- {xiaogpt-3.2 → xiaogpt-3.4}/xiaogpt/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: xiaogpt
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.4
|
4
4
|
Summary: Play ChatGPT or other LLM with xiaomi AI speaker
|
5
5
|
Author-Email: yihong0618 <zouzou0208@gmail.com>
|
6
6
|
License: MIT
|
@@ -28,7 +28,7 @@ Requires-Dist: langchain-community>=0.0.38
|
|
28
28
|
Requires-Dist: lingua-language-detector>=2.0.2; python_version < "3.13"
|
29
29
|
Provides-Extra: locked
|
30
30
|
Requires-Dist: aiohappyeyeballs==2.4.0; extra == "locked"
|
31
|
-
Requires-Dist: aiohttp==3.10.
|
31
|
+
Requires-Dist: aiohttp==3.10.10; extra == "locked"
|
32
32
|
Requires-Dist: aiosignal==1.3.1; extra == "locked"
|
33
33
|
Requires-Dist: annotated-types==0.6.0; extra == "locked"
|
34
34
|
Requires-Dist: anyio==4.3.0; extra == "locked"
|
@@ -47,14 +47,14 @@ Requires-Dist: distro==1.9.0; extra == "locked"
|
|
47
47
|
Requires-Dist: edge-tts==6.1.10; extra == "locked"
|
48
48
|
Requires-Dist: exceptiongroup==1.2.0; python_version < "3.11" and extra == "locked"
|
49
49
|
Requires-Dist: frozenlist==1.4.1; extra == "locked"
|
50
|
-
Requires-Dist: google-ai-generativelanguage==0.6.
|
50
|
+
Requires-Dist: google-ai-generativelanguage==0.6.10; extra == "locked"
|
51
51
|
Requires-Dist: google-api-core==2.15.0; extra == "locked"
|
52
52
|
Requires-Dist: google-api-core[grpc]==2.15.0; extra == "locked"
|
53
53
|
Requires-Dist: google-api-python-client==2.125.0; extra == "locked"
|
54
54
|
Requires-Dist: google-auth==2.26.1; extra == "locked"
|
55
55
|
Requires-Dist: google-auth-httplib2==0.2.0; extra == "locked"
|
56
56
|
Requires-Dist: google-cloud-texttospeech==2.16.3; extra == "locked"
|
57
|
-
Requires-Dist: google-generativeai==0.8.
|
57
|
+
Requires-Dist: google-generativeai==0.8.3; extra == "locked"
|
58
58
|
Requires-Dist: google-search-results==2.4.2; extra == "locked"
|
59
59
|
Requires-Dist: googleapis-common-protos==1.62.0; extra == "locked"
|
60
60
|
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"
|
@@ -65,16 +65,17 @@ Requires-Dist: h11==0.14.0; extra == "locked"
|
|
65
65
|
Requires-Dist: httpcore==1.0.5; extra == "locked"
|
66
66
|
Requires-Dist: httplib2==0.22.0; extra == "locked"
|
67
67
|
Requires-Dist: httpx==0.27.2; extra == "locked"
|
68
|
+
Requires-Dist: httpx-ws==0.6.2; extra == "locked"
|
68
69
|
Requires-Dist: httpx[socks]==0.27.2; extra == "locked"
|
69
70
|
Requires-Dist: idna==3.7; extra == "locked"
|
70
71
|
Requires-Dist: jiter==0.5.0; extra == "locked"
|
71
72
|
Requires-Dist: jsonpatch==1.33; extra == "locked"
|
72
73
|
Requires-Dist: jsonpointer==2.4; extra == "locked"
|
73
|
-
Requires-Dist: langchain==0.3.
|
74
|
-
Requires-Dist: langchain-community==0.3.
|
75
|
-
Requires-Dist: langchain-core==0.3.
|
74
|
+
Requires-Dist: langchain==0.3.3; extra == "locked"
|
75
|
+
Requires-Dist: langchain-community==0.3.2; extra == "locked"
|
76
|
+
Requires-Dist: langchain-core==0.3.10; extra == "locked"
|
76
77
|
Requires-Dist: langchain-text-splitters==0.3.0; extra == "locked"
|
77
|
-
Requires-Dist: langsmith==0.1.
|
78
|
+
Requires-Dist: langsmith==0.1.133; extra == "locked"
|
78
79
|
Requires-Dist: lingua-language-detector==2.0.2; python_version < "3.13" and extra == "locked"
|
79
80
|
Requires-Dist: markdown-it-py==3.0.0; extra == "locked"
|
80
81
|
Requires-Dist: marshmallow==3.20.1; extra == "locked"
|
@@ -85,10 +86,11 @@ Requires-Dist: mutagen==1.47.0; extra == "locked"
|
|
85
86
|
Requires-Dist: mypy-extensions==1.0.0; extra == "locked"
|
86
87
|
Requires-Dist: numexpr==2.10.1; extra == "locked"
|
87
88
|
Requires-Dist: numpy==1.26.3; extra == "locked"
|
88
|
-
Requires-Dist: openai==1.
|
89
|
+
Requires-Dist: openai==1.51.2; extra == "locked"
|
89
90
|
Requires-Dist: orjson==3.10.0; extra == "locked"
|
90
91
|
Requires-Dist: ormsgpack==1.5.0; extra == "locked"
|
91
92
|
Requires-Dist: packaging==23.2; extra == "locked"
|
93
|
+
Requires-Dist: propcache==0.2.0; extra == "locked"
|
92
94
|
Requires-Dist: proto-plus==1.23.0; extra == "locked"
|
93
95
|
Requires-Dist: protobuf==4.25.1; extra == "locked"
|
94
96
|
Requires-Dist: pyasn1==0.5.1; extra == "locked"
|
@@ -102,22 +104,23 @@ Requires-Dist: pyparsing==3.1.2; python_version > "3.0" and extra == "locked"
|
|
102
104
|
Requires-Dist: python-dotenv==1.0.1; extra == "locked"
|
103
105
|
Requires-Dist: pyyaml==6.0.2; extra == "locked"
|
104
106
|
Requires-Dist: requests==2.31.0; extra == "locked"
|
105
|
-
Requires-Dist:
|
107
|
+
Requires-Dist: requests-toolbelt==1.0.0; extra == "locked"
|
108
|
+
Requires-Dist: rich==13.9.2; extra == "locked"
|
106
109
|
Requires-Dist: rsa==4.9; extra == "locked"
|
107
110
|
Requires-Dist: sniffio==1.3.0; extra == "locked"
|
108
111
|
Requires-Dist: socksio==1.0.0; extra == "locked"
|
109
112
|
Requires-Dist: soupsieve==2.5; extra == "locked"
|
110
113
|
Requires-Dist: sqlalchemy==2.0.25; extra == "locked"
|
111
114
|
Requires-Dist: tenacity==8.2.3; extra == "locked"
|
112
|
-
Requires-Dist: tetos==0.
|
115
|
+
Requires-Dist: tetos==0.4.1; extra == "locked"
|
113
116
|
Requires-Dist: tqdm==4.66.1; extra == "locked"
|
114
117
|
Requires-Dist: typing-extensions==4.12.2; extra == "locked"
|
115
118
|
Requires-Dist: typing-inspect==0.9.0; extra == "locked"
|
116
119
|
Requires-Dist: uritemplate==4.1.1; extra == "locked"
|
117
120
|
Requires-Dist: urllib3==2.1.0; extra == "locked"
|
118
121
|
Requires-Dist: websocket-client==1.8.0; extra == "locked"
|
119
|
-
Requires-Dist:
|
120
|
-
Requires-Dist: yarl==1.
|
122
|
+
Requires-Dist: wsproto==1.2.0; extra == "locked"
|
123
|
+
Requires-Dist: yarl==1.14.0; extra == "locked"
|
121
124
|
Requires-Dist: zhipuai==2.1.5.20230904; extra == "locked"
|
122
125
|
Description-Content-Type: text/markdown
|
123
126
|
|
@@ -31,7 +31,7 @@ dependencies = [
|
|
31
31
|
"lingua-language-detector>=2.0.2; python_version < \"3.13\"",
|
32
32
|
]
|
33
33
|
dynamic = []
|
34
|
-
version = "3.
|
34
|
+
version = "3.4"
|
35
35
|
|
36
36
|
[project.license]
|
37
37
|
text = "MIT"
|
@@ -45,7 +45,7 @@ xiaogpt = "xiaogpt.cli:main"
|
|
45
45
|
[project.optional-dependencies]
|
46
46
|
locked = [
|
47
47
|
"aiohappyeyeballs==2.4.0",
|
48
|
-
"aiohttp==3.10.
|
48
|
+
"aiohttp==3.10.10",
|
49
49
|
"aiosignal==1.3.1",
|
50
50
|
"annotated-types==0.6.0",
|
51
51
|
"anyio==4.3.0",
|
@@ -64,14 +64,14 @@ locked = [
|
|
64
64
|
"edge-tts==6.1.10",
|
65
65
|
"exceptiongroup==1.2.0 ; python_version < \"3.11\"",
|
66
66
|
"frozenlist==1.4.1",
|
67
|
-
"google-ai-generativelanguage==0.6.
|
67
|
+
"google-ai-generativelanguage==0.6.10",
|
68
68
|
"google-api-core==2.15.0",
|
69
69
|
"google-api-core[grpc]==2.15.0",
|
70
70
|
"google-api-python-client==2.125.0",
|
71
71
|
"google-auth==2.26.1",
|
72
72
|
"google-auth-httplib2==0.2.0",
|
73
73
|
"google-cloud-texttospeech==2.16.3",
|
74
|
-
"google-generativeai==0.8.
|
74
|
+
"google-generativeai==0.8.3",
|
75
75
|
"google-search-results==2.4.2",
|
76
76
|
"googleapis-common-protos==1.62.0",
|
77
77
|
"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\"",
|
@@ -82,16 +82,17 @@ locked = [
|
|
82
82
|
"httpcore==1.0.5",
|
83
83
|
"httplib2==0.22.0",
|
84
84
|
"httpx==0.27.2",
|
85
|
+
"httpx-ws==0.6.2",
|
85
86
|
"httpx[socks]==0.27.2",
|
86
87
|
"idna==3.7",
|
87
88
|
"jiter==0.5.0",
|
88
89
|
"jsonpatch==1.33",
|
89
90
|
"jsonpointer==2.4",
|
90
|
-
"langchain==0.3.
|
91
|
-
"langchain-community==0.3.
|
92
|
-
"langchain-core==0.3.
|
91
|
+
"langchain==0.3.3",
|
92
|
+
"langchain-community==0.3.2",
|
93
|
+
"langchain-core==0.3.10",
|
93
94
|
"langchain-text-splitters==0.3.0",
|
94
|
-
"langsmith==0.1.
|
95
|
+
"langsmith==0.1.133",
|
95
96
|
"lingua-language-detector==2.0.2 ; python_version < \"3.13\"",
|
96
97
|
"markdown-it-py==3.0.0",
|
97
98
|
"marshmallow==3.20.1",
|
@@ -102,10 +103,11 @@ locked = [
|
|
102
103
|
"mypy-extensions==1.0.0",
|
103
104
|
"numexpr==2.10.1",
|
104
105
|
"numpy==1.26.3",
|
105
|
-
"openai==1.
|
106
|
+
"openai==1.51.2",
|
106
107
|
"orjson==3.10.0",
|
107
108
|
"ormsgpack==1.5.0",
|
108
109
|
"packaging==23.2",
|
110
|
+
"propcache==0.2.0",
|
109
111
|
"proto-plus==1.23.0",
|
110
112
|
"protobuf==4.25.1",
|
111
113
|
"pyasn1==0.5.1",
|
@@ -119,22 +121,23 @@ locked = [
|
|
119
121
|
"python-dotenv==1.0.1",
|
120
122
|
"pyyaml==6.0.2",
|
121
123
|
"requests==2.31.0",
|
122
|
-
"
|
124
|
+
"requests-toolbelt==1.0.0",
|
125
|
+
"rich==13.9.2",
|
123
126
|
"rsa==4.9",
|
124
127
|
"sniffio==1.3.0",
|
125
128
|
"socksio==1.0.0",
|
126
129
|
"soupsieve==2.5",
|
127
130
|
"sqlalchemy==2.0.25",
|
128
131
|
"tenacity==8.2.3",
|
129
|
-
"tetos==0.
|
132
|
+
"tetos==0.4.1",
|
130
133
|
"tqdm==4.66.1",
|
131
134
|
"typing-extensions==4.12.2",
|
132
135
|
"typing-inspect==0.9.0",
|
133
136
|
"uritemplate==4.1.1",
|
134
137
|
"urllib3==2.1.0",
|
135
138
|
"websocket-client==1.8.0",
|
136
|
-
"
|
137
|
-
"yarl==1.
|
139
|
+
"wsproto==1.2.0",
|
140
|
+
"yarl==1.14.0",
|
138
141
|
"zhipuai==2.1.5.20230904",
|
139
142
|
]
|
140
143
|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import abc
|
4
|
+
import asyncio
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
from typing import TYPE_CHECKING, AsyncIterator
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from typing import TypeVar
|
11
|
+
|
12
|
+
from miservice import MiNAService
|
13
|
+
|
14
|
+
from xiaogpt.config import Config
|
15
|
+
|
16
|
+
T = TypeVar("T", bound="TTS")
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class TTS(abc.ABC):
|
22
|
+
"""An abstract base class for Text-to-Speech models."""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self, mina_service: MiNAService, device_id: str, config: Config
|
26
|
+
) -> None:
|
27
|
+
self.mina_service = mina_service
|
28
|
+
self.device_id = device_id
|
29
|
+
self.config = config
|
30
|
+
|
31
|
+
async def wait_for_duration(self, duration: float) -> None:
|
32
|
+
"""Wait for the specified duration."""
|
33
|
+
await asyncio.sleep(duration)
|
34
|
+
while True:
|
35
|
+
if not await self.get_if_xiaoai_is_playing():
|
36
|
+
break
|
37
|
+
await asyncio.sleep(1)
|
38
|
+
|
39
|
+
async def get_if_xiaoai_is_playing(self) -> bool:
|
40
|
+
playing_info = await self.mina_service.player_get_status(self.device_id)
|
41
|
+
# WTF xiaomi api
|
42
|
+
is_playing = (
|
43
|
+
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
|
44
|
+
== 1
|
45
|
+
)
|
46
|
+
return is_playing
|
47
|
+
|
48
|
+
@abc.abstractmethod
|
49
|
+
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
50
|
+
"""Synthesize speech from a stream of text."""
|
51
|
+
raise NotImplementedError
|
@@ -1,10 +1,5 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import abc
|
4
1
|
import asyncio
|
5
2
|
import functools
|
6
|
-
import json
|
7
|
-
import logging
|
8
3
|
import os
|
9
4
|
import random
|
10
5
|
import socket
|
@@ -12,53 +7,13 @@ import tempfile
|
|
12
7
|
import threading
|
13
8
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
14
9
|
from pathlib import Path
|
15
|
-
from typing import
|
16
|
-
|
17
|
-
from xiaogpt.utils import get_hostname
|
18
|
-
|
19
|
-
if TYPE_CHECKING:
|
20
|
-
from typing import TypeVar
|
21
|
-
|
22
|
-
from miservice import MiNAService
|
23
|
-
|
24
|
-
from xiaogpt.config import Config
|
25
|
-
|
26
|
-
T = TypeVar("T", bound="TTS")
|
27
|
-
|
28
|
-
logger = logging.getLogger(__name__)
|
29
|
-
|
30
|
-
|
31
|
-
class TTS(abc.ABC):
|
32
|
-
"""An abstract base class for Text-to-Speech models."""
|
33
|
-
|
34
|
-
def __init__(
|
35
|
-
self, mina_service: MiNAService, device_id: str, config: Config
|
36
|
-
) -> None:
|
37
|
-
self.mina_service = mina_service
|
38
|
-
self.device_id = device_id
|
39
|
-
self.config = config
|
10
|
+
from typing import AsyncIterator
|
40
11
|
|
41
|
-
|
42
|
-
"""Wait for the specified duration."""
|
43
|
-
await asyncio.sleep(duration)
|
44
|
-
while True:
|
45
|
-
if not await self.get_if_xiaoai_is_playing():
|
46
|
-
break
|
47
|
-
await asyncio.sleep(1)
|
48
|
-
|
49
|
-
async def get_if_xiaoai_is_playing(self):
|
50
|
-
playing_info = await self.mina_service.player_get_status(self.device_id)
|
51
|
-
# WTF xiaomi api
|
52
|
-
is_playing = (
|
53
|
-
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
|
54
|
-
== 1
|
55
|
-
)
|
56
|
-
return is_playing
|
12
|
+
from miservice import MiNAService
|
57
13
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
raise NotImplementedError
|
14
|
+
from xiaogpt.config import Config
|
15
|
+
from xiaogpt.tts.base import TTS, logger
|
16
|
+
from xiaogpt.utils import get_hostname
|
62
17
|
|
63
18
|
|
64
19
|
class HTTPRequestHandler(SimpleHTTPRequestHandler):
|
@@ -76,23 +31,31 @@ class HTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
76
31
|
pass
|
77
32
|
|
78
33
|
|
79
|
-
class
|
34
|
+
class TetosFileTTS(TTS):
|
80
35
|
"""A TTS model that generates audio files locally and plays them via URL."""
|
81
36
|
|
82
37
|
def __init__(
|
83
38
|
self, mina_service: MiNAService, device_id: str, config: Config
|
84
39
|
) -> None:
|
40
|
+
from tetos import get_speaker
|
41
|
+
|
85
42
|
super().__init__(mina_service, device_id, config)
|
86
43
|
self.dirname = tempfile.TemporaryDirectory(prefix="xiaogpt-tts-")
|
87
44
|
self._start_http_server()
|
88
45
|
|
89
|
-
|
46
|
+
assert config.tts and config.tts != "mi"
|
47
|
+
speaker_cls = get_speaker(config.tts)
|
48
|
+
try:
|
49
|
+
self.speaker = speaker_cls(**config.tts_options)
|
50
|
+
except TypeError as e:
|
51
|
+
raise ValueError(f"{e}. Please add them via `tts_options` config") from e
|
52
|
+
|
90
53
|
async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
54
|
+
output_file = tempfile.NamedTemporaryFile(
|
55
|
+
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
56
|
+
)
|
57
|
+
duration = await self.speaker.synthesize(text, output_file.name, lang=lang)
|
58
|
+
return Path(output_file.name), duration
|
96
59
|
|
97
60
|
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
98
61
|
queue: asyncio.Queue[tuple[str, float]] = asyncio.Queue()
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import queue
|
4
|
+
import random
|
5
|
+
import threading
|
6
|
+
import uuid
|
7
|
+
from functools import lru_cache
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
9
|
+
from typing import AsyncIterator
|
10
|
+
|
11
|
+
from miservice import MiNAService
|
12
|
+
|
13
|
+
from xiaogpt.config import Config
|
14
|
+
from xiaogpt.tts.base import TTS, logger
|
15
|
+
from xiaogpt.utils import get_hostname
|
16
|
+
|
17
|
+
|
18
|
+
@lru_cache(maxsize=64)
|
19
|
+
def get_queue(key: str) -> queue.Queue[bytes]:
|
20
|
+
return queue.Queue()
|
21
|
+
|
22
|
+
|
23
|
+
class HTTPRequestHandler(BaseHTTPRequestHandler):
|
24
|
+
def do_GET(self):
|
25
|
+
self.send_response(200)
|
26
|
+
self.send_header("Content-type", "audio/mpeg")
|
27
|
+
self.end_headers()
|
28
|
+
key = self.path.split("/")[-1]
|
29
|
+
queue = get_queue(key)
|
30
|
+
while True:
|
31
|
+
chunk = queue.get()
|
32
|
+
if chunk == b"":
|
33
|
+
break
|
34
|
+
self.wfile.write(chunk)
|
35
|
+
|
36
|
+
def log_message(self, format, *args):
|
37
|
+
logger.debug(f"{self.address_string()} - {format}", *args)
|
38
|
+
|
39
|
+
def log_error(self, format, *args):
|
40
|
+
logger.error(f"{self.address_string()} - {format}", *args)
|
41
|
+
|
42
|
+
|
43
|
+
class TetosLiveTTS(TTS):
|
44
|
+
"""A TTS model that generates audio in real-time."""
|
45
|
+
|
46
|
+
def __init__(
|
47
|
+
self, mina_service: MiNAService, device_id: str, config: Config
|
48
|
+
) -> None:
|
49
|
+
from tetos import get_speaker
|
50
|
+
|
51
|
+
super().__init__(mina_service, device_id, config)
|
52
|
+
self._start_http_server()
|
53
|
+
|
54
|
+
assert config.tts and config.tts != "mi"
|
55
|
+
speaker_cls = get_speaker(config.tts)
|
56
|
+
try:
|
57
|
+
self.speaker = speaker_cls(**config.tts_options)
|
58
|
+
except TypeError as e:
|
59
|
+
raise ValueError(f"{e}. Please add them via `tts_options` config") from e
|
60
|
+
if not hasattr(self.speaker, "live"):
|
61
|
+
raise ValueError(f"{config.tts} Speaker does not support live synthesis")
|
62
|
+
|
63
|
+
async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
|
64
|
+
key = str(uuid.uuid4())
|
65
|
+
queue = get_queue(key)
|
66
|
+
|
67
|
+
async def worker():
|
68
|
+
async for chunk in self.speaker.live(text_stream, lang):
|
69
|
+
queue.put(chunk)
|
70
|
+
queue.put(b"")
|
71
|
+
|
72
|
+
task = asyncio.create_task(worker())
|
73
|
+
await self.mina_service.play_by_url(
|
74
|
+
self.device_id, f"http://{self.hostname}:{self.port}/{key}", _type=1
|
75
|
+
)
|
76
|
+
|
77
|
+
while True:
|
78
|
+
if await self.get_if_xiaoai_is_playing():
|
79
|
+
await asyncio.sleep(1)
|
80
|
+
else:
|
81
|
+
break
|
82
|
+
await task
|
83
|
+
|
84
|
+
def _start_http_server(self):
|
85
|
+
# set the port range
|
86
|
+
port_range = range(8050, 8090)
|
87
|
+
# get a random port from the range
|
88
|
+
self.port = int(os.getenv("XIAOGPT_PORT", random.choice(port_range)))
|
89
|
+
# create the server
|
90
|
+
handler = HTTPRequestHandler
|
91
|
+
httpd = ThreadingHTTPServer(("", self.port), handler)
|
92
|
+
# start the server in a new thread
|
93
|
+
server_thread = threading.Thread(target=httpd.serve_forever)
|
94
|
+
server_thread.daemon = True
|
95
|
+
server_thread.start()
|
96
|
+
|
97
|
+
self.hostname = get_hostname()
|
98
|
+
logger.info(f"Serving on {self.hostname}:{self.port}")
|
@@ -23,7 +23,8 @@ from xiaogpt.config import (
|
|
23
23
|
WAKEUP_KEYWORD,
|
24
24
|
Config,
|
25
25
|
)
|
26
|
-
from xiaogpt.tts import TTS, MiTTS,
|
26
|
+
from xiaogpt.tts import TTS, MiTTS, TetosFileTTS
|
27
|
+
from xiaogpt.tts.live import TetosLiveTTS
|
27
28
|
from xiaogpt.utils import detect_language, parse_cookie_string
|
28
29
|
|
29
30
|
EOF = object()
|
@@ -260,8 +261,10 @@ class MiGPT:
|
|
260
261
|
def tts(self) -> TTS:
|
261
262
|
if self.config.tts == "mi":
|
262
263
|
return MiTTS(self.mina_service, self.device_id, self.config)
|
264
|
+
elif self.config.tts == "fish":
|
265
|
+
return TetosLiveTTS(self.mina_service, self.device_id, self.config)
|
263
266
|
else:
|
264
|
-
return
|
267
|
+
return TetosFileTTS(self.mina_service, self.device_id, self.config)
|
265
268
|
|
266
269
|
async def wait_for_tts_finish(self):
|
267
270
|
while True:
|
@@ -274,7 +277,7 @@ class MiGPT:
|
|
274
277
|
message = message.strip().replace(" ", "--")
|
275
278
|
message = message.replace("\n", ",")
|
276
279
|
message = message.replace('"', ",")
|
277
|
-
message = message.replace(
|
280
|
+
message = message.replace("*", "")
|
278
281
|
return message
|
279
282
|
|
280
283
|
async def ask_gpt(self, query: str) -> AsyncIterator[str]:
|
xiaogpt-3.2/xiaogpt/tts/tetos.py
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import tempfile
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
from miservice import MiNAService
|
7
|
-
|
8
|
-
from xiaogpt.config import Config
|
9
|
-
from xiaogpt.tts.base import AudioFileTTS
|
10
|
-
|
11
|
-
|
12
|
-
class TetosTTS(AudioFileTTS):
|
13
|
-
def __init__(
|
14
|
-
self, mina_service: MiNAService, device_id: str, config: Config
|
15
|
-
) -> None:
|
16
|
-
from tetos import get_speaker
|
17
|
-
|
18
|
-
super().__init__(mina_service, device_id, config)
|
19
|
-
assert config.tts and config.tts != "mi"
|
20
|
-
speaker_cls = get_speaker(config.tts)
|
21
|
-
try:
|
22
|
-
self.speaker = speaker_cls(**config.tts_options)
|
23
|
-
except TypeError as e:
|
24
|
-
raise ValueError(f"{e}. Please add them via `tts_options` config") from e
|
25
|
-
|
26
|
-
async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
|
27
|
-
output_file = tempfile.NamedTemporaryFile(
|
28
|
-
suffix=".mp3", mode="wb", delete=False, dir=self.dirname.name
|
29
|
-
)
|
30
|
-
duration = await self.speaker.synthesize(text, output_file.name, lang=lang)
|
31
|
-
return Path(output_file.name), duration
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|