From 8e04962cc18c8e612bf91efd4b6a9b11e5f6ce8d Mon Sep 17 00:00:00 2001 From: abearxiong Date: Fri, 9 May 2025 23:11:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tts/provider/volcengine/test/mix.ts | 47 ++++++++++++++-- src/tts/provider/volcengine/tts-mix.ts | 71 ++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 12 deletions(-) diff --git a/src/tts/provider/volcengine/test/mix.ts b/src/tts/provider/volcengine/test/mix.ts index 18cdf79..31caa69 100644 --- a/src/tts/provider/volcengine/test/mix.ts +++ b/src/tts/provider/volcengine/test/mix.ts @@ -1,5 +1,5 @@ import { config } from './common.ts'; -import { runDemo } from '../tts-mix.ts'; +import { runDemo, TtsMix } from '../tts-mix.ts'; const appId = config.APP_ID; const token = config.TOKEN; @@ -7,11 +7,50 @@ const token = config.TOKEN; const speaker = 'zh_female_roumeinvyou_emo_v2_mars_bigtts'; const text = '明朝开国皇帝朱元璋也称这本书为,万物之根'; const outputPath = 'videos/tts_mix.wav'; - +const text2 = + '明朝开国皇帝朱元璋曾盛赞《道德经》为"万物之根",认为这部道家经典蕴含着治国安邦的至理。这位出身寒微的帝王在建立大明王朝后,深刻体会到老子"无为而治"的智慧,将其奉为治国圭臬。朱元璋不仅亲自批注《道德经》,更命翰林学士编修《御注道德经》,将其中"治大国若烹小鲜"等思想运用于轻徭薄赋的惠民政策中'; +// 按15个字分割文本为数组 +const text2Arr = []; +for (let i = 0; i < text2.length; i += 2) { + text2Arr.push(text2.slice(i, i + 2)); +} +const sleep = async (ms = 1000) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, ms); + }); +}; +const mockSendText = (ttsMax: TtsMix) => { + return new Promise(async (resolve, reject) => { + for (let i = 0; i < text2Arr.length; i++) { + const text = text2Arr[i]; + console.log('开始', i, text); + ttsMax.emitter.emit('text', text); + await sleep(10); + // console.log('完成', i, text); + } + ttsMax.emitter.emit('textEnd'); + }); +}; // tsx src/tts/provider/volcengine/test/mix.ts const main = async () => { - await runDemo(appId, token, speaker, text, outputPath); - console.log('完成'); + try { + console.log('开始', appId, token); + // await runDemo(appId, token, speaker, text, outputPath); + const ttsMax = new TtsMix(appId, token); + setTimeout(() => { + mockSendText(ttsMax).then(() => { + console.log('完成'); + }); + }, 10000); + // await ttsMax.getVoiceDemo(speaker, text, outputPath); + + await ttsMax.getVoiceDemo(speaker, '', outputPath, false); + console.log('完成'); + } catch (err) { + console.log(err); + } }; main(); diff --git a/src/tts/provider/volcengine/tts-mix.ts b/src/tts/provider/volcengine/tts-mix.ts index 870d39f..e719504 100644 --- a/src/tts/provider/volcengine/tts-mix.ts +++ b/src/tts/provider/volcengine/tts-mix.ts @@ -286,7 +286,7 @@ async function startSession(websocket: WebSocket, speaker: string, sessionId: st async function sendText(ws: WebSocket, speaker: string, text: string, sessionId: string): Promise { const header = new Header(PROTOCOL_VERSION, DEFAULT_HEADER_SIZE, FULL_CLIENT_REQUEST, MsgTypeFlagWithEvent, JSON_TYPE).asBytes(); - + console.log('sendText=========', text); const optional = new Optional(EVENT_TaskRequest, sessionId).asBytes(); const payload = getPayloadBytes('1234', EVENT_TaskRequest, text, speaker); return await sendEvent(ws, header, optional, payload); @@ -308,7 +308,20 @@ async function finishConnection(ws: WebSocket): Promise { return await sendEvent(ws, header, optional, payload); } -export async function runDemo(appId: string, token: string, speaker: string, text: string, outputPath: string, emitter?: EventEmitter): Promise { +type RunOptions = { + id?: string; + autoEnd?: boolean; +}; +export async function runDemo( + appId: string, + token: string, + speaker: string, + text: string, + outputPath: string, + emitter?: EventEmitter, + opts: RunOptions = {}, +): Promise { + const autoEnd = opts.autoEnd ?? true; return new Promise((resolve, reject) => { const wsHeader = { 'X-Api-App-Key': appId, @@ -320,6 +333,7 @@ export async function runDemo(appId: string, token: string, speaker: string, tex const url = 'wss://openspeech.bytedance.com/api/v3/tts/bidirection'; const ws = new WebSocket(url, { headers: wsHeader }); const filename = outputPath.split('/').pop() || ''; + // 开始连接 let isBegin = true; const writeFileEmitter = (data: Buffer) => { const value: TTSWriteType = { @@ -348,7 +362,19 @@ export async function runDemo(appId: string, token: string, speaker: string, tex let fileHandle: fs.FileHandle | null = null; let sessionId: string = ''; let isFirstResponse = true; - + let cacheText = ''; + const emitOk = (id: string, code = 200) => { + emitter.emit(id, { code, msg: 'ok' }); + }; + emitter.on('text', async ({ text, id }) => { + await sendText(ws, speaker, text, sessionId); + emitOk(id); + }); + emitter.on('textEnd', async ({ id }) => { + console.log('text end'); + await finishSession(ws, sessionId); + emitOk(id); + }); ws.on('message', async (data) => { try { const res = parserResponse(data as Buffer); @@ -364,8 +390,10 @@ export async function runDemo(appId: string, token: string, speaker: string, tex } if (res.optional.event === EVENT_SessionStarted && isFirstResponse) { isFirstResponse = false; - await sendText(ws, speaker, text, sessionId); - await finishSession(ws, sessionId); + console.log('start session', sessionId, autoEnd); + emitter.emit('isConnect', sessionId); + text && (await sendText(ws, speaker, text, sessionId)); + autoEnd && (await finishSession(ws, sessionId)); fileHandle = await fs.open(outputPath, 'w'); } else if (!isFirstResponse) { if (res.optional.event === EVENT_TTSResponse && res.header.messageType === AUDIO_ONLY_RESPONSE && res.payload && fileHandle) { @@ -413,11 +441,14 @@ export class TtsMix { appId: string; token: string; emitter: EventEmitter; - + isStart = false; constructor(appId: string, token: string) { this.appId = appId; this.token = token; this.emitter = new EventEmitter(); + this.emitter.on('isConnect', () => { + this.isStart = true; + }); } /** * 获取语音 @@ -426,8 +457,32 @@ export class TtsMix { * @param outputPath 输出路径 * @returns */ - async getVoiceDemo(speaker: string, text: string, outputPath: string): Promise { - return runDemo(this.appId, this.token, speaker, text, outputPath, this.emitter); + async getVoiceDemo(speaker: string, text: string, outputPath: string, autoEnd = true): Promise { + const id = nanoid(); + const listenId = 'text' + id; + return runDemo(this.appId, this.token, speaker, text, outputPath, this.emitter, { autoEnd, id: listenId }); + } + async isConnect() { + if (this.isStart) { + return Promise.resolve(true); + } + return new Promise((resolve) => { + this.emitter.once('isConnect', resolve); + }); + } + async sendText(text: string): Promise<{ code?: number; msg?: string }> { + const id = nanoid(); + return new Promise((resolve) => { + this.emitter.once(id, resolve); + this.emitter.emit('text', { text, id }); + }); + } + async sendTextEnd() { + const id = nanoid(); + return new Promise((resolve) => { + this.emitter.once(id, resolve); + this.emitter.emit('textEnd', { id }); + }); } /** * 写入文件的时候同步的流监听