xiaogpt 3.3__py3-none-any.whl → 3.4__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/tts/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from xiaogpt.tts.base import TTS
2
+ from xiaogpt.tts.file import TetosFileTTS
3
+ from xiaogpt.tts.live import TetosLiveTTS
2
4
  from xiaogpt.tts.mi import MiTTS
3
- from xiaogpt.tts.tetos import TetosTTS
4
5
 
5
- __all__ = ["TTS", "TetosTTS", "MiTTS"]
6
+ __all__ = ["TTS", "TetosFileTTS", "MiTTS", "TetosLiveTTS"]
xiaogpt/tts/base.py CHANGED
@@ -2,20 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  import asyncio
5
- import functools
6
5
  import json
7
6
  import logging
8
- import os
9
- import random
10
- import socket
11
- import tempfile
12
- import threading
13
- from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
14
- from pathlib import Path
15
7
  from typing import TYPE_CHECKING, AsyncIterator
16
8
 
17
- from xiaogpt.utils import get_hostname
18
-
19
9
  if TYPE_CHECKING:
20
10
  from typing import TypeVar
21
11
 
@@ -46,7 +36,7 @@ class TTS(abc.ABC):
46
36
  break
47
37
  await asyncio.sleep(1)
48
38
 
49
- async def get_if_xiaoai_is_playing(self):
39
+ async def get_if_xiaoai_is_playing(self) -> bool:
50
40
  playing_info = await self.mina_service.player_get_status(self.device_id)
51
41
  # WTF xiaomi api
