feat: export MiGPT

This commit is contained in:
WJG 2024-02-27 00:14:43 +08:00
parent 2dc65da0fe
commit 9e1e9a3973
No known key found for this signature in database
GPG Key ID: 258474EF8590014A
11 changed files with 233 additions and 49 deletions

View File

@ -1,9 +1,49 @@
import { AISpeaker, AISpeakerConfig } from "./services/speaker/ai";
import { MyBot, MyBotConfig } from "./services/bot";
import { runWithDB } from "./services/db";
import { println } from "./utils/base";
import { kBannerASCII } from "./utils/string";
async function main() {
println(kBannerASCII);
}
export type MiGPTConfig = Omit<MyBotConfig, "speaker"> & {
speaker: AISpeakerConfig;
};
runWithDB(main);
export class MiGPT {
static instance: MiGPT | null;
static reset() {
MiGPT.instance = null;
}
static create(config: MiGPTConfig) {
if (MiGPT.instance) {
console.log("🚨 注意MiGPT 是单例,暂不支持多设备、多账号!");
console.log("如果需要切换设备或账号,请先使用 MiGPT.reset() 重置实例。");
} else {
MiGPT.instance = new MiGPT({ ...config, fromCreate: true });
}
return MiGPT.instance;
}
ai: MyBot;
speaker: AISpeaker;
constructor(config: MiGPTConfig & { fromCreate?: boolean }) {
console.assert(config.fromCreate, "请使用 MiGPT.create() 获取客户端实例!");
const { speaker, ...myBotConfig } = config;
this.speaker = new AISpeaker(speaker);
this.ai = new MyBot({
...myBotConfig,
speaker: this.speaker,
});
}
async start() {
// todo init DB
const main = () => {
console.log(kBannerASCII);
return this.ai.run();
};
return runWithDB(main);
}
async stop() {
return this.ai.stop();
}
}

9
src/runner.ts Normal file
View File

@ -0,0 +1,9 @@
import { runWithDB } from "./services/db";
import { println } from "./utils/base";
import { kBannerASCII } from "./utils/string";
async function main() {
println(kBannerASCII);
}
runWithDB(main);

View File

