@minto-ai/huoshan-mp-tts
Version:
借助“火山引擎在线语音合成API”实现微信小程序端“文本转语音
512 lines (511 loc) • 17.6 kB
JavaScript
var A = Object.defineProperty;
var S = (s, e, t) => e in s ? A(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
var n = (s, e, t) => S(s, typeof e != "symbol" ? e + "" : e, t);
class T {
constructor() {
n(this, "listeners", /* @__PURE__ */ new Map());
}
on(e, t) {
const i = this.listeners.get(e);
i ? i.push(t) : this.listeners.set(e, [t]);
}
emit(e, t) {
const i = this.listeners.get(e);
i && i.forEach((a) => {
a(t);
});
}
clear() {
this.listeners.clear();
}
}
let I = null;
function d() {
return I || (I = new T()), I;
}
function _() {
const s = Date.now().toString(36), e = Math.floor(Math.random() * 2 ** 32).toString(36);
return s + e;
}
function k(s) {
return Object.entries(s).map(([e, t]) => `${encodeURIComponent(e)}=${encodeURIComponent(t)}`).join("&");
}
class R {
/**
* 处理器被激活(首次激活触发)
*/
onActive() {
}
/**
* 处理器首次执行任务之前
*/
onBeforeFirstExecute() {
}
/**
* 处理器执行任务完成
*/
onCompleted() {
}
/**
* 处理器执被销毁
*/
onFinish() {
}
}
class b extends R {
constructor() {
super(...arguments);
n(this, "nextHandler", null);
n(this, "executeController", null);
n(this, "isFirstExecute", !0);
n(this, "isHandleDataAcceptedComplete", !1);
}
setExecuteController(t) {
this.executeController = t;
}
linkHandler(t) {
this.nextHandler = t;
}
get isLastHandler() {
return !this.nextHandler;
}
forwardToHandler(t) {
this.nextHandler && this.nextHandler.handle(t);
}
setHandlerStatus(t) {
this.handlerStatus = t;
}
equalHandlerStatus(t) {
return this.handlerStatus === t;
}
}
var l = /* @__PURE__ */ ((s) => (s.OFFLINE = "offline", s.ACTIVE = "active", s.PENDING = "pending", s.EXECUTING = "executing", s.COMPLETED = "completed", s.FINISH = "finish", s))(l || {}), H = /* @__PURE__ */ ((s) => (s.PENDING = "pending", s.EXECUTING = "executing", s.COMPLETED = "completed", s))(H || {});
const P = d();
class f extends b {
constructor() {
super();
/**
* 待处理任务队列
*/
n(this, "taskQueue", []);
/**
* 当前处理器状态
*/
n(this, "handlerStatus", l.OFFLINE);
}
/**
* 获取待处理任务数量
*/
get taskQueueLength() {
return this.taskQueue.length;
}
/**
* 检查任务执行条件
* 子类可重写此方法,实现自定义的任务执行前置条件检查
*
* @returns {boolean} - 当满足执行条件时返回 true,否则返回 false
*/
executePreCheck() {
return !0;
}
/**
* 触发处理器激活行为(空闲状态 => 激活状态)
*/
triggerHandlerActive() {
this.setHandlerStatus(l.ACTIVE), this.onActive(), this.triggerHandlerPending();
}
/**
* 触发处理器待机行为(激活状态 => 待执行状态 执行状态 => 待执行状态)
*/
triggerHandlerPending() {
this.setHandlerStatus(l.PENDING);
}
/**
* 触发处理器执行行为(激活状态 => 执行状态)
*/
triggerHandlerExecute() {
this.setHandlerStatus(l.EXECUTING);
const t = this.taskQueue.shift();
Promise.resolve().then(() => {
const { isFirstExecute: i } = this;
i && (this.isFirstExecute = !1, this.onBeforeFirstExecute());
let a = !1;
this.isHandleDataAcceptedComplete && !this.taskQueueLength && (a = !0);
const r = {
taskItem: t,
isFirstExecute: i,
isLastExecute: a
};
this.execute(r);
});
}
/**
* 触发处理器成功行为(执行状态 => 成功状态)
*/
triggerHandlerCompleted() {
this.setHandlerStatus(l.COMPLETED), this.nextHandler && (this.nextHandler.handle(null), this.nextHandler.isHandleDataAcceptedComplete = !0), this.onCompleted();
}
/**
* 触发处理器销毁行为(待机状态 => 销毁状态)
*/
triggerHandlerFinish() {
this.taskQueue = [], this.isFirstExecute = !0, this.isHandleDataAcceptedComplete = !1, this.setHandlerStatus(l.FINISH), this.onFinish();
}
/**
* 触发应用被销毁行为
*/
triggerAppFinish() {
P.emit("_appFinish");
}
/**
* 任务完成后的回调函数。
*/
taskCompletedCallback() {
this.taskQueueLength ? this.triggerHandlerExecute() : this.isHandleDataAcceptedComplete ? (this.triggerHandlerCompleted(), this.isLastHandler && this.triggerAppFinish()) : this.triggerHandlerPending();
}
handle(t) {
if (this.taskQueue.push({
uuid: _(),
original: t,
status: H.PENDING
}), !this.executePreCheck()) {
this.triggerHandlerPending();
return;
}
this.equalHandlerStatus(l.PENDING) && this.triggerHandlerExecute();
}
}
const x = d(), p = class p extends f {
constructor() {
super(...arguments);
n(this, "audioContext", wx.createWebAudioContext());
n(this, "bufferSource", null);
n(this, "gainNode", null);
n(this, "volume", 1);
}
execute(t) {
if (t.isLastExecute) {
this.taskCompletedCallback();
return;
}
this.bufferSource = this.audioContext.createBufferSource(), this.bufferSource.buffer = t.taskItem.original, this.gainNode = this.audioContext.createGain(), this.bufferSource.connect(this.gainNode), this.gainNode.connect(this.audioContext.destination), this.gainNode.gain.setValueAtTime(this.volume, this.audioContext.currentTime), this.gainNode.gain.linearRampToValueAtTime(1, this.audioContext.currentTime + p.FADE_DURATION), this.gainNode.gain.setValueAtTime(1, this.audioContext.currentTime + t.taskItem.original.duration - p.FADE_DURATION), this.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + t.taskItem.original.duration), this.bufferSource.start(), this.bufferSource.onended = () => {
this.taskCompletedCallback();
};
}
onFinish() {
this.bufferSource && (this.bufferSource.stop(), this.bufferSource = null), this.gainNode && (this.gainNode.disconnect(), this.gainNode = null), this.audioContext && (this.audioContext.suspend(), this.audioContext = wx.createWebAudioContext()), x.emit("_audioActuatorFinish");
}
onBeforeFirstExecute() {
x.emit("_audioActuatorBeforeFirstExecute");
}
};
n(p, "FADE_DURATION", 0.018);
let m = p;
const F = d(), g = class g extends f {
constructor() {
super(...arguments);
n(this, "lastRemainingByte", new Uint8Array([]));
}
execute(t) {
if (t.isLastExecute) {
const i = new Uint8Array([...this.lastRemainingByte]);
i.length > g.MIN_LOAD_SIZE && this.forwardToHandler(i), this.lastRemainingByte = new Uint8Array([]), this.taskCompletedCallback();
} else {
const i = new Uint8Array([...this.lastRemainingByte, ...t.taskItem.original]);
i.length >= g.MIN_BYTE_SIZE ? (this.forwardToHandler(i), this.lastRemainingByte = new Uint8Array([])) : this.lastRemainingByte = i, this.taskCompletedCallback();
}
}
onFinish() {
F.emit("_byteBufferFinish"), this.lastRemainingByte = new Uint8Array([]);
}
};
n(g, "MIN_BYTE_SIZE", 60 * 1024), n(g, "MIN_LOAD_SIZE", 0.1 * 1024);
let N = g;
const w = d();
class y extends f {
constructor() {
super(...arguments);
n(this, "audioContext", wx.createWebAudioContext());
}
execute(t) {
if (t.isLastExecute) {
this.taskCompletedCallback();
return;
}
const i = t.taskItem.original.buffer;
if (!i) {
this.taskCompletedCallback();
return;
}
this.audioContext.decodeAudioData(i, (a) => {
this.forwardToHandler(a), this.taskCompletedCallback();
}, () => {
this.forwardToHandler(this.audioContext.createBuffer(1, 1, 1)), this.taskCompletedCallback();
});
}
onFinish() {
this.audioContext && (this.audioContext.suspend(), this.audioContext = wx.createWebAudioContext()), w.emit("_decodeDataFinish");
}
}
var D = Object.defineProperty, G = (s, e, t) => e in s ? D(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t, h = (s, e, t) => G(s, typeof e != "symbol" ? e + "" : e, t), c = /* @__PURE__ */ ((s) => (s.SPACE_CHAR = "spaceChar", s.CHINESE_CHAR = "chineseChar", s.CHINESE_PUNCTUATION = "chinesePunctuation", s.ENGLISH_WORD = "englishWord", s.ENGLISH_PUNCTUATION = "englishPunctuation", s.UNKNOWN_CHAR = "unknownChar", s))(c || {});
class U {
constructor() {
h(this, "CHINESE_CHAR_REGEX", /[\u4E00-\u9FA5]/), h(this, "CHINESE_PUNCTUATION_REGEX", /[。?!,;:“”‘’()—…、·]/), h(this, "CHINESE_PARAGRAPH_END_REGEX", /[。?!;]/), h(this, "ENGLISH_WORD_START_REGEX", /[a-z]/i), h(this, "ENGLISH_WORD_CONTINUE_REGEX", /[a-z'’-]/i), h(this, "ENGLISH_PUNCTUATION_REGEX", /[.,!?;:'"()-]/), h(this, "ENGLISH_PARAGRAPH_END_REGEX", /[.!?;]/), h(this, "SPACE_CHAR_REGEX", /\s/), h(this, "MIN_PARAGRAPH_LENGTH", 10), h(this, "pendingTokens", []);
}
/**
* 将输入文本转换为标记流。
* @param text 原始文本内容。
* @returns 标记化后的文本标记数组。
*/
tokenizeText(e) {
const t = [];
let i = 0;
for (; i < e.length; ) {
const a = e[i];
if (this.CHINESE_CHAR_REGEX.test(a))
t.push({
type: c.CHINESE_CHAR,
value: a
});
else if (this.ENGLISH_WORD_START_REGEX.test(a)) {
const r = i;
let o = i + 1;
for (; o < e.length && this.ENGLISH_WORD_CONTINUE_REGEX.test(e[o]); )
o++;
const C = e.substring(r, o);
t.push({
type: c.ENGLISH_WORD,
value: C
}), i += C.length - 1;
} else this.CHINESE_PUNCTUATION_REGEX.test(a) ? t.push({
type: c.CHINESE_PUNCTUATION,
value: a
}) : this.ENGLISH_PUNCTUATION_REGEX.test(a) ? t.push({
type: c.ENGLISH_PUNCTUATION,
value: a
}) : this.SPACE_CHAR_REGEX.test(a) ? t.push({
type: c.SPACE_CHAR,
value: a
}) : t.push({
type: c.UNKNOWN_CHAR,
value: a
});
i++;
}
return t;
}
/**
* 文本净化预处理 - 去除无效字符和格式。
* @param text 原始文本。
* @returns 净化后的文本。
*/
sanitizeText(e) {
return e.trim().replace(/\s+/g, " ").replace(/(\d+)\s+/g, "$1").replace(/\*+/g, "").replace(/[\u{1F600}-\u{1F64F}]/gu, "");
}
/**
* 判断标记是否为段落结束符号。
* @param token 文本标记。
* @returns 是否为段落结束符号。
*/
isParagraphEndToken(e) {
return this.CHINESE_PARAGRAPH_END_REGEX.test(e.value) || this.ENGLISH_PARAGRAPH_END_REGEX.test(e.value);
}
/**
* 将标记数组转换回文本。
* @param tokens 文本标记数组。
* @returns 组合后的文本。
*/
tokensToString(e) {
return e.map((t) => t.value).join("");
}
/**
* 将标记流分割成多个段落。
* @param tokens 文本标记数组。
* @returns 分段后的标记数组集合。
*/
splitToParagraphs(e) {
const t = [];
let i = [];
for (const a of e)
i.push(a), i.length >= this.MIN_PARAGRAPH_LENGTH && this.isParagraphEndToken(a) && (t.push(i), i = []);
return i.length > 0 && t.push(i), t;
}
/**
* 合并因小数点导致的错误分段。
* @param paragraphs 分段后的标记数组集合。
* @returns 合并后的标记数组集合。
*/
mergeDecimalParagraphs(e) {
return e.reduce((t, i) => {
if (t.length === 0)
t.push(i);
else {
const a = t[t.length - 1], r = this.tokensToString(a), o = this.tokensToString(i);
/\d+\.$/.test(r) && /^\d+/.test(o) ? t[t.length - 1] = [...a, ...i] : t.push(i);
}
return t;
}, []);
}
/**
* 处理输入文本,返回分段后的文本数组。
* @param text 输入文本内容。
* @param includeRemaining 是否包含未完成的段落。
* @returns 分段后的文本数组。
*/
processText(e, t = !1) {
const i = this.tokenizeText(this.sanitizeText(e)), a = [...this.pendingTokens, ...i], r = this.mergeDecimalParagraphs(this.splitToParagraphs(a));
if (r.length > 0) {
const o = r[r.length - 1], C = o[o.length - 1];
t ? this.pendingTokens = [] : this.isParagraphEndToken(C) ? /\d+\.$/.test(this.tokensToString(o)) ? this.pendingTokens = r.pop() : this.pendingTokens = [] : this.pendingTokens = r.pop();
}
return r.map((o) => this.tokensToString(o));
}
}
const O = d();
class v extends f {
constructor() {
super(...arguments);
n(this, "textStreamSlicer", new U());
}
execute(t) {
t.isLastExecute ? this.textStreamSlicer.processText("", !0).forEach((i) => {
this.forwardToHandler(i);
}) : this.textStreamSlicer.processText(t.taskItem.original).forEach((i) => {
this.forwardToHandler(i);
}), this.taskCompletedCallback();
}
onFinish() {
O.emit("_textSplitFinish");
}
}
const L = d(), B = {
voice_type: "zh_female_daimengchuanmei_moon_bigtts",
speed_ratio: 1,
volume_ratio: 1,
pitch_ratio: 1,
language: "cn"
};
class X extends f {
constructor() {
super(...arguments);
n(this, "webSocketInstance", null);
n(this, "businessParams");
}
initProperty(t) {
this.businessParams = { ...B, ...t };
}
onActive() {
this.webSocketInstance = wx.connectSocket({
url: this.generateRequestUrl(),
complete: () => {
}
}), this.webSocketInstance.onMessage((t) => {
Object.prototype.toString.call(t.data) === "[object ArrayBuffer]" ? this.forwardToHandler(new Uint8Array(t.data)) : JSON.parse(t.data).event === "sentence_end" && this.taskCompletedCallback();
}), this.webSocketInstance.onOpen(() => {
this.taskCompletedCallback();
});
}
executePreCheck() {
var t;
return ((t = this.webSocketInstance) == null ? void 0 : t.readyState) === 1;
}
execute(t) {
if (t.isLastExecute) {
this.taskCompletedCallback();
return;
}
this.webSocketInstance && this.webSocketInstance.send({
data: JSON.stringify(this.generateRequestParams(t.taskItem.original))
});
}
generateRequestParams(t) {
return {
event: "text",
text: t
};
}
generateRequestUrl() {
return `wss://audio.workbrain.cn/tts?${k({
...this.businessParams,
stream: !0
})}`;
}
onFinish() {
var t;
L.emit("_ttsRequestFinish"), (t = this.webSocketInstance) == null || t.close({}), this.webSocketInstance = null;
}
}
var u = /* @__PURE__ */ ((s) => (s.OFFLINE = "offline", s.EXECUTE = "execute", s))(u || {});
const E = d();
class $ {
constructor(e = {}) {
n(this, "textSplitInstance", new v());
n(this, "audioActuatorInstance", new m());
n(this, "ttsRequestInstance", new X());
n(this, "byteBufferInstance", new N());
n(this, "decodeDataInstance", new y());
n(this, "systemStatus", u.OFFLINE);
this.ttsRequestInstance.initProperty(e), this.textSplitInstance.setExecuteController(this), this.ttsRequestInstance.setExecuteController(this), this.byteBufferInstance.setExecuteController(this), this.decodeDataInstance.setExecuteController(this), this.audioActuatorInstance.setExecuteController(this), this.textSplitInstance.linkHandler(this.ttsRequestInstance), this.ttsRequestInstance.linkHandler(this.byteBufferInstance), this.byteBufferInstance.linkHandler(this.decodeDataInstance), this.decodeDataInstance.linkHandler(this.audioActuatorInstance), wx.setInnerAudioOption && wx.setInnerAudioOption({ obeyMuteSwitch: !1 }), this.bindEvent();
}
bindEvent() {
E.on("_audioActuatorBeforeFirstExecute", () => {
this.emit("audioFirstStart");
}), E.on("_appError", (e) => {
this.finish(), this.emit("appError", e);
}), E.on("_appFinish", () => {
this.finish();
});
}
/**
* 进入待机状态,等待传入文本数据
*/
start() {
return this.systemStatus === u.EXECUTE ? this : (this.textSplitInstance.triggerHandlerActive(), this.ttsRequestInstance.triggerHandlerActive(), this.byteBufferInstance.triggerHandlerActive(), this.decodeDataInstance.triggerHandlerActive(), this.audioActuatorInstance.triggerHandlerActive(), this.systemStatus = u.EXECUTE, this);
}
/**
* 传入文本数据
*
* @param text 待转换的文本
*/
send(e) {
return this.systemStatus !== u.EXECUTE ? this : (this.textSplitInstance.handle(e), this);
}
/**
* 应用停止处理传入的文本,但是并不会停止音频播放
*/
end() {
return this.systemStatus !== u.EXECUTE ? this : (this.textSplitInstance.handle(null), this.textSplitInstance.isHandleDataAcceptedComplete = !0, this);
}
/**
* 停止所有处理器,并且重置状态,触发 appFinish 事件
*/
finish() {
this.systemStatus !== u.OFFLINE && (this.textSplitInstance.triggerHandlerFinish(), this.ttsRequestInstance.triggerHandlerFinish(), this.byteBufferInstance.triggerHandlerFinish(), this.decodeDataInstance.triggerHandlerFinish(), this.audioActuatorInstance.triggerHandlerFinish(), this.systemStatus = u.OFFLINE, this.emit("appFinish"), E.clear());
}
emit(e, t) {
E.emit(e, t);
}
on(e, t) {
return E.on(e, t), this;
}
}
const M = {
/**
* 配置系统参数。
* @returns {object} 返回当前对象,支持链式调用。
*/
config() {
return this;
},
/**
* 创建文本转语音控制器实例。
* @param {Partial<BusinessParams>} [businessParams] - 业务参数,默认为空对象。
* @returns {TtsController} 返回TtsController实例。
*/
create(s = {}) {
return new $(s);
}
};
export {
M as default
};