52
42
  is_playing = (
@@ -59,82 +49,3 @@ class TTS(abc.ABC):
59
49
  async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
60
50
  """Synthesize speech from a stream of text."""
61
51
  raise NotImplementedError
62
-
63
-
64
- class HTTPRequestHandler(SimpleHTTPRequestHandler):
65
- def log_message(self, format, *args):
66
- logger.debug(f"{self.address_string()} - {format}", *args)
67
-
68
- def log_error(self, format, *args):
69
- logger.error(f"{self.address_string()} - {format}", *args)
70
-
71
- def copyfile(self, source, outputfile):
72
- try:
73
- super().copyfile(source, outputfile)
74
- except (socket.error, ConnectionResetError, BrokenPipeError):
75
- # ignore this or TODO find out why the error later
76
- pass
77
-
78
-
79
- class AudioFileTTS(TTS):
80
- """A TTS model that generates audio files locally and plays them via URL."""
81
-
82
- def __init__(
83
- self, mina_service: MiNAService, device_id: str, config: Config
84
- ) -> None:
85
- super().__init__(mina_service, device_id, config)
86
- self.dirname = tempfile.TemporaryDirectory(prefix="xiaogpt-tts-")
87
- self._start_http_server()
88
-
89
- @abc.abstractmethod
90
- async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
91
- """Synthesize speech from text and save it to a file.
92
- Return the file path and the duration of the audio in seconds.
93
- The file path must be relative to the self.dirname.
94
- """
95
- raise NotImplementedError
96
-
97
- async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
98
- queue: asyncio.Queue[tuple[str, float]] = asyncio.Queue()
99
- finished = asyncio.Event()
100
-
101
- async def worker():
102
- async for text in text_stream:
103
- path, duration = await self.make_audio_file(lang, text)
104
- url = f"http://{self.hostname}:{self.port}/{path.name}"
105
- await queue.put((url, duration))
106
- finished.set()
107
-
108
- task = asyncio.create_task(worker())
109
-
110
- while True:
111
- try:
112
- url, duration = queue.get_nowait()
113
- except asyncio.QueueEmpty:
114
- if finished.is_set():
115
- break
116
- else:
117
- await asyncio.sleep(0.1)
118
- continue
119
- logger.debug("Playing URL %s (%s seconds)", url, duration)
120
- await asyncio.gather(
121
- self.mina_service.play_by_url(self.device_id, url, _type=1),
122
- self.wait_for_duration(duration),
123
- )
124
- await task
125
-
126
- def _start_http_server(self):
127
- # set the port range
128
- port_range = range(8050, 8090)
129
- # get a random port from the range
130
- self.port = int(os.getenv("XIAOGPT_PORT", random.choice(port_range)))
131
- # create the server
132
- handler = functools.partial(HTTPRequestHandler, directory=self.dirname.name)
133
- httpd = ThreadingHTTPServer(("", self.port), handler)
134
- # start the server in a new thread
135
- server_thread = threading.Thread(target=httpd.serve_forever)
136
- server_thread.daemon = True
137
- server_thread.start()
138
-
139
- self.hostname = get_hostname()
140
- logger.info(f"Serving on {self.hostname}:{self.port}")
xiaogpt/tts/file.py ADDED
@@ -0,0 +1,103 @@
1
+ import asyncio
2
+ import functools
3
+ import os
4
+ import random
5
+ import socket
6
+ import tempfile
7
+ import threading
8
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
9
+ from pathlib import Path
10
+ from typing import AsyncIterator
11
+
12
+ from miservice import MiNAService
13
+
14
+ from xiaogpt.config import Config
15
+ from xiaogpt.tts.base import TTS, logger
16
+ from xiaogpt.utils import get_hostname
17
+
18
+
19
+ class HTTPRequestHandler(SimpleHTTPRequestHandler):
20
+ def log_message(self, format, *args):
21
+ logger.debug(f"{self.address_string()} - {format}", *args)
22
+
23
+ def log_error(self, format, *args):
24
+ logger.error(f"{self.address_string()} - {format}", *args)
25
+
26
+ def copyfile(self, source, outputfile):
27
+ try:
28
+ super().copyfile(source, outputfile)
29
+ except (socket.error, ConnectionResetError, BrokenPipeError):
30
+ # ignore this or TODO find out why the error later
31
+ pass
32
+
33
+
34
+ class TetosFileTTS(TTS):
35
+ """A TTS model that generates audio files locally and plays them via URL."""
36
+
37
+ def __init__(
38
+ self, mina_service: MiNAService, device_id: str, config: Config
39
+ ) -> None:
40
+ from tetos import get_speaker
41
+
42
+ super().__init__(mina_service, device_id, config)
43
+ self.dirname = tempfile.TemporaryDirectory(prefix="xiaogpt-tts-")
44
+ self._start_http_server()
45
+
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
+
53
+ async def make_audio_file(self, lang: str, text: str) -> tuple[Path, float]:
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
59
+
60
+ async def synthesize(self, lang: str, text_stream: AsyncIterator[str]) -> None:
61
+ queue: asyncio.Queue[tuple[str, float]] = asyncio.Queue()
62
+ finished = asyncio.Event()
63
+
64
+ async def worker():
65
+ async for text in text_stream:
66
+ path, duration = await self.make_audio_file(lang, text)
67
+ url = f"http://{self.hostname}:{self.port}/{path.name}"
68
+ await queue.put((url, duration))
69
+ finished.set()
70
+
71
+ task = asyncio.create_task(worker())
72
+
73
+ while True:
74
+ try:
75
+ url, duration = queue.get_nowait()
76
+ except asyncio.QueueEmpty:
77
+ if finished.is_set():
78
+ break
79
+ else:
80
+ await asyncio.sleep(0.1)
81
+ continue
82
+ logger.debug("Playing URL %s (%s seconds)", url, duration)
83
+ await asyncio.gather(
84
+ self.mina_service.play_by_url(self.device_id, url, _type=1),
85
+ self.wait_for_duration(duration),
86
+ )
87
+ await task
88
+
89
+ def _start_http_server(self):
90
+ # set the port range
91
+ port_range = range(8050, 8090)
92
+ # get a random port from the range
93
+ self.port = int(os.getenv("XIAOGPT_PORT", random.choice(port_range)))
94
+ # create the server
95
+ handler = functools.partial(HTTPRequestHandler, directory=self.dirname.name)
96
+ httpd = ThreadingHTTPServer(("", self.port), handler)
97
+ # start the server in a new thread
98
+ server_thread = threading.Thread(target=httpd.serve_forever)
99
+ server_thread.daemon = True
100
+ server_thread.start()
101
+
102
+ self.hostname = get_hostname()
103
+ logger.info(f"Serving on {self.hostname}:{self.port}")
xiaogpt/tts/live.py ADDED
@@ -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}")
xiaogpt/xiaogpt.py CHANGED
@@ -23,7 +23,8 @@ from xiaogpt.config import (
23
23
  WAKEUP_KEYWORD,
24
24
  Config,
25
25
  )
26
- from xiaogpt.tts import TTS, MiTTS, TetosTTS
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 TetosTTS(self.mina_service, self.device_id, self.config)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xiaogpt
3
- Version: 3.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
@@ -65,6 +65,7 @@ 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"
@@ -111,14 +112,14 @@ Requires-Dist: socksio==1.0.0; extra == "locked"
111
112
  Requires-Dist: soupsieve==2.5; extra == "locked"
112
113
  Requires-Dist: sqlalchemy==2.0.25; extra == "locked"
113
114
  Requires-Dist: tenacity==8.2.3; extra == "locked"
114
- Requires-Dist: tetos==0.3.1; extra == "locked"
115
+ Requires-Dist: tetos==0.4.1; extra == "locked"
115
116
  Requires-Dist: tqdm==4.66.1; extra == "locked"
116
117
  Requires-Dist: typing-extensions==4.12.2; extra == "locked"
117
118
  Requires-Dist: typing-inspect==0.9.0; extra == "locked"
