feat: 开放小爱音箱接入 MiGPT-Next 源代码
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { sleep } from "@mi-gpt/utils";
|
||||
import { OpenXiaoAI } from "./xiaoai.js";
|
||||
|
||||
async function main() {
|
||||
await OpenXiaoAI.start({
|
||||
openai: {
|
||||
model: "gpt-4o-mini",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
},
|
||||
async onMessage(_engine, { text }) {
|
||||
if (text.startsWith("你好")) {
|
||||
await sleep(1000);
|
||||
return { text: "你好,很高兴认识你!" };
|
||||
}
|
||||
},
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,2 @@
|
||||
import { createRequire } from 'node:module';
|
||||
export const RustServer = createRequire(import.meta.url)('./open-xiaoai.node');
|
||||
@@ -0,0 +1,236 @@
|
||||
import { jsonEncode } from "@mi-gpt/utils/parse";
|
||||
import { RustServer } from "./open-xiaoai.js";
|
||||
import type { ISpeaker } from "@mi-gpt/engine/base";
|
||||
|
||||
export interface CommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number;
|
||||
}
|
||||
|
||||
class SpeakerManager implements ISpeaker {
|
||||
status: "playing" | "paused" | "idle" = "idle";
|
||||
|
||||
/**
|
||||
* 获取播放状态
|
||||
*/
|
||||
async getPlaying(sync = false) {
|
||||
if (sync) {
|
||||
// 同步远端最新状态
|
||||
const res = await this.runShell("mphelper mute_stat");
|
||||
if (res?.stdout.includes("1")) {
|
||||
this.status = "playing";
|
||||
} else if (res?.stdout.includes("2")) {
|
||||
this.status = "paused";
|
||||
}
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放/暂停
|
||||
*/
|
||||
async setPlaying(playing = true) {
|
||||
const res = await this.runShell(
|
||||
playing ? "mphelper play" : "mphelper pause"
|
||||
);
|
||||
return res?.stdout.includes('"code": 0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放文字、音频链接、音频流
|
||||
*/
|
||||
async play({
|
||||
text,
|
||||
url,
|
||||
bytes,
|
||||
timeout = 10 * 60 * 1000,
|
||||
blocking = false,
|
||||
}: {
|
||||
text?: string;
|
||||
url?: string;
|
||||
bytes?: Uint8Array;
|
||||
/**
|
||||
* 超时时长(毫秒)
|
||||
*
|
||||
* 默认 10 分钟
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* 是否阻塞运行(仅对播放文字、音频链接有效)
|
||||
*
|
||||
* 如果是则等到音频播放完毕才会返回
|
||||
*/
|
||||
blocking?: boolean;
|
||||
}) {
|
||||
if (bytes) {
|
||||
return RustServer.on_output_data(bytes) as Promise<boolean>;
|
||||
}
|
||||
|
||||
if (blocking) {
|
||||
const res = await this.runShell(
|
||||
url
|
||||
? `miplayer -f '${url}'`
|
||||
: `/usr/sbin/tts_play2.sh '${text || "你好"}'`,
|
||||
{ timeout }
|
||||
);
|
||||
return res?.exit_code === 0;
|
||||
}
|
||||
|
||||
const res = await this.runShell(
|
||||
url
|
||||
? `ubus call mediaplayer player_play_url '${jsonEncode({
|
||||
url: url,
|
||||
type: 1,
|
||||
})}'`
|
||||
: `ubus call mibrain text_to_speech '${jsonEncode({
|
||||
text: text || "你好",
|
||||
save: 0,
|
||||
})}'`,
|
||||
{ timeout }
|
||||
);
|
||||
return res?.stdout.includes('"code": 0') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* (取消)唤醒小爱
|
||||
*/
|
||||
async wakeUp(
|
||||
awake = true,
|
||||
options?: {
|
||||
/**
|
||||
* 静默唤醒
|
||||
*/
|
||||
silent: boolean;
|
||||
}
|
||||
) {
|
||||
const { silent = false } = options ?? {};
|
||||
const command = awake
|
||||
? silent
|
||||
? `ubus call pnshelper event_notify '{"src":1,"event":0}'`
|
||||
: `ubus call pnshelper event_notify '{"src":0,"event":0}'`
|
||||
: `
|
||||
ubus call pnshelper event_notify '{"src":3, "event":7}'
|
||||
sleep 0.1
|
||||
ubus call pnshelper event_notify '{"src":3, "event":8}'
|
||||
`;
|
||||
const res = await this.runShell(command);
|
||||
return res?.stdout.includes('"code": 0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 把文字指令交给原来的小爱执行
|
||||
*/
|
||||
async askXiaoAI(
|
||||
text: string,
|
||||
options?: {
|
||||
/**
|
||||
* 静默执行
|
||||
*/
|
||||
silent: boolean;
|
||||
}
|
||||
) {
|
||||
const { silent = false } = options ?? {};
|
||||
const res = await this.runShell(
|
||||
`ubus call mibrain ai_service '${jsonEncode({
|
||||
tts: silent ? undefined : 1,
|
||||
nlp: 1,
|
||||
nlp_text: text,
|
||||
})}'`
|
||||
);
|
||||
return res?.stdout.includes('"code": 0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断原来小爱的运行
|
||||
*
|
||||
* 注意:重启需要大约 1-2s 的时间,在此期间无法使用小爱音箱自带的 TTS 服务
|
||||
*/
|
||||
async abortXiaoAI() {
|
||||
const res = await this.runShell(
|
||||
"/etc/init.d/mico_aivs_lab restart >/dev/null 2>&1"
|
||||
);
|
||||
return res?.exit_code === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启动分区
|
||||
*/
|
||||
async getBoot() {
|
||||
const res = await this.runShell("echo $(fw_env -g boot_part)");
|
||||
return res?.stdout.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启动分区
|
||||
*/
|
||||
async setBoot(boot_part: "boot0" | "boot1") {
|
||||
const res = await this.runShell(
|
||||
`fw_env -s boot_part ${boot_part} >/dev/null 2>&1 && echo $(fw_env -g boot_part)`
|
||||
);
|
||||
return res?.stdout.includes(boot_part);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备型号、序列号信息
|
||||
*/
|
||||
async getDevice() {
|
||||
const res = await this.runShell("echo $(micocfg_model) $(micocfg_sn)");
|
||||
const info = res?.stdout.trim().split(" ");
|
||||
return {
|
||||
model: info?.[0] ?? "unknown",
|
||||
sn: info?.[1] ?? "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取麦克风状态
|
||||
*/
|
||||
async getMic() {
|
||||
const res = await this.runShell(
|
||||
"[ ! -f /tmp/mipns/mute ] && echo on || echo off"
|
||||
);
|
||||
let status: "on" | "off" = "off";
|
||||
if (res?.stdout.includes("on")) {
|
||||
status = "on";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开/关闭麦克风
|
||||
*/
|
||||
async setMic(on = true) {
|
||||
const res = await this.runShell(
|
||||
on
|
||||
? `ubus -t1 -S call pnshelper event_notify '{"src":3, "event":7}' 2>&1`
|
||||
: `ubus -t1 -S call pnshelper event_notify '{"src":3, "event":8}' 2>&1`
|
||||
);
|
||||
return res?.stdout.includes('"code":0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行脚本
|
||||
*/
|
||||
async runShell(
|
||||
script: string,
|
||||
options?: {
|
||||
/**
|
||||
* 超时时间(单位:毫秒)
|
||||
*/
|
||||
timeout?: number;
|
||||
}
|
||||
): Promise<CommandResult | undefined> {
|
||||
const { timeout = 10 * 1000 } = options ?? {};
|
||||
try {
|
||||
const res = await RustServer.run_shell(script, timeout);
|
||||
if (res) {
|
||||
return JSON.parse(res);
|
||||
}
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const OpenXiaoAISpeaker = new SpeakerManager();
|
||||
@@ -0,0 +1,71 @@
|
||||
import { type EngineConfig, MiGPTEngine } from "@mi-gpt/engine";
|
||||
import { deepMerge } from "@mi-gpt/utils";
|
||||
import { jsonDecode } from "@mi-gpt/utils/parse";
|
||||
import type { Prettify } from "@mi-gpt/utils/typing";
|
||||
import { RustServer } from "./open-xiaoai.js";
|
||||
import { OpenXiaoAISpeaker } from "./speaker.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
type OpenXiaoAIConfig = Prettify<EngineConfig<OpenXiaoAIEngine>>;
|
||||
|
||||
const kDefaultOpenXiaoAIConfig: OpenXiaoAIConfig = {
|
||||
//
|
||||
};
|
||||
|
||||
class OpenXiaoAIEngine extends MiGPTEngine {
|
||||
speaker = OpenXiaoAISpeaker;
|
||||
|
||||
async start(config: OpenXiaoAIConfig) {
|
||||
await super.start(deepMerge(kDefaultOpenXiaoAIConfig, config));
|
||||
// 注册全局回调函数
|
||||
(global as any).RUST_CALLBACKS = {
|
||||
on_event: this.onEvent,
|
||||
on_input_data: this.onRecord,
|
||||
};
|
||||
// 启动服务
|
||||
console.log("✅ 服务已启动...");
|
||||
await RustServer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到事件
|
||||
*/
|
||||
onEvent = (event: string) => {
|
||||
const e = JSON.parse(event);
|
||||
if (e.event === "playing") {
|
||||
// 更新播放状态
|
||||
OpenXiaoAISpeaker.status =
|
||||
e.data === "Playing"
|
||||
? "playing"
|
||||
: e.data === "Paused"
|
||||
? "paused"
|
||||
: "idle";
|
||||
} else if (e.event === "instruction" && e.data.NewLine) {
|
||||
// 收到语音识别结果
|
||||
const line = jsonDecode(e.data.NewLine);
|
||||
if (
|
||||
line?.header?.namespace === "SpeechRecognizer" &&
|
||||
line?.header?.name === "RecognizeResult" &&
|
||||
line?.payload?.is_final &&
|
||||
line?.payload?.results?.[0]?.text
|
||||
) {
|
||||
const text = line.payload.results[0].text;
|
||||
this.onMessage({
|
||||
text,
|
||||
id: randomUUID(),
|
||||
sender: "user",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 收到录音音频流
|
||||
*/
|
||||
onRecord = (data: Uint8Array) => {
|
||||
console.log("🔥 收到录音音频流", data.length);
|
||||
};
|
||||
}
|
||||
|
||||
export const OpenXiaoAI = new OpenXiaoAIEngine();
|
||||
Reference in New Issue
Block a user