├── requirements.txt ├── Dockerfile ├── .github └── workflows │ └── CI.yaml ├── LICENSE ├── .gitignore ├── README.md └── xiaogpt.py /requirements.txt: -------------------------------------------------------------------------------- 1 | rich 2 | git+https://github.com/yihong0618/MiService 3 | requests 4 | revChatGPT 5 | openai 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | WORKDIR /app 3 | RUN pip install aiohttp 4 | COPY . . 5 | RUN pip install --no-cache-dir -r requirements.txt 6 | ENV OPENAI_API_KEY=$OPENAI_API_KEY 7 | ENV XDG_CONFIG_HOME=/config 8 | VOLUME /config 9 | ENTRYPOINT ["python3","xiaogpt.py"] -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | testing: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: install python 3.9 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.9' 19 | cache: 'pip' # caching pip dependencies 20 | - name: Check formatting (black) 21 | run: | 22 | pip install black 23 | black . --check 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 yihong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/ 131 | xiaogptconfig.json 132 | xiao_config.json 133 | xiao_config.json.example 134 | xiao_config.json.example 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xiaogpt 2 | Play ChatGPT with Xiaomi AI Speaker 3 | 4 | ![image](https://user-images.githubusercontent.com/15976103/220028375-c193a859-48a1-4270-95b6-ef540e54a621.png) 5 | 6 | ## Update by zsc 7 | 这个是可行命令 8 | `python3 xiaogpt.py --hardware LX04 --use_chatgpt_api --use_command` 9 | 10 | 11 | ## 一点原理 12 | 13 | [不用 root 使用小爱同学和 ChatGPT 交互折腾记](https://github.com/yihong0618/gitblog/issues/258) 14 | 15 | 16 | ## 准备 17 | 18 | 1. ChatGPT id 19 | 2. 小爱音响 20 | 3. 能正常联网的环境或 proxy 21 | 4. python3.8+ 22 | 23 | ## 使用 24 | 25 | 1. pip install aiohttp # 解决 miserver 依赖 26 | 2. pip install -r requirements.txt 27 | 3. 参考 [MiService](https://github.com/Yonsm/MiService) 项目 README 并在本地 terminal 跑 `micli list` 拿到你音响的 DID 成功 **别忘了设置 export MI_DID=xxx** 这个 MI_DID 用 28 | 4. 参考 [revChatGPT](https://github.com/acheong08/ChatGPT) 项目 README 配置 chatGPT 的 config 29 | 5. run `python xiaogpt.py --hardware ${your_hardware}` hardware 你看小爱屁股上有型号,输入进来 30 | 6. 跑起来之后就可以问小爱同学问题了,“帮我"开头的问题,会发送一份给 ChatGPT 然后小爱同学用 tts 回答 31 | 7. 因为现在必须指定 conversation_id 和 parent_id 来持续对话,会自动建一个新的 conversation 32 | 8. 如果上面不可用,可以尝试用手机抓包,https://userprofile.mina.mi.com/device_profile/v2/conversation 找到 cookie 利用 --cookie '${cookie}' cookie 别忘了用单引号包裹 33 | 9. 默认用目前 ubus, 如果你的设备不支持 ubus 可以使用 --use_command 来使用 command 来 tts 34 | 10. 使用 --mute_xiaoai 选项,可以让小爱不回答,但会频繁请求,玩一下可以使用,不建议一直用 35 | 11. 使用 --account ‘${account}’ --password ‘${password}’ 可以不进行步骤 2 36 | 12. 如果有能力可以自行替换唤醒词,也可以去掉唤醒词,源码在 https://github.com/yihong0618/xiaogpt/blob/main/xiaogpt.py#L32 37 | 13. 可以使用 gpt-3 的 api 那样可以更流畅的对话,速度快, 请 google 如何用 openai api, 命令 --use_gpt3 38 | 14. 可以使用 --use_chatgpt_api 的 api 那样可以更流畅的对话,速度特别快,达到了对话的体验, 请 google 如何用 openai api, 命令 --use_chatgpt_api 39 | 40 | e.g. 41 | ```shell 42 | python3 xiaogpt.py --hardware LX06; 43 | # or 44 | python3 xiaogpt.py --hardware LX06 --conversation_id="xxxxxxxx"; 45 | # or 46 | python3 xiaogpt.py --hardware LX06 --cookie ${cookie}; 47 | # 如果你想直接输入账号密码 48 | python3 xiaogpt.py --hardware LX06 --account ${your_xiaomi_account} --password ${your_password}; 49 | # 如果你想 mute 小米的回答 50 | python3 xiaogpt.py --hardware LX06 --mute_xiaoai 51 | # 如果你想使用 gpt3 ai 52 | export OPENAI_API_KEY=${your_api_key} 53 | python3 xiaogpt.py --hardware LX06 --mute_xiaoai --use_gpt3 54 | # 如果你想用 chatgpt api 55 | export OPENAI_API_KEY=${your_api_key} 56 | python3 xiaogpt.py --hardware LX06 --use_chatgpt_api 57 | ``` 58 | 59 | ## config.json 60 | 如果想通过单一配置文件启动也是可以的, 可以通过 --config 参数指定配置文件, config 文件必须是合法的 JSON 格式 61 | 参数优先级 62 | - cli args > default > config 63 | 64 | ```shell 65 | python3 xiaogpt.py --config xiao_config.json 66 | ``` 67 | 或者 68 | ```shell 69 | cp xiao_config.json.example xiao_config.json 70 | python3 xiaogpt.py 71 | ``` 72 | 73 | ## 注意 74 | 75 | 1. 请开启小爱同学的蓝牙 76 | 2. 如果要更改提示词和 PROMPT 在代码最上面自行更改 77 | 3. 目前已知 LX04 和 L05B L05C 可能需要使用 `--use_command` 78 | 79 | ## QA 80 | 81 | 1. 用破解么?不用 82 | 2. 连不上 revChatGPT?国情,你得设置 proxy 并且该地区可用的 proxy 83 | 3. 你做这玩意也没用啊?确实。。。但是挺好玩的,有用对你来说没用,对我们来说不一定呀 84 | 4. 想把它变得更好?PR Issue always welcome. 85 | 5. 还有问题?提 Issuse 哈哈 86 | 87 | ## 视频教程 88 | https://www.youtube.com/watch?v=K4YA8YwzOOA 89 | 90 | ## Docker 91 | 92 | ### 常规用法 93 | 94 | docker run -e OPENAI_API_KEY=< your-openapi-key > yihong0618/xiaogpt < 命令行参数 > 95 | 96 | 如 97 | 98 | ```shell 99 | docker run -e OPENAI_API_KEY= yihong0618/xiaogpt --account= --password= --hardware= --use_chatgpt_api 100 | ``` 101 | 102 | ### 使用配置文件 103 | 104 | 1.xiaogpt的配置文件可通过指定volume /config,以及指定参数--config来处理,如 105 | 106 | ```shell 107 | docker run -e OPENAI_API_KEY= -v :/config yihong0618/xiaogpt --account= --password= --hardware= --use_chatgpt_api --config=/config/config.json 108 | ``` 109 | 110 | 2.如果使用revChatGPT,则可通过指定volume /config,以及指定环境变量XDG_CONFIG_HOME来处理 ( **revChatGPT配置文件需要放置到/revChatGPT/config.json** ) ,如 111 | 112 | ```shell 113 | docker run -e XDG_CONFIG_HOME=/config -v :/config yihong0618/xiaogpt --account= --password= --hardware= --use_chatgpt_api --config=/config/config.json 114 | ``` 115 | 116 | # 感谢 117 | 118 | - [xiaomi](https://www.mi.com/) 119 | - @[Yonsm](https://github.com/Yonsm) 的 [MiService](https://github.com/Yonsm/MiService) 120 | 121 | ## 赞赏 122 | 123 | 谢谢就够了 124 | -------------------------------------------------------------------------------- /xiaogpt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import asyncio 4 | import json 5 | import os 6 | from os import environ as env 7 | import subprocess 8 | import time 9 | from http.cookies import SimpleCookie 10 | from pathlib import Path 11 | 12 | import openai 13 | from aiohttp import ClientSession 14 | from miservice import MiAccount, MiNAService 15 | from requests.utils import cookiejar_from_dict 16 | #from revChatGPT.V1 import Chatbot, configure 17 | from rich import print 18 | 19 | LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2" 20 | COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}" 21 | 22 | HARDWARE_COMMAND_DICT = { 23 | "LX06": "5-1", 24 | "L05B": "5-3", 25 | "S12A": "5-1", 26 | "LX01": "5-1", 27 | "L06A": "5-1", 28 | "LX04": "5-1", 29 | "L05C": "5-3", 30 | "L17A": "7-3", 31 | "X08E": "7-3", 32 | # add more here 33 | } 34 | MI_USER = "" 35 | MI_PASS = "" 36 | OPENAI_API_KEY = "" 37 | KEY_WORD = "帮我" 38 | PROMPT = "请用100字以内回答" 39 | 40 | # simulate the response from xiaoai server by type the input. 41 | CLI_INTERACTIVE_MODE = False 42 | 43 | 44 | ### HELP FUNCTION ### 45 | def parse_cookie_string(cookie_string): 46 | cookie = SimpleCookie() 47 | cookie.load(cookie_string) 48 | cookies_dict = {} 49 | cookiejar = None 50 | for k, m in cookie.items(): 51 | cookies_dict[k] = m.value 52 | cookiejar = cookiejar_from_dict(cookies_dict, cookiejar=None, overwrite=True) 53 | return cookiejar 54 | 55 | 56 | class GPT3Bot: 57 | def __init__(self, session): 58 | self.api_key = OPENAI_API_KEY 59 | self.api_url = "https://api.openai.com/v1/completions" 60 | self.headers = { 61 | "Content-Type": "application/json", 62 | "Authorization": f"Bearer {self.api_key}", 63 | } 64 | # TODO support more models here 65 | self.data = { 66 | "prompt": "", 67 | "model": "text-davinci-003", 68 | "max_tokens": 1024, 69 | "temperature": 1, 70 | "top_p": 1, 71 | } 72 | self.session = session 73 | 74 | async def ask(self, query): 75 | # TODO Support for continuous dialogue 76 | # pass all prompt and answers 77 | # PR welcome 78 | self.data["prompt"] = query 79 | r = await self.session.post(self.api_url, headers=self.headers, json=self.data) 80 | return await r.json() 81 | 82 | 83 | class ChatGPTBot: 84 | def __init__(self, session): 85 | self.session = session 86 | self.history = [] 87 | 88 | async def ask(self, query): 89 | openai.api_key = OPENAI_API_KEY 90 | ms = [] 91 | for h in self.history: 92 | ms.append({"role": "user", "content": h[0]}) 93 | ms.append({"role": "assistant", "content": h[1]}) 94 | ms.append({"role": "user", "content": f"{query}"}) 95 | completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=ms) 96 | message = ( 97 | completion["choices"][0] 98 | .get("message") 99 | .get("content") 100 | .encode("utf8") 101 | .decode() 102 | ) 103 | self.history.append([f"{query}", message]) 104 | # only keep 5 history 105 | self.history = self.history[-5:] 106 | return message 107 | 108 | 109 | class MiGPT: 110 | def __init__( 111 | self, 112 | hardware, 113 | cookie="", 114 | use_command=False, 115 | mute_xiaoai=False, 116 | use_gpt3=False, 117 | use_chatgpt_api=False, 118 | verbose=False, 119 | ): 120 | self.mi_token_home = Path.home() / ".mi.token" 121 | self.hardware = hardware 122 | self.cookie_string = "" 123 | self.last_timestamp = 0 # timestamp last call mi speaker 124 | self.session = None 125 | self.chatbot = None # a little slow to init we move it after xiaomi init 126 | self.user_id = "" 127 | self.device_id = "" 128 | self.service_token = "" 129 | self.cookie = cookie 130 | self.use_command = use_command 131 | self.tts_command = HARDWARE_COMMAND_DICT.get(hardware, "5-1") 132 | self.conversation_id = None 133 | self.parent_id = None 134 | self.miboy_account = None 135 | self.mina_service = None 136 | # try to mute xiaoai config 137 | self.mute_xiaoai = mute_xiaoai 138 | # mute xiaomi in runtime 139 | self.this_mute_xiaoai = mute_xiaoai 140 | # if use gpt3 api 141 | self.use_gpt3 = use_gpt3 142 | self.use_chatgpt_api = use_chatgpt_api 143 | self.verbose = verbose 144 | 145 | async def init_all_data(self, session): 146 | await self.login_miboy(session) 147 | await self._init_data_hardware() 148 | with open(self.mi_token_home) as f: 149 | user_data = json.loads(f.read()) 150 | self.user_id = user_data.get("userId") 151 | self.service_token = user_data.get("micoapi")[1] 152 | self._init_cookie() 153 | await self._init_first_data_and_chatbot() 154 | 155 | async def login_miboy(self, session): 156 | self.session = session 157 | self.account = MiAccount( 158 | session, 159 | MI_USER, 160 | MI_PASS, 161 | str(self.mi_token_home), 162 | ) 163 | # Forced login to refresh to refresh token 164 | await self.account.login("micoapi") 165 | self.mina_service = MiNAService(self.account) 166 | print(self.mina_service.__dict__) 167 | 168 | async def _init_data_hardware(self): 169 | if self.cookie: 170 | # if use cookie do not need init 171 | return 172 | hardware_data = await self.mina_service.device_list() 173 | for h in hardware_data: 174 | if h.get("hardware", "") == self.hardware: 175 | self.device_id = h.get("deviceID") 176 | break 177 | else: 178 | raise Exception(f"we have no hardware: {self.hardware} please check") 179 | 180 | def _init_cookie(self): 181 | if self.cookie: 182 | self.cookie = parse_cookie_string(self.cookie) 183 | else: 184 | self.cookie_string = COOKIE_TEMPLATE.format( 185 | device_id=self.device_id, 186 | service_token=self.service_token, 187 | user_id=self.user_id, 188 | ) 189 | self.cookie = parse_cookie_string(self.cookie_string) 190 | 191 | async def _init_first_data_and_chatbot(self): 192 | data = await self.get_latest_ask_from_xiaoai() 193 | self.last_timestamp, self.last_record = self.get_last_timestamp_and_record(data) 194 | # TODO refactor this 195 | if self.use_gpt3: 196 | self.chatbot = GPT3Bot(self.session) 197 | elif self.use_chatgpt_api: 198 | self.chatbot = ChatGPTBot(self.session) 199 | else: 200 | self.chatbot = Chatbot(configure()) 201 | 202 | async def simulate_xiaoai_question(self): 203 | data = { 204 | "code": 0, 205 | "message": "Success", 206 | "data": '{"bitSet":[0,1,1],"records":[{"bitSet":[0,1,1,1,1],"answers":[{"bitSet":[0,1,1,1],"type":"TTS","tts":{"bitSet":[0,1],"text":"Fake Answer"}}],"time":1677851434593,"query":"Fake Question","requestId":"fada34f8fa0c3f408ee6761ec7391d85"}],"nextEndTime":1677849207387}', 207 | } 208 | # Convert the data['data'] value from a string to a dictionary 209 | data_dict = json.loads(data["data"]) 210 | # Get the first item in the records list 211 | record = data_dict["records"][0] 212 | # Replace the query and time values with user input 213 | record["query"] = input("Enter the new query: ") 214 | record["time"] = int(time.time() * 1000) 215 | # Convert the updated data_dict back to a string and update the data['data'] value 216 | data["data"] = json.dumps(data_dict) 217 | await asyncio.sleep(1) 218 | 219 | return data 220 | 221 | async def get_latest_ask_from_xiaoai(self): 222 | if CLI_INTERACTIVE_MODE: 223 | r = await self.simulate_xiaoai_question() 224 | return r 225 | 226 | r = await self.session.get( 227 | LATEST_ASK_API.format( 228 | hardware=self.hardware, timestamp=str(int(time.time() * 1000)) 229 | ), 230 | cookies=parse_cookie_string(self.cookie), 231 | ) 232 | return await r.json() 233 | 234 | def get_last_timestamp_and_record(self, data): 235 | if d := data.get("data"): 236 | records = json.loads(d).get("records") 237 | if not records: 238 | return 0, None 239 | last_record = records[0] 240 | timestamp = last_record.get("time") 241 | return timestamp, last_record 242 | 243 | async def do_tts(self, value): 244 | if CLI_INTERACTIVE_MODE: 245 | print(f"do_tts, CLI_INTERACTIVE_MODE:{value}") 246 | await asyncio.sleep(2) 247 | return 248 | 249 | if not self.use_command: 250 | try: 251 | await self.mina_service.text_to_speech(self.device_id, value) 252 | except: 253 | # do nothing is ok 254 | pass 255 | else: 256 | subprocess.check_output(["micli.py", self.tts_command, value]) 257 | 258 | def _normalize(self, message): 259 | message = message.replace(" ", "--") 260 | message = message.replace("\n", ",") 261 | message = message.replace('"', ",") 262 | return message 263 | 264 | async def ask_gpt(self, query): 265 | if self.use_gpt3: 266 | return await self.ask_gpt3(query) 267 | elif self.use_chatgpt_api: 268 | return await self.ask_chatgpt_api(query) 269 | return await self.ask_chatgpt(query) 270 | 271 | async def ask_chatgpt_api(self, query): 272 | message = await self.chatbot.ask(query) 273 | message = self._normalize(message) 274 | return message 275 | 276 | async def ask_gpt3(self, query): 277 | data = await self.chatbot.ask(query) 278 | choices = data.get("choices") 279 | if not choices: 280 | print("No reply from gpt3") 281 | else: 282 | message = choices[0].get("text", "") 283 | message = self._normalize(message) 284 | return message 285 | 286 | async def ask_chatgpt(self, query): 287 | # TODO maybe use v2 to async it here 288 | if self.conversation_id and self.parent_id: 289 | data = list( 290 | self.chatbot.ask( 291 | query, 292 | conversation_id=self.conversation_id, 293 | parent_id=self.parent_id, 294 | ) 295 | )[-1] 296 | else: 297 | data = list(self.chatbot.ask(query))[-1] 298 | if message := data.get("message", ""): 299 | self.conversation_id = data.get("conversation_id") 300 | self.parent_id = data.get("parent_id") 301 | # xiaoai tts did not support space 302 | message = self._normalize(message) 303 | return message 304 | return "" 305 | 306 | async def get_if_xiaoai_is_playing(self): 307 | playing_info = await self.mina_service.player_get_status(self.device_id) 308 | # WTF xiaomi api 309 | is_playing = ( 310 | json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1) 311 | == 1 312 | ) 313 | return is_playing 314 | 315 | async def stop_if_xiaoai_is_playing(self): 316 | is_playing = await self.get_if_xiaoai_is_playing() 317 | if is_playing: 318 | # stop it 319 | await self.mina_service.player_pause(self.device_id) 320 | 321 | async def run_forever(self): 322 | print(f"Running xiaogpt now, 用`{KEY_WORD}`开头来提问") 323 | async with ClientSession() as session: 324 | await self.init_all_data(session) 325 | while 1: 326 | if self.verbose: 327 | print( 328 | f"Now listening xiaoai new message timestamp: {self.last_timestamp}" 329 | ) 330 | try: 331 | r = await self.get_latest_ask_from_xiaoai() 332 | except Exception: 333 | # we try to init all again 334 | await self.init_all_data(session) 335 | r = await self.get_latest_ask_from_xiaoai() 336 | # spider rule 337 | if not self.mute_xiaoai: 338 | await asyncio.sleep(3) 339 | else: 340 | await asyncio.sleep(0.3) 341 | 342 | new_timestamp, last_record = self.get_last_timestamp_and_record(r) 343 | if new_timestamp > self.last_timestamp: 344 | self.last_timestamp = new_timestamp 345 | query = last_record.get("query", "") 346 | if query.find(KEY_WORD) != -1: 347 | # only mute when your clause start's with the keyword 348 | if self.this_mute_xiaoai: 349 | await self.stop_if_xiaoai_is_playing() 350 | self.this_mute_xiaoai = False 351 | # drop 帮我回答 352 | query = query.replace(KEY_WORD, "") 353 | query = f"{query},{PROMPT}" 354 | # waiting for xiaoai speaker done 355 | if not self.mute_xiaoai: 356 | await asyncio.sleep(4) 357 | await self.do_tts("正在问GPT请耐心等待") 358 | try: 359 | print( 360 | "以下是小爱的回答: ", 361 | last_record.get("answers")[0] 362 | .get("tts", {}) 363 | .get("text"), 364 | ) 365 | except: 366 | print("小爱没回") 367 | message = await self.ask_gpt(query) 368 | # tts to xiaoai with ChatGPT answer 369 | print("以下是GPT的回答: " + message) 370 | await self.do_tts(message) 371 | if self.mute_xiaoai: 372 | while 1: 373 | is_playing = await self.get_if_xiaoai_is_playing() 374 | time.sleep(2) 375 | if not is_playing: 376 | break 377 | self.this_mute_xiaoai = True 378 | else: 379 | if self.verbose: 380 | print("No new xiao ai record") 381 | 382 | 383 | if __name__ == "__main__": 384 | parser = argparse.ArgumentParser() 385 | parser.add_argument( 386 | "--hardware", 387 | dest="hardware", 388 | type=str, 389 | default="", 390 | help="小爱 hardware", 391 | ) 392 | parser.add_argument( 393 | "--account", 394 | dest="account", 395 | type=str, 396 | default="", 397 | help="xiaomi account", 398 | ) 399 | parser.add_argument( 400 | "--password", 401 | dest="password", 402 | type=str, 403 | default="", 404 | help="xiaomi password", 405 | ) 406 | parser.add_argument( 407 | "--openai_key", 408 | dest="openai_key", 409 | type=str, 410 | default="", 411 | help="openai api key", 412 | ) 413 | parser.add_argument( 414 | "--cookie", 415 | dest="cookie", 416 | type=str, 417 | default="", 418 | help="xiaomi cookie", 419 | ) 420 | parser.add_argument( 421 | "--use_command", 422 | dest="use_command", 423 | action="store_true", 424 | help="use command to tts", 425 | ) 426 | parser.add_argument( 427 | "--mute_xiaoai", 428 | dest="mute_xiaoai", 429 | action="store_true", 430 | help="try to mute xiaoai answer", 431 | ) 432 | parser.add_argument( 433 | "--verbose", 434 | dest="verbose", 435 | action="store_true", 436 | help="show info", 437 | ) 438 | parser.add_argument( 439 | "--use_gpt3", 440 | dest="use_gpt3", 441 | action="store_true", 442 | help="if use openai gpt3 api", 443 | ) 444 | parser.add_argument( 445 | "--use_chatgpt_api", 446 | dest="use_chatgpt_api", 447 | action="store_true", 448 | help="if use openai chatgpt api", 449 | ) 450 | parser.add_argument( 451 | "--config", 452 | dest="config", 453 | type=str, 454 | default="", 455 | help="config file path", 456 | ) 457 | 458 | options = parser.parse_args() 459 | 460 | if options.config: 461 | config = {} 462 | if os.path.exists(options.config): 463 | with open(options.config, "r") as f: 464 | config = json.load(f) 465 | else: 466 | raise Exception(f"{options.config} doesn't exist") 467 | 468 | # update options with config 469 | for key, value in config.items(): 470 | if not getattr(options, key, None): 471 | setattr(options, key, value) 472 | 473 | # if set 474 | MI_USER = options.account or env.get("MI_USER") or MI_USER 475 | MI_PASS = options.password or env.get("MI_PASS") or MI_PASS 476 | OPENAI_API_KEY = options.openai_key or env.get("OPENAI_API_KEY") 477 | if options.use_gpt3: 478 | if not OPENAI_API_KEY: 479 | raise Exception("Use gpt-3 api need openai API key, please google how to") 480 | if options.use_chatgpt_api: 481 | if not OPENAI_API_KEY: 482 | raise Exception("Use chatgpt api need openai API key, please google how to") 483 | 484 | miboy = MiGPT( 485 | options.hardware, 486 | options.cookie, 487 | options.use_command, 488 | options.mute_xiaoai, 489 | options.use_gpt3, 490 | options.use_chatgpt_api, 491 | options.verbose, 492 | ) 493 | asyncio.run(miboy.run_forever()) 494 | --------------------------------------------------------------------------------