118
119
  Requires-Dist: uritemplate==4.1.1; extra == "locked"
119
120
  Requires-Dist: urllib3==2.1.0; extra == "locked"
120
121
  Requires-Dist: websocket-client==1.8.0; extra == "locked"
121
- Requires-Dist: websockets==12.0; extra == "locked"
122
+ Requires-Dist: wsproto==1.2.0; extra == "locked"
122
123
  Requires-Dist: yarl==1.14.0; extra == "locked"
123
124
  Requires-Dist: zhipuai==2.1.5.20230904; extra == "locked"
124
125
  Description-Content-Type: text/markdown
@@ -1,7 +1,7 @@
1
- xiaogpt-3.3.dist-info/METADATA,sha256=5nJ9Wb_Io16JM6QtUo8sC7Xfz6CoHI01VBQFwAPvOVw,31468
2
- xiaogpt-3.3.dist-info/WHEEL,sha256=pM0IBB6ZwH3nkEPhtcp50KvKNX-07jYtnb1g1m6Z4Co,90
3
- xiaogpt-3.3.dist-info/entry_points.txt,sha256=q4WRS7kS4kQ5kZX57Fq40VrhCi74NZcyRPRX4JP2veo,61
4
- xiaogpt-3.3.dist-info/licenses/LICENSE,sha256=XdClh516MvlnOf9749JZHCxSB7y6_fyXcWmLDz6IkZY,1063
1
+ xiaogpt-3.4.dist-info/METADATA,sha256=JZdIj1t1yuHoWkRqXOzO0Uo6rS1Q3pJ52TlLJQ0VpPg,31516
2
+ xiaogpt-3.4.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ xiaogpt-3.4.dist-info/entry_points.txt,sha256=q4WRS7kS4kQ5kZX57Fq40VrhCi74NZcyRPRX4JP2veo,61
4
+ xiaogpt-3.4.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=BDGvj1JuWVw47qfREWGKnSXeiFg6DVJJAz2rHVryqmc,1160
@@ -21,10 +21,11 @@ xiaogpt/langchain/callbacks.py,sha256=yR9AXQt9OHVYBWC47Q1I_BUT4Xg9iM44vnW2vv0BLp
21
21
  xiaogpt/langchain/chain.py,sha256=z0cqRlL0ElWnf31ByxZBN7AKOT-svXQDt5_NDft_nYc,1495
22
22
  xiaogpt/langchain/examples/email/mail_box.py,sha256=xauqrjE4-G4XPQnokUPE-MZgAaHQ_VrUDLlbfYTdCoo,6372
23
23
  xiaogpt/langchain/examples/email/mail_summary_tools.py,sha256=6cWvBJUaA7iaywcHdbUoww8WiCtaNw3TmwyxyF4DY7E,1561
24
- xiaogpt/tts/__init__.py,sha256=xasHDrmgECirf1MSyrfURSaMBqtdZBi3cQNeDvPo_cQ,145
25
- xiaogpt/tts/base.py,sha256=k0ZUcLJZWU5U_fXu_w-cLFgZpE2KkV89ARbVDXLqTck,4665
24
+ xiaogpt/tts/__init__.py,sha256=75_W5ZhON87RSutiLhJB29Ub-634iI2IlTEZd0alao8,210
25
+ xiaogpt/tts/base.py,sha256=8vP8fIksSZttmrMaUT4vtiDbkfijkr9lbhyod1_5tc4,1440
26
+ xiaogpt/tts/file.py,sha256=pWzozktMAgJraPiANWQC5dB4EQcL9QCwJWbX_M4hqWE,3721
27
+ xiaogpt/tts/live.py,sha256=tmyW26TRTau1Ci_7aHiNyzv-UfzoaiWgnlxz4gA62yw,3109
26
28
  xiaogpt/tts/mi.py,sha256=1MzCB27DBohPQ_4Xz4W_FV9p-chJFDavOHB89NviLcM,1095
27
- xiaogpt/tts/tetos.py,sha256=fkuOSYGqAfJyyPEXbsiOS--CewGf1JUiahoN33nzOAA,1058
28
29
  xiaogpt/utils.py,sha256=YYmRDNtccxqB9gyN_xhKZwgL1_PNYEp7So_-jt2tiVg,2668
29
- xiaogpt/xiaogpt.py,sha256=ZSVsMzwSW0uxwW2VN4phXV1A-suyrwhE0jAMGtaUB5I,16196
30
- xiaogpt-3.3.dist-info/RECORD,,
30
+ xiaogpt/xiaogpt.py,sha256=Og982fJSlzbMhcjP_Hj5dj1dKS-31y4gXA8sZRwZOPI,16366
31
+ xiaogpt-3.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.2)
2
+ Generator: pdm-backend (2.4.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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