Merge pull request #178 from idootop/dev

release: v4.2.0
This commit is contained in:
Del 2024-08-26 21:46:31 +08:00 committed by GitHub
commit 9f83eb1a0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 72 additions and 327 deletions

View File

@ -147,7 +147,7 @@ export default {
*/
// 是否启用连续对话功能,部分小爱音箱型号无法查询到正确的播放状态,需要关闭连续对话
streamResponse: true,
streamResponse: false,
// 连续对话时,无响应多久后自动退出
exitKeepAliveAfter: 30, // 默认 30 秒,建议不要超过 1 分钟
// 连续对话时,下发 TTS 指令多长时间后开始检测设备播放状态(默认 3 秒)
@ -164,6 +164,6 @@ export default {
// 是否跟踪 Mi Service 相关日志(打开后可以查看设备 did
enableTrace: false, // 一般情况下不要打开
// 网络请求超时时长(单位毫秒,默认 5 秒)
timeout: 5000,
timeout: 5000,
},
};

View File

@ -2,10 +2,10 @@
[![npm version](https://badge.fury.io/js/mi-gpt.svg)](https://www.npmjs.com/package/mi-gpt) [![Docker Image Version](https://img.shields.io/docker/v/idootop/mi-gpt?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/mi-gpt)
<video src='https://github.com/idootop/mi-gpt/assets/35302658/dc336916-9087-418b-bc1b-04d5534dce8f'></video>
> 👉 查看完整演示视频:【[整活!将小爱音箱接入 ChatGPT 和豆包,改造成你的专属语音助手~](https://www.bilibili.com/video/BV1N1421y7qn/?share_source=copy_web&vd_source=5d4e78ff2a0dc6a661baa65f479199c1)】
<video src='https://github.com/idootop/mi-gpt/assets/35302658/dc336916-9087-418b-bc1b-04d5534dce8f'></video>
## 👋 项目简介
在这个数字化的世界里,家已不仅仅是一个居住的地方,而是我们数字生活的延伸。
@ -29,7 +29,7 @@
- **💬 流式响应**。爱情来得太快就像龙卷风,而你的小爱音箱也是,对你的爱意秒回,爱你不会让你等太久。
- **🧠 长短期记忆**。小爱音箱现在能记住你们之间的每一次对话,越聊越默契,就像是你身边的老朋友。
- **🔊 自定义 TTS**。厌倦了小爱同学的语音?帮你解锁[「豆包」](https://doubao.com)同款音色,就像真人在回你的消息。
- **🤖️ 智能家居 Agent**。心情不好?小爱立刻懂你,自动帮你播放喜欢的音乐,调节灯光,逗你开心。_TODO_
- ~**🤖️ 智能家居 Agent**。心情不好?小爱立刻懂你,自动帮你播放喜欢的音乐,调节灯光,逗你开心。~
## 🦄 Sponsors
@ -39,15 +39,9 @@
## ⚡️ 快速开始
> 查看视频教程 👉 【[MiGPT 光速入门教程,从零教你调教小爱音箱~](https://www.bilibili.com/video/BV1zb421H7cS)】
### 视频教程
`MiGPT` 有两种启动方式: [Docker](#docker) 和 [Node.js](#nodejs)。
启动成功后,你可以通过以下方式来召唤 AI 回答问题:
- **小爱同学,请 xxx**。比如 `小爱同学,请问地球为什么是圆的?`
- **小爱同学,你 xxx**。比如 `小爱同学,你喜欢我吗?`
- **小爱同学,召唤 xxx**。比如 `小爱同学,召唤傻妞`
👉 [MiGPT 光速入门视频教程,手把手教你调教小爱音箱~](https://www.bilibili.com/video/BV1zb421H7cS)
### 设备要求
@ -57,6 +51,16 @@
> 注意本项目暂不支持小度音箱、天猫精灵、HomePod 等智能音箱设备,亦无相关适配计划。
### 使用方式
`MiGPT` 有两种启动方式: [Docker](#docker) 和 [Node.js](#nodejs)。
启动成功后,你可以通过以下方式来召唤 AI 回答问题:
- **小爱同学,请 xxx**。比如 `小爱同学,请问地球为什么是圆的?`
- **小爱同学,你 xxx**。比如 `小爱同学,你喜欢我吗?`
- **小爱同学,召唤 xxx**。比如 `小爱同学,召唤傻妞`
### Docker
[![Docker Image Version](https://img.shields.io/docker/v/idootop/mi-gpt?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/mi-gpt)
@ -104,7 +108,7 @@ main();
## 📖 使用文档
以下为更详细的使用教程,大多数问题都可在 [💬 常见问题](https://github.com/idootop/mi-gpt/blob/main/docs/faq.md) 中找到答案。
提示:大多数问题都可在 [💬 常见问题](https://github.com/idootop/mi-gpt/blob/main/docs/faq.md) 中找到答案。
- [🔥 官方视频教程](https://www.bilibili.com/video/BV1zb421H7cS)
- [⚙️ 参数设置](https://github.com/idootop/mi-gpt/blob/main/docs/settings.md)
@ -116,14 +120,14 @@ main();
- [✨ 更新日志](https://github.com/idootop/mi-gpt/blob/main/docs/changelog.md)
- [🚀 Roadmap](https://github.com/idootop/mi-gpt/blob/main/docs/roadmap.md)
## 👍 推荐项目与教程
## 🤩 推荐项目与教程
| 项目链接 | 简介 | 来源 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| **相关项目** | | |
| [@shinedlc/mi-gpt](https://github.com/shinedlc/mi-gpt) | 一个接入了摄像头硬件 + 本机搭建 Ollama 模型的 MiGPT 分支,让小爱同学可以看到和理解现实世界 | @shinedlc |
| [@lmk123/migpt-cli](https://github.com/lmk123/migpt-cli) | 通过图形化界面的方式创建并管理 MiGPT支持运行多个账号。 | @lmk123 |
| [@lmk123/migpt-cli/gui](https://migptgui.com/gui/) | 直接在网页上更方便的编辑和生成 `.migpt.js``.env` 配置文件 | @lmk123 |
| [MiGPT GUI](https://migptgui.com/) | 通过图形化界面的方式创建并管理 MiGPT支持运行多个账号 | @lmk123 |
| - | 直接在网页上更方便的编辑和生成 `.migpt.js``.env` 配置文件 | @lmk123 |
| [@shinedlc/mi-gpt](https://github.com/shinedlc/mi-gpt) | 支持摄像头模块的 MiGPT 分支,让小爱同学可以看到和理解现实世界 | @shinedlc |
| **使用教程** | | |
| [MiGPT 官方视频教程](https://www.bilibili.com/video/BV1zb421H7cS) | 官方视频教程配套 PPT 文件 👉 [MiGPT 官方教程.pdf](https://github.com/idootop/mi-gpt/blob/main/assets/pdf/MiGPT%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8B.pdf) | @idootop |
| [MiGPT 接入豆包等大模型教程](https://migptgui.com/docs/apply/) | 豆包、MoonshotKimi等常见大模型的详细接入教程 | @lmk123 |

View File

@ -1,5 +1,21 @@
# ✨ 更新日志
## v4.2.0
### ✨ 新功能
- ✅ 新增对小爱音箱 LLM 消息的支持
### 🐛 修复
- ✅ 修复 LLM 返回值格式与预期不符的问题 by @yanyao2333
### ❤️ 感谢
- @yanyao2333 让 LLM 返回值的解析更加健壮 https://github.com/idootop/mi-gpt/pull/160
- @LyCecilion 对小爱音箱丢消息问题的详细反馈 https://github.com/idootop/mi-gpt/issues/177
- @Jasonzhu1207 在 telegram 群中帮忙解答问题
## v4.1.0
### 🐛 修复

View File

@ -1,6 +1,6 @@
{
"name": "mi-gpt",
"version": "4.1.0",
"version": "4.2.0",
"type": "module",
"description": "将小爱音箱接入 ChatGPT 和豆包,改造成你的专属语音助手。",
"homepage": "https://github.com/idootop/mi-gpt",
@ -25,16 +25,17 @@
"scripts": {
"start": "node ./app.js",
"dev": "node --env-file=.env ./app.js",
"build": "npx -y prisma generate && tsup",
"build": "npx -y prisma generate && rm -rf dist && tsup",
"db:gen": "npx -y prisma migrate dev --name init",
"db:reset": "rm -f .mi.json .bot.json prisma/app.db prisma/app.db-journal",
"prepublish": "npm run build",
"postinstall": "npx -y prisma migrate dev --name hello"
},
"dependencies": {
"@prisma/client": "^5.14.0",
"fs-extra": "^11.2.0",
"mi-service-lite": "^3.0.0",
"openai": "^4.52.2",
"mi-service-lite": "^3.1.0",
"openai": "^4.56.0",
"prisma": "^5.14.0",
"proxy-agent": "^6.4.0"
},

View File

@ -15,11 +15,11 @@ importers:
specifier: ^11.2.0
version: 11.2.0
mi-service-lite:
specifier: ^3.0.0
version: 3.0.0
specifier: ^3.1.0
version: 3.1.0
openai:
specifier: ^4.52.2
version: 4.52.2
specifier: ^4.56.0
version: 4.56.0
prisma:
specifier: ^5.14.0
version: 5.14.0
@ -561,8 +561,8 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.7.2:
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
axios@1.7.5:
resolution: {integrity: sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -863,8 +863,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
mi-service-lite@3.0.0:
resolution: {integrity: sha512-Fbz3lGPNp1Jbqqlj4EK1vya9zj3WCWXeW6+mnpcQi9RTMMPmGXC+126HHmw8WWUWj4G0tNHHP/ApHMCAIzzENQ==}
mi-service-lite@3.1.0:
resolution: {integrity: sha512-WOMK8poZZ4nvXezETGdJHiqOOMON/+8prv8Hi9kjODBGCkdUtp+Q1w9OJRYxZUiB+ZaH5f9okaqAq5TBRpg1VA==}
engines: {node: '>=16'}
micromatch@4.0.5:
@ -933,9 +933,14 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
openai@4.52.2:
resolution: {integrity: sha512-mMc0XgFuVSkcm0lRIi8zaw++otC82ZlfkCur1qguXYWPETr/+ZwL9A/vvp3YahX+shpaT6j03dwsmUyLAfmEfg==}
openai@4.56.0:
resolution: {integrity: sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw==}
hasBin: true
peerDependencies:
zod: ^3.23.8
peerDependenciesMeta:
zod:
optional: true
pac-proxy-agent@7.0.2:
resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==}
@ -1173,10 +1178,6 @@ packages:
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
@ -1566,7 +1567,7 @@ snapshots:
asynckit@0.4.0: {}
axios@1.7.2:
axios@1.7.5:
dependencies:
follow-redirects: 1.15.6
form-data: 4.0.0
@ -1905,9 +1906,9 @@ snapshots:
merge2@1.4.1: {}
mi-service-lite@3.0.0:
mi-service-lite@3.1.0:
dependencies:
axios: 1.7.2
axios: 1.7.5
pako: 2.1.0
transitivePeerDependencies:
- debug
@ -1961,7 +1962,7 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
openai@4.52.2:
openai@4.56.0:
dependencies:
'@types/node': 18.19.33
'@types/node-fetch': 2.6.11
@ -1970,7 +1971,6 @@ snapshots:
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
web-streams-polyfill: 3.3.3
transitivePeerDependencies:
- encoding
@ -2230,8 +2230,6 @@ snapshots:
v8-compile-cache-lib@3.0.1:
optional: true
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {}

View File

@ -199,7 +199,7 @@ export class Speaker extends BaseSpeaker {
private async _fetchFirstMessage() {
const msgs = await this.getMessages({
limit: 1,
filterTTS: false,
filterAnswer: false,
});
this.currentQueryMsg = msgs[0];
}
@ -285,23 +285,26 @@ export class Speaker extends BaseSpeaker {
async getMessages(options?: {
limit?: number;
timestamp?: number;
filterTTS?: boolean;
filterAnswer?: boolean;
}): Promise<QueryMessage[]> {
const filterTTS = options?.filterTTS ?? true;
const filterAnswer = options?.filterAnswer ?? true;
const conversation = await this.MiNA!.getConversations(options);
this._lastConversation = conversation;
let records = conversation?.records ?? [];
if (filterTTS) {
if (filterAnswer) {
// 过滤有小爱回答的消息
records = records.filter(
(e) => e.answers.length > 0 && e.answers.some((e) => e.type === "TTS")
(e) =>
["TTS", "LLM"].includes(e.answers[0]?.type) && // 过滤 TTS 和 LLM 消息
e.answers.length === 1 // 播放音乐时会有 TTS、Audio 两个 Answer
);
}
return records.map((e) => {
const ttsAnswer = e.answers.find((e) => e.type === "TTS") as any;
const msg: any = e.answers[0];
const answer = msg?.tts?.text?.trim() ?? msg?.llm?.text?.trim();
return {
answer,
text: e.query,
answer: ttsAnswer?.tts?.text?.trim(),
timestamp: e.time,
};
});

View File

@ -1,51 +0,0 @@
import { MyBot } from "../src/services/bot";
import { AISpeaker } from "../src/services/speaker/ai";
export async function testMyBot() {
// await testStreamResponse();
await testRunBot();
}
async function testRunBot() {
const name = "傻妞";
const speaker = new AISpeaker({
name,
tts: "custom",
userId: process.env.MI_USER!,
password: process.env.MI_PASS!,
did: process.env.MI_DID,
});
const bot = new MyBot({
speaker,
bot: {
name,
profile: `性别女,性格乖巧可爱,喜欢搞怪,爱吃醋。`,
},
master: {
name: "陆小千",
profile: `性别男,善良正直,总是舍己为人,是傻妞的主人。`,
},
});
const res = await bot.run();
console.log("✅ done");
}
async function testStreamResponse() {
const stream = await MyBot.chatWithStreamResponse({
user: "地球为什么是圆的?",
onFinished: (text) => {
console.log("\nFinal result 111:\n", text);
},
});
const config: any = {
userId: process.env.MI_USER!,
password: process.env.MI_PASS!,
did: process.env.MI_DID,
tts: "custom",
};
const speaker = new AISpeaker(config);
await speaker.initMiServices();
await speaker.response({ stream });
const res = await stream.getFinalResult();
console.log("\nFinal result 222:\n", res);
}

View File

@ -1,58 +0,0 @@
import { assert } from "console";
import {
ConversationManager,
MessageContext,
} from "../src/services/bot/conversation";
import { println } from "../src/utils/base";
import { MessageCRUD } from "../src/services/db/message";
export async function testDB() {
const manager = new ConversationManager({
bot: {
name: "小爱同学",
profile: "我是小爱同学,机器人",
},
master: {
name: "陆小千",
profile: "我是陆小千,人类",
},
room: {
name: "客厅",
description: "陆小千的客厅,小爱同学放在角落里",
},
});
const { room, bot, master, memory } = await manager.get();
assert(room, "❌ 初始化用户失败");
const ctx = { bot, master, room } as MessageContext;
let message = await manager.onMessage(ctx, {
sender: master!,
text: "你好!",
});
assert(message?.text === "你好!", "❌ 插入消息失败");
message = await manager.onMessage(ctx, {
sender: bot!,
text: "你好!很高兴认识你",
});
await manager.onMessage(ctx, {
sender: master!,
text: "你是谁?",
});
await manager.onMessage(ctx, {
sender: bot!,
text: "我是小爱同学,你可以叫我小爱!",
});
const messages = await manager.getMessages({ take: 100 });
assert(messages.length === 4, "❌ 查询消息数量异常");
assert(messages[0].text === "你好!", "❌ 查询消息排序异常");
const newMessages = await MessageCRUD.gets({
take: 100,
cursorId: message!.id,
order: "asc",
});
assert(newMessages.length === 2, "❌ 查询消息数量异常(游标)");
assert(
newMessages[1].text === "我是小爱同学,你可以叫我小爱!",
"❌ 查询消息排序异常(游标)"
);
println("✅ hello world");
}

View File

@ -1,9 +0,0 @@
import { Logger } from "../src/utils/log";
export function testLog() {
Logger.log("你好", ["世界"], { hello: "world!" });
Logger.success("你好", ["世界"], { hello: "world!" });
Logger.error("你好", ["世界"], { hello: "world!" });
Logger.assert(true, "你好 111", ["世界"], { hello: "world!" });
Logger.assert(false, "你好 222", ["世界"], { hello: "world!" });
}

View File

@ -1,37 +0,0 @@
import { randomUUID } from "crypto";
import { openai } from "../src/services/openai";
export async function testOpenAI() {
await testChat();
// await testStreamChat();
}
async function testChat() {
const res = await openai.chat({ user: "地球为什么是圆的?" });
console.log("\nFinal result:\n", res?.content);
}
async function testStreamChat() {
const requestId = randomUUID();
const res = await openai.chatStream({
requestId,
user: "地球为什么是圆的?",
onStream: (text) => {
console.log(text);
},
});
console.log("\nFinal result:\n", res);
}
async function testAbortStreamChat() {
const requestId = randomUUID();
const res = await openai.chatStream({
requestId,
user: "hello!",
onStream: (text) => {
console.log(text);
openai.cancel(requestId);
},
});
console.log("xxx", res);
}

View File

@ -1,122 +0,0 @@
import { AISpeaker } from "../src/services/speaker/ai";
import { StreamResponse } from "../src/services/speaker/stream";
import { sleep } from "../src/utils/base";
export async function testSpeaker() {
const speaker = new AISpeaker({
userId: process.env.MI_USER!,
password: process.env.MI_PASS!,
did: process.env.MI_DID,
tts: "xiaoai",
debug: true,
});
await speaker.initMiServices();
await testTTS(speaker);
// await testAISpeakerStatus(speaker);
// await testSpeakerResponse(speaker);
// await testSpeakerStreamResponse(speaker);
// await testSpeakerGetMessages(speaker);
// await testSwitchSpeaker(speaker);
// await testSpeakerUnWakeUp(speaker);
// await testAISpeaker(speaker);
}
async function testTTS(speaker: AISpeaker) {
const res1 = await speaker.MiIOT!.doAction(5, 1, "你好,很高兴认识你");
const res2 = await speaker.MiNA!.play({ tts: "你好,很高兴认识你" });
console.log("finished");
}
async function testAISpeakerStatus(speaker: AISpeaker) {
const playingCommand = [5, 3, 1];
const res1 = await speaker.MiIOT!.getProperty(
playingCommand[0],
playingCommand[1]
);
const res2 = await speaker.MiNA!.getStatus();
console.log("finished");
}
async function testAISpeaker(speaker: AISpeaker) {
speaker.askAI = async (msg) => {
return { text: "你说:" + msg.text };
};
await speaker.run();
console.log("finished");
}
async function testSpeakerUnWakeUp(speaker: AISpeaker) {
await speaker.wakeUp();
await sleep(1000);
await speaker.unWakeUp();
console.log("hello");
}
async function testSwitchSpeaker(speaker: AISpeaker) {
await speaker.response({ text: "你好,我是傻妞,很高兴认识你!" });
const success = await speaker.switchSpeaker("魅力苏菲");
console.log("switchSpeaker 魅力苏菲", success);
await speaker.response({ text: "你好,我是傻妞,很高兴认识你!" });
console.log("hello");
}
async function testSpeakerGetMessages(speaker: AISpeaker) {
let msgs = await speaker.getMessages({ filterTTS: true });
console.log("filterTTS msgs", msgs);
msgs = await speaker.getMessages({ filterTTS: false });
console.log("no filterTTS msgs", msgs);
}
async function testSpeakerResponse(speaker: AISpeaker) {
let status = await speaker.MiNA!.getStatus();
console.log("curent status", status);
await speaker.response({ text: "你好,我是傻妞,很高兴认识你!" });
sleep(1000);
status = await speaker.MiNA!.getStatus();
console.log("tts status", status);
}
async function testSpeakerStreamResponse(speaker: AISpeaker) {
const stream = new StreamResponse();
const text = `
###
1. ****
2. ****
3. ****西
4. ****
5. ****
###
1. ****
2. **西**西
3. ****
4. ****
5. ****
`;
const add = async (text: string) => {
stream.addResponse(text);
await sleep(100);
};
setTimeout(async () => {
for (const s of text.split("")) {
await add(s);
}
stream.finish();
});
await speaker.response({ stream });
console.log("hello!");
}