@minto-ai/huoshan-mp-tts
Version:
借助“火山引擎在线语音合成API”实现微信小程序端“文本转语音
981 lines (980 loc) • 30.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
class EventBus {
constructor() {
__publicField(this, "listeners", /* @__PURE__ */ new Map());
}
on(eventName, callback) {
const callbacks = this.listeners.get(eventName);
if (callbacks) {
callbacks.push(callback);
} else {
this.listeners.set(eventName, [callback]);
}
}
emit(eventName, data) {
const callbacks = this.listeners.get(eventName);
if (callbacks) {
callbacks.forEach((callback) => {
callback(data);
});
}
}
}
function createEventBus() {
return new EventBus();
}
function getUuid() {
const timestamp = Date.now().toString(36);
const randomValue = Math.floor(Math.random() * 2 ** 32).toString(36);
return timestamp + randomValue;
}
function objectToQueryString(obj) {
return Object.entries(obj).map(([key, value]) => {
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}).join("&");
}
class SsmlConverter {
/**
* 创建 Ssml 转换器
* @param pronunciationRules 多音字/词规则列表,按长度降序用于最长匹配
*/
constructor(pronunciationRules) {
__publicField(this, "pronunciationRules");
this.pronunciationRules = [
...pronunciationRules.sort((a, b) => b.content.length - a.content.length)
];
}
/**
* 批量转换段落为 Ssml 字符串数组
* @param paragraphs 段落文本数组
* @returns Ssml 字符串数组
*/
convertParagraphs(paragraphs) {
const ssmlList = [];
for (const paragraph of paragraphs) {
ssmlList.push(this.convertParagraph(paragraph));
}
return ssmlList;
}
/**
* 单段落转换为 Ssml
* @param paragraph 段落文本
* @returns Ssml 字符串
*/
convertParagraph(paragraph) {
let ssmlText = "";
let cursor = 0;
while (cursor < paragraph.length) {
const matchedRule = this.pronunciationRules.find(
(rule) => paragraph.startsWith(rule.content, cursor)
) ?? null;
if (matchedRule !== null) {
ssmlText += `<phoneme alphabet="${matchedRule.alphabet}" ph="${matchedRule.ph}">${matchedRule.content}</phoneme>`;
cursor += matchedRule.content.length;
} else {
ssmlText += paragraph[cursor];
cursor += 1;
}
}
return `<speak>${ssmlText}</speak>`;
}
}
let ssmlConverter = null;
function createSsmlConverter(pronunciationRules) {
if (ssmlConverter === null) {
ssmlConverter = new SsmlConverter(pronunciationRules);
}
return ssmlConverter;
}
class LifeHandler {
/**
* 处理器被激活(首次激活触发)
*/
onActive() {
}
/**
* 处理器首次执行任务之前
*/
onBeforeFirstExecute() {
}
/**
* 处理器执行任务完成
*/
onCompleted() {
}
/**
* 处理器执被销毁
*/
onFinish() {
}
}
class BaseHandler extends LifeHandler {
constructor() {
super(...arguments);
__publicField(this, "nextHandler", null);
__publicField(this, "executeController", null);
__publicField(this, "isFirstExecute", true);
__publicField(this, "isHandleDataAcceptedComplete", false);
}
setExecuteController(executeController) {
this.executeController = executeController;
}
linkHandler(nextHandler) {
this.nextHandler = nextHandler;
}
get isLastHandler() {
return !this.nextHandler;
}
forwardToHandler(result) {
if (this.nextHandler) {
this.nextHandler.handle(result);
}
}
setHandlerStatus(status) {
this.handlerStatus = status;
}
equalHandlerStatus(status) {
return this.handlerStatus === status;
}
}
var SerialHandlerStatus = /* @__PURE__ */ ((SerialHandlerStatus2) => {
SerialHandlerStatus2["OFFLINE"] = "offline";
SerialHandlerStatus2["ACTIVE"] = "active";
SerialHandlerStatus2["PENDING"] = "pending";
SerialHandlerStatus2["EXECUTING"] = "executing";
SerialHandlerStatus2["COMPLETED"] = "completed";
SerialHandlerStatus2["FINISH"] = "finish";
return SerialHandlerStatus2;
})(SerialHandlerStatus || {});
var SerialTaskItemStatus = /* @__PURE__ */ ((SerialTaskItemStatus2) => {
SerialTaskItemStatus2["PENDING"] = "pending";
SerialTaskItemStatus2["EXECUTING"] = "executing";
SerialTaskItemStatus2["COMPLETED"] = "completed";
return SerialTaskItemStatus2;
})(SerialTaskItemStatus || {});
class SerialHandler extends BaseHandler {
constructor() {
super();
/**
* 待处理任务队列
*/
__publicField(this, "taskQueue", []);
/**
* 当前处理器状态
*/
__publicField(this, "handlerStatus", SerialHandlerStatus.OFFLINE);
}
/**
* 获取待处理任务数量
*/
get taskQueueLength() {
return this.taskQueue.length;
}
/**
* 检查任务执行条件
* 子类可重写此方法,实现自定义的任务执行前置条件检查
*
* @returns {boolean} - 当满足执行条件时返回 true,否则返回 false
*/
executePreCheck() {
return true;
}
/**
* 触发处理器激活行为(空闲状态 => 激活状态)
*/
triggerHandlerActive() {
this.setHandlerStatus(SerialHandlerStatus.ACTIVE);
this.onActive();
this.triggerHandlerPending();
}
/**
* 触发处理器待机行为(激活状态 => 待执行状态 执行状态 => 待执行状态)
*/
triggerHandlerPending() {
this.setHandlerStatus(SerialHandlerStatus.PENDING);
}
/**
* 触发处理器执行行为(激活状态 => 执行状态)
*/
triggerHandlerExecute() {
this.setHandlerStatus(SerialHandlerStatus.EXECUTING);
const taskItem = this.taskQueue.shift();
Promise.resolve().then(() => {
const { isFirstExecute } = this;
if (isFirstExecute) {
this.isFirstExecute = false;
this.onBeforeFirstExecute();
}
let isLastExecute = false;
if (this.isHandleDataAcceptedComplete && !this.taskQueueLength) {
isLastExecute = true;
}
const context = {
taskItem,
isFirstExecute,
isLastExecute
};
this.execute(context);
});
}
/**
* 触发处理器成功行为(执行状态 => 成功状态)
*/
triggerHandlerCompleted() {
this.setHandlerStatus(SerialHandlerStatus.COMPLETED);
if (this.nextHandler) {
this.nextHandler.handle(null);
this.nextHandler.isHandleDataAcceptedComplete = true;
}
this.onCompleted();
}
/**
* 触发处理器销毁行为(待机状态 => 销毁状态)
*/
triggerHandlerFinish() {
this.taskQueue = [];
this.isFirstExecute = true;
this.isHandleDataAcceptedComplete = false;
this.setHandlerStatus(SerialHandlerStatus.FINISH);
this.onFinish();
}
/**
* 触发应用被销毁行为
*/
triggerAppFinish() {
var _a;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_appFinish");
}
/**
* 任务完成后的回调函数。
*/
taskCompletedCallback() {
if (this.taskQueueLength) {
this.triggerHandlerExecute();
} else {
if (this.isHandleDataAcceptedComplete) {
this.triggerHandlerCompleted();
if (this.isLastHandler) {
this.triggerAppFinish();
}
} else {
this.triggerHandlerPending();
}
}
}
handle(original) {
this.taskQueue.push({
uuid: getUuid(),
original,
status: SerialTaskItemStatus.PENDING
});
if (!this.executePreCheck()) {
this.triggerHandlerPending();
return;
}
if (this.equalHandlerStatus(SerialHandlerStatus.PENDING)) {
this.triggerHandlerExecute();
}
}
}
const _AudioActuator = class _AudioActuator extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "audioContext", wx.createWebAudioContext());
__publicField(this, "bufferSource", null);
__publicField(this, "gainNode", null);
__publicField(this, "volume", 1);
}
onActive() {
if (!this.audioContext) {
this.audioContext = wx.createWebAudioContext();
this.audioContext.resume();
}
}
execute(context) {
if (context.isLastExecute) {
this.taskCompletedCallback();
return;
}
this.bufferSource = this.audioContext.createBufferSource();
this.bufferSource.buffer = context.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 + _AudioActuator.FADE_DURATION);
this.gainNode.gain.setValueAtTime(1, this.audioContext.currentTime + context.taskItem.original.duration - _AudioActuator.FADE_DURATION);
this.gainNode.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + context.taskItem.original.duration);
this.bufferSource.start();
this.bufferSource.onended = () => {
this.taskCompletedCallback();
};
}
onFinish() {
var _a;
if (this.bufferSource) {
this.bufferSource.stop();
this.bufferSource = null;
}
if (this.gainNode) {
this.gainNode.disconnect();
this.gainNode = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_audioActuatorFinish");
}
onBeforeFirstExecute() {
var _a;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_audioActuatorBeforeFirstExecute");
}
};
__publicField(_AudioActuator, "FADE_DURATION", 0.018);
let AudioActuator = _AudioActuator;
const _ByteBuffer = class _ByteBuffer extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "lastRemainingByte", new Uint8Array([]));
}
execute(context) {
if (context.isLastExecute) {
const originalByte = new Uint8Array([...this.lastRemainingByte]);
if (originalByte.length > _ByteBuffer.MIN_LOAD_SIZE) {
this.forwardToHandler(originalByte);
}
this.lastRemainingByte = new Uint8Array([]);
this.taskCompletedCallback();
} else {
const originalByte = new Uint8Array([...this.lastRemainingByte, ...context.taskItem.original]);
if (originalByte.length >= _ByteBuffer.MIN_BYTE_SIZE) {
this.forwardToHandler(originalByte);
this.lastRemainingByte = new Uint8Array([]);
} else {
this.lastRemainingByte = originalByte;
}
this.taskCompletedCallback();
}
}
onFinish() {
var _a;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_byteBufferFinish");
this.lastRemainingByte = new Uint8Array([]);
}
};
__publicField(_ByteBuffer, "MIN_BYTE_SIZE", 60 * 1024);
__publicField(_ByteBuffer, "MIN_LOAD_SIZE", 0.1 * 1024);
let ByteBuffer = _ByteBuffer;
class CreateSsml extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "ssmlConverter");
}
/**
* 初始化属性
* @param ssmlConfig SSML 配置(包含发音规则)
*/
initProperty(ssmlConfig) {
this.ssmlConverter = createSsmlConverter(ssmlConfig.pronunciationRules ?? []);
}
/**
* 执行处理,将当前文本转换为 SSML 并传递到下一个处理器
* @param context 串行任务上下文
*/
execute(context) {
if (context.isLastExecute) {
this.taskCompletedCallback();
} else {
this.ssmlConverter.convertParagraphs([context.taskItem.original]).forEach((paragraph) => {
this.forwardToHandler(paragraph);
});
this.taskCompletedCallback();
}
}
/**
* 完成回调,广播文本切割结束事件
*/
onFinish() {
var _a;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_createSsmlFinish");
}
}
class DecodeData extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "audioContext", wx.createWebAudioContext());
}
onActive() {
if (!this.audioContext) {
this.audioContext = wx.createWebAudioContext();
this.audioContext.resume();
}
}
execute(context) {
if (context.isLastExecute) {
this.taskCompletedCallback();
return;
}
const arrayBuffer = context.taskItem.original.buffer;
if (!arrayBuffer) {
this.taskCompletedCallback();
return;
}
this.audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
this.forwardToHandler(audioBuffer);
this.taskCompletedCallback();
}, () => {
this.forwardToHandler(this.audioContext.createBuffer(1, 1, 1));
this.taskCompletedCallback();
});
}
onFinish() {
var _a;
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_decodeDataFinish");
}
}
var o = Object.defineProperty;
var I = (r, t, e) => t in r ? o(r, t, { enumerable: true, configurable: true, writable: true, value: e }) : r[t] = e;
var i = (r, t, e) => I(r, typeof t != "symbol" ? t + "" : t, e);
var h = /* @__PURE__ */ ((r) => (r.SPACE_CHAR = "spaceChar", r.CHINESE_CHAR = "chineseChar", r.CHINESE_PUNCTUATION = "chinesePunctuation", r.ENGLISH_WORD = "englishWord", r.ENGLISH_PUNCTUATION = "englishPunctuation", r.NUMBER = "number", r.UNKNOWN_CHAR = "unknownChar", r))(h || {});
class d {
constructor() {
i(this, "CHINESE_CHAR_REGEX", /[\u4E00-\u9FA5]/);
i(this, "CHINESE_PUNCTUATION_REGEX", /[,。!?;:、"'()—…·]/);
i(this, "CHINESE_PARAGRAPH_END_REGEX", /[。!?;]/);
i(this, "ENGLISH_WORD_START_REGEX", /[a-z]/i);
i(this, "ENGLISH_WORD_CONTINUE_REGEX", /[a-z'’-]/i);
i(this, "ENGLISH_PUNCTUATION_REGEX", /[.,!?;:'"()-]/);
i(this, "ENGLISH_PARAGRAPH_END_REGEX", /[.!?;]/);
i(this, "SPACE_CHAR_REGEX", /\s/);
i(this, "DIGIT_REGEX", /\d/);
i(this, "DECIMAL_REGEX", /[\d.]/);
i(this, "TRAILING_CLOSER_REGEX", /[)"'\]}】》』」’”]/);
i(this, "MIN_PARAGRAPH_LENGTH", 10);
i(this, "pendingTokens", []);
}
/**
* 将输入文本转换为文本标记。
*
* 标记化规则:
* - 中文字符:单个字符作为一个标记
* - 中文标点:单个标点作为一个标记
* - 英文单词:连续英文字母,支持缩写撇号('’)和连字符(-)
* - 数字序列:支持小数点,如 22.5 作为一个完整数字标记
* - 空白字符:空格、制表符等作为一个标记
* - 其他字符:作为未知类型标记
*
* @param text 原始文本
* @returns 标记化后的文本标记数组
*/
textToTokens(t) {
const e = [];
let n = 0;
for (; n < t.length; ) {
const s = t[n];
if (this.CHINESE_CHAR_REGEX.test(s))
e.push({
type: h.CHINESE_CHAR,
value: s
});
else if (this.DIGIT_REGEX.test(s)) {
const l = n;
let a = n + 1;
for (; a < t.length && this.DECIMAL_REGEX.test(t[a]); )
a++;
const T = t.substring(l, a);
e.push({
type: h.NUMBER,
value: T
}), n += T.length - 1;
} else if (this.ENGLISH_WORD_START_REGEX.test(s)) {
const l = n;
let a = n + 1;
for (; a < t.length && this.ENGLISH_WORD_CONTINUE_REGEX.test(t[a]); )
a++;
const T = t.substring(l, a);
e.push({
type: h.ENGLISH_WORD,
value: T
}), n += T.length - 1;
} else this.CHINESE_PUNCTUATION_REGEX.test(s) ? e.push({
type: h.CHINESE_PUNCTUATION,
value: s
}) : this.ENGLISH_PUNCTUATION_REGEX.test(s) ? e.push({
type: h.ENGLISH_PUNCTUATION,
value: s
}) : this.SPACE_CHAR_REGEX.test(s) ? e.push({
type: h.SPACE_CHAR,
value: s
}) : e.push({
type: h.UNKNOWN_CHAR,
value: s
});
n++;
}
return e;
}
/**
* 文本标记转换为段落,并更新 pendingTokens。
*
* 跨次调用缓存逻辑:
* 1. 先将前次剩余标记(pendingTokens)与本次新标记拼接
* 2. 对拼接后的完整标记序列进行切分判断
* 3. 未达切分条件的标记缓存到 pendingTokens 供下次使用
*
* @param tokens 文本标记数组
* @returns 段落数组(每段为 Token[])
*/
tokenToParagraphs(t) {
const e = this.pendingTokens.length > 0 ? [...this.pendingTokens, ...t] : [...t];
this.pendingTokens = [];
const n = [];
let s = [], l = 0;
for (; l < e.length; ) {
const a = e[l];
if (s.push(a), s.length >= this.MIN_PARAGRAPH_LENGTH && (this.CHINESE_PARAGRAPH_END_REGEX.test(a.value) || this.ENGLISH_PARAGRAPH_END_REGEX.test(a.value))) {
let T = l + 1;
for (; T < e.length; ) {
const g = e[T];
if (g.type === h.SPACE_CHAR || this.TRAILING_CLOSER_REGEX.test(g.value))
s.push(g), T++;
else
break;
}
n.push([...s]), s = [], l = T - 1;
}
l += 1;
}
return s.length > 0 && (this.pendingTokens = [...s]), n;
}
/**
* 根据输入文本构建段落列表,并在必要时输出剩余标记。
*
* @param text 输入文本
* @param includeRemaining 是否包含待处理的剩余标记
* @returns 段落数组(每段为字符串)
*/
build(t, e) {
const n = this.textToTokens(t), s = this.tokenToParagraphs(n);
return e && this.pendingTokens.length > 0 && (s.push([...this.pendingTokens]), this.pendingTokens = []), s.map((a) => a.map((T) => T.value).join(""));
}
/**
* 重置段落构建器,清空内部 pendingTokens 缓存。
* 用于单例模式下重复调用时确保状态干净。
*/
reset() {
this.pendingTokens = [];
}
}
let E = null;
function N() {
return E !== null ? (E.reset(), E) : (E = new d(), E);
}
class A {
constructor() {
i(this, "pendingText", "");
i(this, "MD_LINK_TAIL_PATTERNS", [/\[$/, /\[[^\]]*$/, /\[[^\]]*\]$/, /\[[^\]]*\]\($/, /\[[^\]]*\]\([^)]*$/, /\[[^\]]*\]\([^)]*\)$/]);
i(this, "NUMBER_TAIL_PATTERNS", [/\d+$/, /\d+\.$/, /\d+\.\d+$/]);
i(this, "IMG_TAG_TAIL_PATTERNS", [/((<)|(<i)|(<im)|(<img))$/, /<img$/, /<img[^>]*$/, /<img[^>]*>$/, /<img[^>]*\/$/, /<img[^>]*\/>$/]);
i(this, "VIDEO_TAG_TAIL_PATTERNS", [/((<)|(<v)|(<vi)|(<vid)|(<vide)|(<video))$/, /((<\/v)|(<\/vi)|(<\/vid)|(<\/vide)|(<\/video))$/, /<video$/, /<video[^>]*$/, /<video[^>]*>$/, /<video[^>]*><\/$/, /<video[^>]*><\/video$/, /<video[^>]*><\/video>$/]);
}
/**
* 判断片段末尾是否处在 Markdown 链接结构中(未完成或完整)。
*
* @param text 输入文本。
* @returns 是否在链接尾部。
*/
isMarkdownLinkTail(t) {
return this.MD_LINK_TAIL_PATTERNS.some((e) => e.test(t));
}
/**
* 判断片段末尾是否是数字或小数的一部分(未完成或完整)。
*
* @param text 输入文本。
* @returns 是否命中数字尾部。
*/
isNumberTail(t) {
return this.NUMBER_TAIL_PATTERNS.some((e) => e.test(t));
}
/**
* 判断片段末尾是否处在 CSS img 标签结构中(未完成或完整)。
*
* @param text 输入文本。
* @returns 是否命中 img 标签尾部。
*/
isImgTagTail(t) {
return this.IMG_TAG_TAIL_PATTERNS.some((e) => e.test(t));
}
/**
* 判断片段末尾是否处在 HTML video 标签结构中(未完成或完整)。
*
* @param text 输入文本。
* @returns 是否命中 video 标签尾部。
*/
isVideoTagTail(t) {
return this.VIDEO_TAG_TAIL_PATTERNS.some((e) => e.test(t));
}
/**
* 合并缓存并判断是否需要继续缓存当前片段,以避免在未完成结构处切句。
*
* @param text 当前输入片段。
* @param includeRemaining 是否包含待处理的剩余文本。
* @returns 预处理结果:若触发缓存则返回空字符串与新的缓存;否则返回合并后的文本与空缓存。
*/
preprocess(t, e) {
let n = "";
return this.pendingText.length > 0 ? n = `${this.pendingText}${t}` : n = t, e ? (this.pendingText = "", {
processedText: n,
nextPendingText: ""
}) : this.isMarkdownLinkTail(n) ? (this.pendingText = n, {
processedText: "",
nextPendingText: n
}) : this.isNumberTail(n) ? (this.pendingText = n, {
processedText: "",
nextPendingText: n
}) : this.isImgTagTail(n) ? (this.pendingText = n, {
processedText: "",
nextPendingText: n
}) : this.isVideoTagTail(n) ? (this.pendingText = n, {
processedText: "",
nextPendingText: n
}) : (this.pendingText = "", {
processedText: n,
nextPendingText: ""
});
}
}
let _ = null;
function R() {
return _ !== null || (_ = new A()), _;
}
class P {
/**
* 执行语义替换。
* @param {string} text 输入文本
* @returns {string} 替换后的文本
*/
replace(t) {
let e = t;
return e = e.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"), e = e.replace(/<img[^>]*\/>/g, ""), e = e.replace(/<img[^>]*>/g, ""), e = e.replace(/<video[^>]*><\/video\s*>/g, ""), e;
}
}
let p = null;
function G() {
return p !== null || (p = new P()), p;
}
class x {
/**
* 清洗段落数组。
*
* 处理流程:
* 1. 去除每段首尾空白字符;
* 2. 过滤空字符串;
* 3. 过滤仅含标点/符号/空白而无字母、数字或中文的“无意义段”。
*
* @param paragraphs 原始段落数组
* @returns 清洗后的段落数组
*/
sanitize(t) {
return t.map((e) => e.trim()).filter((e) => e.length > 0).filter((e) => /[\p{L}\p{N}]/u.test(e));
}
}
let u = null;
function C() {
return u !== null || (u = new x()), u;
}
class S {
/**
* 清洗输入文本,按需移除特定字符。
*
* @param text 输入文本
* @returns 规范化后的文本
*/
sanitize(t) {
let e = t;
return e = e.replace(/\*+/g, ""), e = e.replace(/#+/g, ""), e = e.replace(/[\u{1F000}-\u{1FAFF}]/gu, ""), e;
}
}
let c = null;
function H() {
return c !== null || (c = new S()), c;
}
class f {
constructor() {
i(this, "sanitizeTextInstance", H());
i(this, "preprocessTextInstance", R());
i(this, "replaceTextInstance", G());
i(this, "paragraphsInstance", N());
i(this, "sanitizeParagraphsInstance", C());
}
/**
* 同步处理输入文本片段,返回已分段的文本数组
*
* @param text 本次输入的文本片段
* @param includeRemaining 是否已无后续文本
* @returns 分段后的段落数组
*/
processText(t, e = false) {
const n = this.sanitizeTextInstance.sanitize(t), {
processedText: s
} = this.preprocessTextInstance.preprocess(n, e), l = this.replaceTextInstance.replace(s), a = this.paragraphsInstance.build(l, e);
return this.sanitizeParagraphsInstance.sanitize(a);
}
}
class TextSplit extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "textStreamSlicer", new f());
}
execute(context) {
if (context.isLastExecute) {
this.textStreamSlicer.processText("", true).forEach((paragraph) => {
this.forwardToHandler(paragraph);
});
} else {
this.textStreamSlicer.processText(context.taskItem.original).forEach((paragraph) => {
this.forwardToHandler(paragraph);
});
}
this.taskCompletedCallback();
}
onFinish() {
var _a;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_textSplitFinish");
}
}
class TtsRequest extends SerialHandler {
constructor() {
super(...arguments);
__publicField(this, "webSocketInstance", null);
__publicField(this, "businessParams");
__publicField(this, "systemConfig");
}
initProperty(systemConfig2, businessParams) {
this.businessParams = { ...businessParams };
this.systemConfig = {
...systemConfig2
};
}
onActive() {
this.webSocketInstance = wx.connectSocket({
url: this.generateRequestUrl(),
complete: () => {
}
});
this.webSocketInstance.onMessage((response) => {
if (Object.prototype.toString.call(response.data) === "[object ArrayBuffer]") {
this.forwardToHandler(new Uint8Array(response.data));
} else {
const data = JSON.parse(response.data);
if (data.event === "sentence_end") {
this.taskCompletedCallback();
}
}
});
this.webSocketInstance.onOpen(() => {
this.taskCompletedCallback();
});
}
executePreCheck() {
var _a;
return ((_a = this.webSocketInstance) == null ? void 0 : _a.readyState) === 1;
}
execute(context) {
if (context.isLastExecute) {
this.taskCompletedCallback();
return;
}
if (this.webSocketInstance) {
this.webSocketInstance.send({
data: JSON.stringify(this.generateRequestParams(context.taskItem.original))
});
}
}
generateRequestParams(text) {
return {
event: "text",
text
};
}
generateRequestUrl() {
return `${this.systemConfig.ttsRequestBaseUrl}?${objectToQueryString({ ...this.businessParams })}`;
}
onFinish() {
var _a, _b;
(_a = this.executeController) == null ? void 0 : _a.$bus.emit("_ttsRequestFinish");
(_b = this.webSocketInstance) == null ? void 0 : _b.close({});
this.webSocketInstance = null;
}
}
var SystemStatus = /* @__PURE__ */ ((SystemStatus2) => {
SystemStatus2["OFFLINE"] = "offline";
SystemStatus2["EXECUTE"] = "execute";
return SystemStatus2;
})(SystemStatus || {});
class TtsController {
constructor(systemConfig2, businessParams, ssmlConfig) {
__publicField(this, "textSplitInstance", new TextSplit());
__publicField(this, "createSsmlInstance");
__publicField(this, "audioActuatorInstance", new AudioActuator());
__publicField(this, "ttsRequestInstance", new TtsRequest());
__publicField(this, "byteBufferInstance", new ByteBuffer());
__publicField(this, "decodeDataInstance", new DecodeData());
__publicField(this, "systemStatus", SystemStatus.OFFLINE);
__publicField(this, "$bus", createEventBus());
this.ttsRequestInstance.initProperty(systemConfig2, businessParams);
this.createSsmlInstance = new CreateSsml();
this.createSsmlInstance.initProperty(ssmlConfig);
this.textSplitInstance.setExecuteController(this);
this.createSsmlInstance.setExecuteController(this);
this.ttsRequestInstance.setExecuteController(this);
this.byteBufferInstance.setExecuteController(this);
this.decodeDataInstance.setExecuteController(this);
this.audioActuatorInstance.setExecuteController(this);
this.textSplitInstance.linkHandler(this.createSsmlInstance);
this.createSsmlInstance.linkHandler(this.ttsRequestInstance);
this.ttsRequestInstance.linkHandler(this.byteBufferInstance);
this.byteBufferInstance.linkHandler(this.decodeDataInstance);
this.decodeDataInstance.linkHandler(this.audioActuatorInstance);
if (wx.setInnerAudioOption) {
wx.setInnerAudioOption({ obeyMuteSwitch: false });
}
this.bindEvent();
}
/**
* 绑定事件监听器
*/
bindEvent() {
this.$bus.on("_audioActuatorBeforeFirstExecute", () => {
this.emit("audioFirstStart");
});
this.$bus.on("_appError", (error) => {
this.finish();
this.emit("appError", error);
});
this.$bus.on("_appFinish", () => {
this.finish();
});
}
/**
* 进入待机状态,等待传入文本数据
*/
start() {
if (this.systemStatus === SystemStatus.EXECUTE) {
return this;
}
this.textSplitInstance.triggerHandlerActive();
this.createSsmlInstance.triggerHandlerActive();
this.ttsRequestInstance.triggerHandlerActive();
this.byteBufferInstance.triggerHandlerActive();
this.decodeDataInstance.triggerHandlerActive();
this.audioActuatorInstance.triggerHandlerActive();
this.systemStatus = SystemStatus.EXECUTE;
return this;
}
/**
* 传入文本数据
*
* @param text 待转换的文本
*/
send(text) {
if (this.systemStatus !== SystemStatus.EXECUTE) {
return this;
}
this.textSplitInstance.handle(text);
return this;
}
/**
* 应用停止处理传入的文本,但是并不会停止音频播放
*/
end() {
if (this.systemStatus !== SystemStatus.EXECUTE) {
return this;
}
this.textSplitInstance.handle(null);
this.textSplitInstance.isHandleDataAcceptedComplete = true;
return this;
}
/**
* 停止所有处理器,并且重置状态,触发 appFinish 事件
*/
finish() {
if (this.systemStatus === SystemStatus.OFFLINE) {
return;
}
this.textSplitInstance.triggerHandlerFinish();
this.createSsmlInstance.triggerHandlerFinish();
this.ttsRequestInstance.triggerHandlerFinish();
this.byteBufferInstance.triggerHandlerFinish();
this.decodeDataInstance.triggerHandlerFinish();
this.audioActuatorInstance.triggerHandlerFinish();
this.systemStatus = SystemStatus.OFFLINE;
this.emit("appFinish");
}
emit(eventName, data) {
this.$bus.emit(eventName, data);
}
on(eventName, callback) {
this.$bus.on(eventName, callback);
return this;
}
}
let systemConfig;
const defaultSystemConfig = {
ttsRequestBaseUrl: "wss://audio.workbrain.cn/tts"
};
const defaultBusinessParams = {
voice_type: "zh_female_daimengchuanmei_moon_bigtts",
text_type: "plain",
speed_ratio: 1,
volume_ratio: 1,
pitch_ratio: 1,
language: "cn",
provider: 0,
cache: false,
stream: true
};
const defaultSsmlConfig = {
pronunciationRules: []
};
const index = {
/**
* 配置系统参数。
* @param {SystemConfig} [_systemConfig] - 系统配置参数。
* @returns {TtsController} 返回TtsController实例。
*/
config(_systemConfig) {
systemConfig = {
...defaultSystemConfig,
..._systemConfig
};
return this;
},
/**
* 创建文本转语音控制器实例。
* @param {Partial<BusinessParams>} [_businessParams] - 业务参数,默认为空对象。
* @param {Partial<SsmlConfig>} [_ssmlConfig] - SSML配置参数,默认为空对象。
* @returns {TtsController} 返回TtsController实例。
*/
create(_businessParams = {}, _ssmlConfig = {}) {
const businessParams = {
...defaultBusinessParams,
..._businessParams
};
const ssmlConfig = {
...defaultSsmlConfig,
..._ssmlConfig
};
return new TtsController(systemConfig, businessParams, ssmlConfig);
}
};
export {
index as default
};