coze-plugin-utils
Version:
Comprehensive utility library for Coze plugins with multimedia processing, browser automation, cloud storage integration, and AI-powered video/audio generation capabilities
107 lines (89 loc) • 3.48 kB
text/typescript
import { getGlobalConfig, createTempDir } from '../core';
import type { IGenerateVoiceOptions } from '../types/config';
import * as sdk from 'microsoft-cognitiveservices-speech-sdk';
import path from 'node:path';
import fs from 'node:fs/promises';
export async function tts({
voiceName,
text,
withFrontend = false,
speed = 1.0,
pitch = 0,
volumn = 1.0,
}: IGenerateVoiceOptions): Promise<{ audio:string; duration:number; frontend?: any; }> {
const azureConfig = getGlobalConfig('azure');
if (!azureConfig || !azureConfig.speech) {
throw new Error('Azure Speech Service is not configured');
}
const { key, region } = azureConfig.speech;
const speechConfig = sdk.SpeechConfig.fromSubscription(key, region);
const matches = /^\w+-\w+/.exec(voiceName);
const lang = matches ? matches[0] : 'en-US';
speechConfig.speechSynthesisVoiceName = voiceName;
speechConfig.speechRecognitionLanguage = lang;
speechConfig.speechSynthesisOutputFormat = sdk.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3;
const synthesizer = new sdk.SpeechSynthesizer(speechConfig);
function formatProsody(speed: number, volumn: number, pitch: number): string {
const format = (v: number): string => {
let ret = `${Math.round((v - 1) * 100)}%`;
if(!ret.startsWith('-')) ret = `+${ret}`;
return ret;
};
return `speed="${format(speed)}" pitch="${format(pitch)}" volumn="${format(volumn)}"`;
}
let xml = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="${lang}">
<voice name="${voiceName}"><lang xml:lang="${lang}">
<prosody ${formatProsody(speed, volumn, pitch)}>${text}</prosody>
</lang></voice></speak>
`;
if (lang !== 'zh-CN') {
const chineseCharactersReg = /[\u4e00-\u9fa5]+/;
xml = xml.replace(chineseCharactersReg, '<lang xml:lang="zh-CN">“$&”</lang>');
}
let frontend: any;
if (withFrontend) {
frontend = {
words: [],
};
let index = 0;
synthesizer.wordBoundary = (sender, event): void => {
let _text = event.text;
_text = _text.replace(/<[^>]*?>/g, '').trim();
// 忽略可能多余的标点符号
const _index = text.slice(index).indexOf(_text);
if (_index === -1) return;
const textOffset = _index + index;
index = textOffset + _text.length;
frontend.words.push({
audioOffset: event.audioOffset / 10000,
text: event.text,
textOffset,
duration: event.duration / 10000,
boundaryType: event.boundaryType,
});
};
}
const audioData: { data: ArrayBuffer, duration:number } = await new Promise((resolve, reject) => {
synthesizer.speakSsmlAsync(
xml,
(result: any) => {
if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
// console.log('synthesis finished.');
// console.log(result);
resolve({ data: result.audioData, duration: result.privAudioDuration / 1e7 });
} else {
// console.error(result);
reject(new Error('SynthesizingAudioFailed'));
}
},
(_error: unknown) => {
// console.error('Error:', error);
reject(new Error('SynthesizingAudioFailed'));
},
);
});
const tmpDir = await createTempDir();
const audioPath = path.join(tmpDir, 'speech.mp3');
await fs.writeFile(audioPath, Buffer.from(audioData.data));
return { audio: audioPath, duration: audioData.duration, frontend };
}