@ -67,10 +67,11 @@ const userTemplate = `
{{message}}
`.trim();
export type MyBotConfig = DeepPartial<IBotConfig> & { speaker: AISpeaker };
export class MyBot {
speaker: AISpeaker;
manager: ConversationManager;
constructor(config: DeepPartial<IBotConfig> & { speaker: AISpeaker }) {
constructor(config: MyBotConfig) {
this.speaker = config.speaker;
this.manager = new ConversationManager(config);
}
@ -154,6 +155,7 @@ export class MyBot {
.chatStream({
...options,
requestId,
trace: true,
onStream: (text) => {
if (stream.status === "canceled") {
return openai.abort(requestId);

View File

@ -3,9 +3,9 @@ import { HttpsProxyAgent } from "https-proxy-agent";
import { isNotEmpty } from "../utils/is";
import { Logger } from "../utils/log";
export const kProxyAgent = new HttpsProxyAgent(
process.env.HTTP_PROXY ?? "http://127.0.0.1:7890"
);
export const kProxyAgent = process.env.HTTP_PROXY
? new HttpsProxyAgent(process.env.HTTP_PROXY)
: null;
const _baseConfig: CreateAxiosDefaults = {
timeout: 10 * 1000,

View File

@ -17,10 +17,14 @@ export interface ChatOptions {
tools?: Array<ChatCompletionTool>;
jsonMode?: boolean;
requestId?: string;
trace?: boolean;
}
class OpenAIClient {
private _logger = Logger.create({ tag: "OpenAI" });
traceInput = false;
traceOutput = true;
private _logger = Logger.create({ tag: "Open AI" });
private _client = new OpenAI({
httpAgent: kProxyAgent,
apiKey: kEnvs.OPENAI_API_KEY!,
@ -44,14 +48,14 @@ class OpenAIClient {
tools,
jsonMode,
requestId,
trace = false,
model = kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
} = options;
this._logger.log(
`🔥 onAskAI
🤖 System: ${system ?? "None"}
😊 User: ${user}
`.trim()
);
if (trace && this.traceInput) {
this._logger.log(
`🔥 onAskAI\n🤖 System: ${system ?? "None"}\n😊 User: ${user}`.trim()
);
}
const systemMsg: ChatCompletionMessageParam[] = system
? [{ role: "system", content: system }]
: [];
@ -76,7 +80,9 @@ class OpenAIClient {
return null;
});
const message = chatCompletion?.choices?.[0]?.message;
this._logger.success(`🤖️ Answer: ${message?.content ?? "None"}`.trim());
if (trace && this.traceOutput) {
this._logger.log(`✅ Answer: ${message?.content ?? "None"}`.trim());
}
return message;
}
@ -92,14 +98,14 @@ class OpenAIClient {
jsonMode,
requestId,
onStream,
trace = false,
model = kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
} = options;
this._logger.log(
`🔥 onAskAI
🤖 System: ${system ?? "None"}
😊 User: ${user}
`.trim()
);
if (trace && this.traceInput) {
this._logger.log(
`🔥 onAskAI\n🤖 System: ${system ?? "None"}\n😊 User: ${user}`.trim()
);
}
const systemMsg: ChatCompletionMessageParam[] = system
? [{ role: "system", content: system }]
: [];
@ -135,7 +141,9 @@ class OpenAIClient {
content += text;
}
}
this._logger.success(`🤖️ Answer: ${content ?? "None"}`.trim());
if (trace && this.traceOutput) {
this._logger.log(`✅ Answer: ${content ?? "None"}`.trim());
}
return withDefault(content, undefined);
}
}

View File

@ -1,4 +1,4 @@
import { pickOne } from "../../utils/base";
import { pickOne, toSet } from "../../utils/base";
import {
Speaker,
SpeakerCommand,
@ -9,12 +9,6 @@ import {
export type AISpeakerConfig = SpeakerConfig & {
askAI?: (msg: QueryMessage) => Promise<SpeakerAnswer>;
/**
*
*
*
*/
switchSpeakerPrefix?: string;
/**
* AI
*
@ -41,6 +35,12 @@ export type AISpeakerConfig = SpeakerConfig & {
* //
*/
callAIPrefix?: string[];
/**
*
*
*
*/
switchSpeakerPrefix?: string[];
/**
*
*
@ -67,6 +67,14 @@ export type AISpeakerConfig = SpeakerConfig & {
* 退
*/
onExitAI?: string[];
/**
* AI
*/
audio_active?: string;
/**
* AI
*/
audio_error?: string;
};
type AnswerStep = (
@ -77,7 +85,7 @@ type AnswerStep = (
export class AISpeaker extends Speaker {
askAI: AISpeakerConfig["askAI"];
name: string;
switchSpeakerPrefix: string;
switchSpeakerPrefix: string[];
onEnterAI: string[];
onExitAI: string[];
callAIPrefix: string[];
@ -85,23 +93,30 @@ export class AISpeaker extends Speaker {
exitKeywords: string[];
onAIAsking: string[];
onAIError: string[];
audio_active?: string;
audio_error?: string;
constructor(config: AISpeakerConfig) {
super(config);
const {
askAI,
name = "豆包",
switchSpeakerPrefix = "音色切换到",
switchSpeakerPrefix,
wakeUpKeyWords = ["打开", "进入", "召唤"],
exitKeywords = ["关闭", "退出", "再见"],
onAIAsking = ["让我先想想", "请稍等"],
onAIError = ["啊哦,出错了,请稍后再试吧!"],
audio_active = process.env.AUDIO_ACTIVE,
audio_error = process.env.AUDIO_ERROR,
} = config;
this.askAI = askAI;
this.switchSpeakerPrefix = switchSpeakerPrefix;
this.name = name;
this.onAIError = onAIError;
this.onAIAsking = onAIAsking;
this.audio_active = audio_active;
this.audio_error = audio_error;
this.switchSpeakerPrefix =
switchSpeakerPrefix ?? getDefaultSwitchSpeakerPrefix();
this.wakeUpKeyWords = wakeUpKeyWords.map((e) => e + this.name);
this.exitKeywords = exitKeywords.map((e) => e + this.name);
this.onEnterAI = config.onEnterAI ?? [
@ -150,12 +165,16 @@ export class AISpeaker extends Speaker {
},
},
{
match: (msg) => msg.text.startsWith(this.switchSpeakerPrefix),
match: (msg) =>
this.switchSpeakerPrefix.some((e) => msg.text.startsWith(e)),
run: async (msg) => {
await this.response({
text: "正在切换音色,请稍等...",
});
const speaker = msg.text.replace(this.switchSpeakerPrefix, "");
const prefix = this.switchSpeakerPrefix.find((e) =>
msg.text.startsWith(e)
)!;
const speaker = msg.text.replace(prefix, "");
const success = await this.switchDefaultSpeaker(speaker);
await this.response({
text: success ? "音色已切换!" : "音色切换失败!",
@ -177,7 +196,7 @@ export class AISpeaker extends Speaker {
async (msg, data) => {
// 思考中
await this.response({
audio: process.env.AUDIO_ACTIVE,
audio: this.audio_active,
text: pickOne(this.onAIAsking)!,
});
},
@ -190,7 +209,7 @@ export class AISpeaker extends Speaker {
if (!data.answer) {
// 回答异常
await this.response({
audio: process.env.AUDIO_ERROR,
audio: this.audio_error,
text: pickOne(this.onAIError)!,
keepAlive: this.keepAlive,
});
@ -217,3 +236,20 @@ export class AISpeaker extends Speaker {
return data.answer;
}
}
const getDefaultSwitchSpeakerPrefix = () => {
let prefixes = ["音色切换到", "切换音色到", "把音色调到"];
const replaces = [
["音色", "声音"],
["切换", "调"],
["到", "为"],
["到", "成"],
];
for (const r of replaces) {
prefixes = toSet([
...prefixes,
...prefixes.map((e) => e.replace(r[0], r[1])),
]);
}
return prefixes;
};

View File

@ -9,6 +9,7 @@ import { sleep } from "../../utils/base";
import { Logger } from "../../utils/log";
import { Http } from "../http";
import { StreamResponse } from "./stream";
import { kAreYouOK } from "../../utils/string";
export type TTSProvider = "xiaoai" | "doubao";
@ -23,6 +24,10 @@ export type BaseSpeakerConfig = MiServiceConfig & {
tts?: TTSProvider;
// 检测间隔(单位毫秒,默认 100 毫秒)
interval?: number;
/**
* TTS /
*/
audio_beep?: string;
};
export class BaseSpeaker {
@ -35,7 +40,12 @@ export class BaseSpeaker {
config: MiServiceConfig;
constructor(config: BaseSpeakerConfig) {
this.config = config;
const { interval = 100, tts = "doubao" } = config;
const {
interval = 100,
tts = "doubao",
audio_beep = process.env.AUDIO_BEEP,
} = config;
this.audio_beep = audio_beep;
this.interval = interval;
this.tts = tts;
}
@ -52,9 +62,11 @@ export class BaseSpeaker {
async unWakeUp() {
// 通过 TTS 不发音文本,使小爱退出唤醒状态
await this.MiIOT!.doAction(5, 1, "¿ʞо ∩оʎ ǝɹɐ"); // are you ok?
await this.MiNA!.pause()
await this.MiIOT!.doAction(5, 1, kAreYouOK);
}
audio_beep?: string;
responding = false;
async response(options: {
tts?: TTSProvider;
@ -93,7 +105,7 @@ export class BaseSpeaker {
if (_response.length < 1) {
// 播放开始提示音
if (playSFX) {
await this.MiNA!.play({ url: process.env.AUDIO_BEEP });
await this.MiNA!.play({ url: this.audio_beep });
}
// 在播放 TTS 语音之前,先取消小爱音箱的唤醒状态,防止将 TTS 语音识别成用户指令
if (ttsNotXiaoai) {
@ -117,7 +129,7 @@ export class BaseSpeaker {
if (_response.length > 0) {
// 播放结束提示音
if (playSFX) {
await this.MiNA!.play({ url: process.env.AUDIO_BEEP });
await this.MiNA!.play({ url: this.audio_beep });
}
}
// 保持唤醒状态
@ -163,14 +175,14 @@ export class BaseSpeaker {
const play = async (args?: { tts?: string; url?: string }) => {
// 播放开始提示音
if (playSFX) {
await this.MiNA!.play({ url: process.env.AUDIO_BEEP });
await this.MiNA!.play({ url: this.audio_beep });
}
// 在播放 TTS 语音之前,先取消小爱音箱的唤醒状态,防止将 TTS 语音识别成用户指令
if (ttsNotXiaoai) {
await this.unWakeUp();
}
await this.MiNA!.play(args);
this.logger.success(ttsText ?? audio);
this.logger.log("🔊 " + (ttsText ?? audio));
// 等待回答播放完毕
while (true) {
const res = await this.MiNA!.getStatus();
@ -188,7 +200,7 @@ export class BaseSpeaker {
}
// 播放结束提示音
if (playSFX) {
await this.MiNA!.play({ url: process.env.AUDIO_BEEP });
await this.MiNA!.play({ url: this.audio_beep });
}
// 保持唤醒状态
if (keepAlive) {

View File

@ -1,4 +1,5 @@
import { firstOf, lastOf, sleep } from "../../utils/base";
import { kAreYouOK } from "../../utils/string";
import { BaseSpeaker, BaseSpeakerConfig } from "./base";
import { StreamResponse } from "./stream";
@ -38,6 +39,10 @@ export type SpeakerConfig = BaseSpeakerConfig & {
* 退30
*/
exitKeepAliveAfter?: number;
/**
*
*/
audio_silent?: string;
};
export class Speaker extends BaseSpeaker {
@ -47,7 +52,12 @@ export class Speaker extends BaseSpeaker {
constructor(config: SpeakerConfig) {
super(config);
const { heartbeat = 1000, exitKeepAliveAfter = 30 } = config;
const {
heartbeat = 1000,
exitKeepAliveAfter = 30,
audio_silent = process.env.AUDIO_SILENT,
} = config;
this.audio_silent = audio_silent;
this._commands = config.commands ?? [];
this.heartbeat = heartbeat;
this.exitKeepAliveAfter = exitKeepAliveAfter;
@ -78,13 +88,16 @@ export class Speaker extends BaseSpeaker {
}
}
audio_silent?: string;
async activeKeepAliveMode() {
while (this.status === "running") {
if (this.keepAlive) {
// 唤醒中
if (!this.responding) {
// 没有回复时,一直播放静音音频使小爱闭嘴
await this.MiNA?.play({ url: process.env.AUDIO_SILENT });
await this.MiNA?.play(
this.audio_silent ? { url: this.audio_silent } : { tts: kAreYouOK }
);
}
}
await sleep(this.interval);

View File

@ -2,6 +2,8 @@ import { readJSONSync } from "./io";
export const kVersion = readJSONSync("package.json").version;
export const kAreYouOK = "¿ʞо ∩оʎ ǝɹɐ"; // are you ok?
export const kBannerASCII = `
/$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$$

View File

@ -7,16 +7,18 @@ import { testSpeaker } from "./speaker";
import { testOpenAI } from "./openai";
import { testMyBot } from "./bot";
import { testLog } from "./log";
import { testMiGPT } from "./migpt";
dotenv.config();
async function main() {
println(kBannerASCII);
// println(kBannerASCII);
// testDB();
// testSpeaker();
// testOpenAI();
// testMyBot();
testLog();
// testLog();
testMiGPT();
}
runWithDB(main);

60
tests/migpt.ts Normal file
View File

@ -0,0 +1,60 @@
import { MiGPT } from "../src";
const botProfile = `
20
-
-
-
-
-
- 穿穿
-
-
-
-
-
-
-
-
`;
const masterProfile = `
18
- 1988
- 线
-
-
`;
export async function testMiGPT() {
const name = "豆包";
const client = MiGPT.create({
speaker: {
name,
tts: "doubao",
userId: process.env.MI_USER!,
password: process.env.MI_PASS!,
did: process.env.MI_DID,
},
bot: {
name,
profile: botProfile,
},
master: {
name: "王黎",
profile: masterProfile,
},
});
await client.start();
}