Use XiaoAi speakers as a programmable Python voice entrypoint
Project description
xiaoai-bridge
Use XiaoAi speakers as a programmable Python voice entrypoint.
简体中文 · Quickstart · Write a Handler · Commands · Troubleshooting
What it does
xiaoai-bridge continuously listens to selected XiaoAi speakers, forwards new user questions to your Python handler, then plays the handler response through the same speaker.
You talk to XiaoAi
↓
xiaoai-bridge polls XiaoAi conversation records
↓
Your handler(question, speaker) runs in your own project
↓
xiaoai-bridge plays text or audio through the matching speaker
Use it to:
- Connect XiaoAi speakers to your own LLM or automation logic.
- Write home voice automations in plain Python.
- Share one handler across multiple speakers while still knowing which speaker triggered the request.
- Return text, remote audio URLs, local audio paths, or streaming text chunks.
[!NOTE] This project uses Xiaomi MiNA APIs inspired by
idootop/migpt-next. These APIs are not public stable APIs and may be affected by account security policy, device model, firmware version, region, or upstream changes.
xiaoai-bridgeappends TTS/audio playback after observing conversation records. It may not intercept or replace XiaoAi's original response. On some devices or scenarios, users may hear XiaoAi's native response first, then the handler response.
Install
With uv
Create your own bot project and install xiaoai-bridge as a dependency:
mkdir my-xiaoai-bot
cd my-xiaoai-bot
uv init
uv add xiaoai-bridge
xiaoai-init
With pip
python -m venv .venv
source .venv/bin/activate
pip install xiaoai-bridge
[!TIP] Keep your
handler.py,.env, and any private automation code in your own project. Do not editsite-packages/xiaoai_bridge/handler.pyor the package source.
Quickstart
xiaoai-init creates .env, handler.py, and a small .gitignore in the current directory. Existing files are skipped unless you pass --force.
Recommended login method on machines with a desktop browser:
xiaoai-login
This opens Xiaomi login in your browser, waits for you to sign in, then tries to copy userId and passToken from local browser cookies into .env.
If you are on a headless machine or cookie reading is unavailable, manually fill these values in .env:
MI_XIAOMI_USER_ID="your Xiaomi userId"
MI_XIAOMI_PASS_TOKEN="your passToken, including the V1: prefix"
Create or edit handler.py next to .env:
def handler(question: str, speaker):
print(f"Question: {question}, speaker: {speaker.display_name}", flush=True)
return f"{speaker.display_name} heard: {question}"
Check login:
xiaoai-check-login
Select the XiaoAi speakers to listen to:
xiaoai-select
Start the bridge:
xiaoai-bridge
You can also override the handler from the command line:
xiaoai-bridge --handler ./handler.py:handler
xiaoai-bridge --handler my_bot.handlers:handler
Handler priority:
CLI --handler > MI_HANDLER > built-in demo handler
Get passToken
Recommended: browser login command
On a desktop machine, run:
xiaoai-login
The command opens https://account.xiaomi.com/, waits while you sign in, then reads Xiaomi cookies from local browsers using browser-cookie3. If successful, it updates .env automatically:
MI_XIAOMI_USER_ID="..."
MI_XIAOMI_PASS_TOKEN="V1:..."
If this fails because the machine is headless, the browser is unsupported, cookies are encrypted, or no desktop browser is available, use the manual path below.
Manual copy
- Open
https://account.xiaomi.com/in a browser and sign in. - Open Developer Tools.
- Find Cookies / Storage for
https://account.xiaomi.com. - Copy:
userIdpassToken
- Write them to
.env:
MI_XIAOMI_USER_ID="..."
MI_XIAOMI_PASS_TOKEN="V1:..."
If Chrome does not show passToken, try Firefox. Copy the V1: prefix as part of passToken.
Select XiaoAi speakers
Run the interactive selector:
xiaoai-select
Keys:
| Key | Action |
|---|---|
↑ / ↓ |
Move cursor |
Space |
Select / deselect |
a |
Select all / none |
Enter |
Save to .env |
q |
Cancel |
The selector updates:
MI_SPEAKER_SN="sn1,sn2,..."
MI_SPEAKER_MAC="mac1,mac2,..."
Write a Handler
Create a handler in your own project, for example handler.py:
from xiaoai_bridge.mina_client import MiNADevice
def handler(question: str, speaker: MiNADevice) -> str | None:
print(f"Question: {question}, speaker: {speaker.display_name}", flush=True)
return f"{speaker.display_name}, you asked: {question}"
Configure it with one of these forms:
MI_HANDLER="./handler.py:handler"
MI_HANDLER="/absolute/path/to/handler.py:handler"
MI_HANDLER="my_bot.handlers:handler"
If the callable name is omitted, handler is used by default:
MI_HANDLER="./handler.py"
speaker commonly contains:
| Field | Meaning |
|---|---|
speaker.display_name |
Speaker name, preferring alias/name |
speaker.serial_number |
SN |
speaker.mac |
MAC address |
speaker.hardware |
Hardware model, for example LX06 |
speaker.device_id |
MiNA device id |
speaker.miot_did |
Mi Home did |
Branch by speaker
def handler(question: str, speaker) -> str | None:
if speaker.display_name == "Living Room XiaoAi":
return "This reply is from the living room speaker."
return f"{speaker.display_name} received it."
Async handler
async def handler(question: str, speaker) -> str | None:
answer = await your_llm_call(question)
return answer
Streaming handler
import asyncio
async def handler(question: str, speaker):
async for chunk in ask_your_llm_stream(question):
yield chunk
async def ask_your_llm_stream(question: str):
for chunk in ["First sentence.", "Second sentence.", "Third sentence."]:
await asyncio.sleep(0.5)
yield chunk
Each non-empty yielded chunk triggers one XiaoAi TTS playback.
[!TIP] Yield sentences or short paragraphs, not tokens or single characters. XiaoAi TTS is not a WebSocket audio stream; tiny chunks cause frequent short playback segments.
Return a remote audio URL
def handler(question: str, speaker) -> str | None:
return "https://example.com/reply.mp3"
Return a local audio path
def handler(question: str, speaker) -> str | None:
return "/Users/example/Music/reply.mp3"
XiaoAi speakers cannot read files directly from your computer. xiaoai-bridge starts a lightweight HTTP server and maps the file to:
http://<your-lan-ip>:8765/audio/<token>.mp3
If the speaker cannot access that address, set:
MI_PUBLIC_BASE_URL="http://your-reachable-host:8765"
Or return an audio URL that is already reachable by the speaker.
Commands
| Command | Purpose |
|---|---|
xiaoai-init |
Create .env, handler.py, and .gitignore in a user project |
xiaoai-login |
Open Xiaomi login in a browser and write userId/passToken to .env |
xiaoai-bridge |
Start the bridge and listen to selected speakers |
xiaoai-bridge --handler ./handler.py:handler |
Start with a specific handler |
xiaoai-select |
Interactively select one or more XiaoAi speakers |
xiaoai-check-login |
Check Xiaomi login, device list, and selected speakers |
xiaoai-test-speak |
Play default test TTS |
xiaoai-test-speak "hello" |
Play custom test TTS |
Source checkout commands
If you are developing this repository itself, use uv run:
uv sync --dev
uv run xiaoai-bridge --handler ./handler.py:handler
uv run ruff check .
uv run pytest
Token expiration
Normally, if cached serviceToken expires, the program tries to refresh it with passToken from .env.
If passToken is also invalid, you may see:
- XiaoAi no longer plays your configured response.
- Console errors such as
401,XiaomiAuthError, orlogin failed. xiaoai-check-loginfails.
Recovery:
# 1. Get a fresh userId / passToken from browser cookies and update .env
# 2. Delete old serviceToken cache
rm -f .data/token_cache.json
# 3. Check login
xiaoai-check-login
# 4. Restart the bridge
xiaoai-bridge
Runtime behavior
On startup, the program:
- Reads
.env. - Loads the configured handler from
MI_HANDLERor--handler. - Uses Xiaomi login state to obtain a MiNA
serviceToken. - Lists devices and matches selected speakers.
- Initializes conversation cursors without replaying old records.
- Polls new conversations every
MI_POLL_INTERVAL_SECONDS. - Calls
handler(question, speaker)for each new question. - Plays TTS or audio based on the handler result.
Troubleshooting
No sound
Check login first:
xiaoai-check-login
Then test TTS:
xiaoai-test-speak "test sound"
If the command succeeds but there is no sound, check:
- The selected speaker is the one you are testing.
- The speaker is online and not in an abnormal playback state.
- The speaker volume is not zero.
Handler cannot be loaded
Check MI_HANDLER:
MI_HANDLER="./handler.py:handler"
Make sure:
- the file exists relative to the directory where you run
xiaoai-bridge; - the callable exists and is named
handleror explicitly named after:; - for
module:callable, the module is importable in the current environment.
No user questions printed
Confirm:
xiaoai-selectselected the correct device.- You asked the selected speaker a question that produces a normal answer, not only the wake word.
- The bridge is running:
xiaoai-bridge
Login failed
Prefer passToken login. Frequent automatic account/password login may trigger Xiaomi risk control.
If login fails:
rm -f .data/token_cache.json
xiaoai-check-login
If it still fails, refresh MI_XIAOMI_USER_ID and MI_XIAOMI_PASS_TOKEN.
Current boundaries
- Only MiNA-related capabilities are implemented; full MIoT RC4 protocol support is not included.
- Only new questions after startup are processed; old records are not replayed.
- Streaming text is segmented TTS playback, not true audio streaming.
- Local audio playback depends on network reachability. If the speaker cannot access the generated URL, set
MI_PUBLIC_BASE_URLor return a remote URL. - Xiaomi APIs may change with time, region, account security policy, or device firmware.
Security and privacy
- Do not commit
.env,.data/token_cache.json,passToken, orserviceTokento a public repository. passTokenis a login credential. Refresh it if it expires or leaks.- Keep
handler.pyprivate if it contains personal automation logic, keys, or local service URLs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xiaoai_bridge-0.2.1.tar.gz.
File metadata
- Download URL: xiaoai_bridge-0.2.1.tar.gz
- Upload date:
- Size: 49.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a08aeb4f7724f47442f14462d962a441291c76d28142c8a2820beb5be2e24a8
|
|
| MD5 |
ff9d8cca9c8ab27681b6f9dd50e7a762
|
|
| BLAKE2b-256 |
2210408899e60695ce60d409cb2231245ae488c630db34664843c2ed02d6e25f
|
File details
Details for the file xiaoai_bridge-0.2.1-py3-none-any.whl.
File metadata
- Download URL: xiaoai_bridge-0.2.1-py3-none-any.whl
- Upload date:
- Size: 29.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5dc8929dfc7433c37fa955e2c2af27e5eb4120d176211431e15aae61db316f3
|
|
| MD5 |
cea0e3716ace5f02a4c10215f8435a64
|
|
| BLAKE2b-256 |
e0d17bdc1478f2699554b8ecbde15195318f3e3d492dc955b41e614485c043a9
|