2024-08-03 12:46:12 +08:00
|
|
|
|
import { errorMessage, successMessage } from '../Public/generalTools'
|
|
|
|
|
|
import { SoftwareService } from '../../define/db/service/SoftWare/softwareService'
|
|
|
|
|
|
import path from 'path'
|
2024-08-12 16:26:08 +08:00
|
|
|
|
import fs from 'fs'
|
2024-08-03 12:46:12 +08:00
|
|
|
|
import { define } from '../../define/define'
|
|
|
|
|
|
import { isEmpty } from 'lodash'
|
|
|
|
|
|
import { ValidateJson } from '../../define/Tools/validate'
|
2024-08-12 16:26:08 +08:00
|
|
|
|
import { CheckFileOrDirExist, CheckFolderExistsOrCreate, DeleteFolderAllFile } from '../../define/Tools/file'
|
|
|
|
|
|
import { TTSSelectModel } from '../../define/enum/tts'
|
2024-08-03 12:46:12 +08:00
|
|
|
|
const { EdgeTTS } = require('node-edge-tts')
|
2024-08-12 16:26:08 +08:00
|
|
|
|
const { v4: uuidv4 } = require('uuid')
|
|
|
|
|
|
import { TTSService } from '../../define/db/tts/ttsService'
|
|
|
|
|
|
import { tts } from '../../model/tts'
|
|
|
|
|
|
import { GeneralResponse } from '../../model/generalResponse'
|
2024-08-03 12:46:12 +08:00
|
|
|
|
|
|
|
|
|
|
export class TTS {
|
|
|
|
|
|
softService: SoftwareService
|
2024-08-12 16:26:08 +08:00
|
|
|
|
ttsService: TTSService
|
2024-08-03 12:46:12 +08:00
|
|
|
|
|
|
|
|
|
|
constructor() { }
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化TTS服务
|
|
|
|
|
|
*/
|
|
|
|
|
|
async InitService() {
|
|
|
|
|
|
if (!this.softService) {
|
|
|
|
|
|
this.softService = await SoftwareService.getInstance()
|
|
|
|
|
|
}
|
2024-08-12 16:26:08 +08:00
|
|
|
|
if (!this.ttsService) {
|
|
|
|
|
|
this.ttsService = await TTSService.getInstance()
|
|
|
|
|
|
}
|
2024-08-03 12:46:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-12 16:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
//#region 设置相关
|
|
|
|
|
|
|
2024-08-03 12:46:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化TTS设置
|
|
|
|
|
|
*/
|
2024-08-04 15:00:00 +08:00
|
|
|
|
async InitTTSSetting() {
|
|
|
|
|
|
let defaultData = {
|
2024-08-03 12:46:12 +08:00
|
|
|
|
selectModel: 'edge-tts',
|
|
|
|
|
|
edgeTTS: {
|
|
|
|
|
|
value: 'zh-CN-XiaoxiaoNeural',
|
|
|
|
|
|
gender: 'Female',
|
|
|
|
|
|
label: '晓晓',
|
|
|
|
|
|
lang: 'zh-CN',
|
|
|
|
|
|
saveSubtitles: true,
|
|
|
|
|
|
pitch: 0, // 语调
|
|
|
|
|
|
rate: 10, // 倍速
|
|
|
|
|
|
volumn: 0 // 音量
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-04 15:00:00 +08:00
|
|
|
|
await this.SaveTTSConfig(defaultData)
|
|
|
|
|
|
return defaultData
|
2024-08-03 12:46:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取TTS配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
async GetTTSCOnfig(): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.InitService()
|
|
|
|
|
|
let res = this.softService.GetSoftWarePropertyData('ttsSetting')
|
|
|
|
|
|
let resObj = undefined
|
|
|
|
|
|
if (isEmpty(res)) {
|
|
|
|
|
|
// 没有数据,需要初始化
|
2024-08-04 15:00:00 +08:00
|
|
|
|
resObj = await this.InitTTSSetting()
|
2024-08-03 12:46:12 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
let tryParse = ValidateJson(res)
|
|
|
|
|
|
if (!tryParse) {
|
|
|
|
|
|
throw new Error('解析TTS配置失败,数据格式不正确')
|
|
|
|
|
|
}
|
|
|
|
|
|
resObj = JSON.parse(res)
|
|
|
|
|
|
}
|
|
|
|
|
|
return successMessage(resObj, '获取TTS配置成功', 'TTS_GetTTSCOnfig')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return errorMessage('获取TTS配置失败,错误信息如下:' + error.toString(), 'TTS_GetTTSCOnfig')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 保存TTS配置
|
|
|
|
|
|
* @param {*} data 要保存的数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
async SaveTTSConfig(data: TTSSettingModel.TTSSetting) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.InitService()
|
|
|
|
|
|
let res = this.softService.SaveSoftwarePropertyData('ttsSetting', JSON.stringify(data))
|
|
|
|
|
|
return res
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return errorMessage('保存TTS配置失败,错误信息如下:' + error.toString(), 'TTS_SaveTTSConfig')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-12 16:26:08 +08:00
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
|
|
//#region 合成音频相关
|
2024-08-03 12:46:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成音频
|
|
|
|
|
|
* @param text 要生成的文本
|
|
|
|
|
|
*/
|
|
|
|
|
|
async GenerateAudio(text: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.InitService()
|
|
|
|
|
|
let ttsSetting = await this.GetTTSCOnfig()
|
|
|
|
|
|
if (ttsSetting.code === 0) {
|
|
|
|
|
|
return ttsSetting
|
|
|
|
|
|
}
|
2024-08-12 16:26:08 +08:00
|
|
|
|
let res = undefined
|
2024-08-03 12:46:12 +08:00
|
|
|
|
|
2024-08-12 16:26:08 +08:00
|
|
|
|
// 生成对应的ID
|
|
|
|
|
|
let thisId = uuidv4()
|
|
|
|
|
|
|
|
|
|
|
|
// 讲text写道本地
|
|
|
|
|
|
let textPath = path.join(define.tts_path, `${thisId}/${thisId}.txt`)
|
|
|
|
|
|
await CheckFolderExistsOrCreate(path.dirname(textPath))
|
|
|
|
|
|
await fs.promises.writeFile(textPath, text, 'utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
let audioPath = path.join(define.tts_path, `${thisId}/${thisId}.mp3`)
|
|
|
|
|
|
let selectModel = ttsSetting.data.selectModel as TTSSelectModel
|
|
|
|
|
|
|
|
|
|
|
|
let hasSrt = true
|
2024-08-03 12:46:12 +08:00
|
|
|
|
switch (selectModel) {
|
2024-08-12 16:26:08 +08:00
|
|
|
|
case TTSSelectModel.edgeTTS:
|
|
|
|
|
|
hasSrt = ttsSetting.data.edgeTTS.saveSubtitles
|
|
|
|
|
|
res = await this.GenerateAudioByEdgeTTS(text, ttsSetting.data.edgeTTS, audioPath)
|
2024-08-03 12:46:12 +08:00
|
|
|
|
break
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new Error('未知的TTS模式')
|
|
|
|
|
|
}
|
2024-08-12 16:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
if (res == undefined) {
|
|
|
|
|
|
throw new Error('生成音频失败,未知错误')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 这边返回成功,保存配音历史
|
|
|
|
|
|
this.ttsService.AddTTSHistory({
|
|
|
|
|
|
name: text.substring(0, 10),
|
|
|
|
|
|
ttsPath: res.mp3Path ? path.relative(define.tts_path, res.mp3Path) : null,
|
|
|
|
|
|
hasSrt: hasSrt,
|
|
|
|
|
|
selectModel: selectModel,
|
|
|
|
|
|
srtJsonPath: res.srtJsonPath ? path.relative(define.tts_path, res.srtJsonPath) : null,
|
|
|
|
|
|
id: thisId,
|
|
|
|
|
|
textPath: textPath ? path.relative(define.tts_path, textPath) : null
|
|
|
|
|
|
})
|
2024-08-03 12:46:12 +08:00
|
|
|
|
return res
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2024-08-12 16:26:08 +08:00
|
|
|
|
* 使用EdgeTTS生成音频的方法,生成完成,返回生成的音频路径
|
2024-08-03 12:46:12 +08:00
|
|
|
|
* @param text 要生成的文本
|
|
|
|
|
|
* @param edgeTTS edgetts的设置
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
2024-08-12 16:26:08 +08:00
|
|
|
|
async GenerateAudioByEdgeTTS(text: string, edgeTTS: TTSSettingModel.EdgeTTSSetting, mp3Path: string) {
|
2024-08-03 12:46:12 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const tts = new EdgeTTS({
|
|
|
|
|
|
voice: edgeTTS.value,
|
|
|
|
|
|
lang: edgeTTS.lang,
|
|
|
|
|
|
outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
|
2024-08-12 16:26:08 +08:00
|
|
|
|
saveSubtitles: true,
|
2024-08-03 12:46:12 +08:00
|
|
|
|
pitch: `${edgeTTS.pitch}%`,
|
|
|
|
|
|
rate: `${edgeTTS.rate}%`,
|
|
|
|
|
|
volumn: `${edgeTTS.volumn}%`
|
|
|
|
|
|
})
|
2024-08-12 16:26:08 +08:00
|
|
|
|
let ttsRes = await tts.ttsPromise(text, mp3Path)
|
2024-08-03 12:46:12 +08:00
|
|
|
|
console.log(ttsRes)
|
2024-08-12 16:26:08 +08:00
|
|
|
|
return {
|
|
|
|
|
|
mp3Path: mp3Path,
|
|
|
|
|
|
srtJsonPath: mp3Path + '.json'
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
|
|
//#region 合成字幕
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通过配音历史ID生成字幕
|
|
|
|
|
|
* @param ttsId 配音历史ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
async GenerateSRT(ttsId: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取配音历史
|
|
|
|
|
|
let ttsHistory = this.ttsService.GetTTSHistoryById(ttsId)
|
|
|
|
|
|
let selectModel = ttsHistory.selectModel as TTSSelectModel
|
|
|
|
|
|
let res = undefined
|
|
|
|
|
|
switch (selectModel) {
|
|
|
|
|
|
case TTSSelectModel.edgeTTS:
|
|
|
|
|
|
res = await this.GenerateSRTByEdgeTTS(ttsHistory)
|
|
|
|
|
|
break
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new Error('未知的TTS模式')
|
|
|
|
|
|
}
|
|
|
|
|
|
// 这边重新请求,返回一个完整的
|
|
|
|
|
|
let ttsHistoryData = this.ttsService.GetTTSHistoryById(ttsId)
|
|
|
|
|
|
return successMessage(ttsHistoryData, '生成字幕成功', 'TTS_GenerateSRT')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return errorMessage('生成字幕失败,错误信息如下:' + error.toString(), 'TTS_GenerateSRT')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async GenerateSRTByEdgeTTS(ttsHistory: tts.TTSModel) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 一系列的检查文件是不是存在
|
|
|
|
|
|
if (isEmpty(ttsHistory.textPath)) {
|
|
|
|
|
|
throw new Error('生成字幕失败,文本文件不存在')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isEmpty(ttsHistory.srtJsonPath)) {
|
|
|
|
|
|
throw new Error('生成字幕失败,srtJson文件不存在')
|
|
|
|
|
|
}
|
|
|
|
|
|
let checkFileExist = await CheckFileOrDirExist(ttsHistory.textPath)
|
|
|
|
|
|
if (!checkFileExist) {
|
|
|
|
|
|
throw new Error('生成字幕失败,文本文件不存在')
|
|
|
|
|
|
}
|
|
|
|
|
|
checkFileExist = await CheckFileOrDirExist(ttsHistory.srtJsonPath)
|
|
|
|
|
|
if (!checkFileExist) {
|
|
|
|
|
|
throw new Error('生成字幕失败,srtJson文件不存在')
|
|
|
|
|
|
}
|
|
|
|
|
|
let text = await fs.promises.readFile(ttsHistory.textPath, 'utf-8');
|
|
|
|
|
|
let srtJson = JSON.parse(await fs.promises.readFile(ttsHistory.srtJsonPath, 'utf-8'));
|
|
|
|
|
|
|
|
|
|
|
|
// 根据标点符号和换行符分割文案
|
|
|
|
|
|
// 更新后的正则表达式,匹配所有中文和英文的标点符号以及换行符
|
|
|
|
|
|
const parts = text.match(
|
|
|
|
|
|
/[^,。!?;:“”()《》,.!?;:"()<>$$$$\n]+[,。!?;:“”()《》,.!?;:"()<>$$$$\n]*/g
|
|
|
|
|
|
);
|
|
|
|
|
|
// 初始化 SRT 内容
|
|
|
|
|
|
let srtContent = "";
|
|
|
|
|
|
let index = 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 函数用于去掉文本末尾的标点符号
|
|
|
|
|
|
const removeTrailingPunctuation = (text: string) => {
|
|
|
|
|
|
return text.replace(/[\s,。!?;:“”()《》,.!?;:"()<>$$$$]+$/, "");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 配对时间轴和文案分段
|
|
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
|
|
const part = parts[i];
|
|
|
|
|
|
let startTime =
|
|
|
|
|
|
srtJson[i * 2]?.start || srtJson[srtJson.length - 1].start;
|
|
|
|
|
|
let endTime =
|
|
|
|
|
|
srtJson[i * 2 + 1]?.end || srtJson[srtJson.length - 1].end;
|
|
|
|
|
|
|
|
|
|
|
|
// 去掉文案末尾的标点符号
|
|
|
|
|
|
const cleanedPart = removeTrailingPunctuation(part.trim());
|
|
|
|
|
|
|
|
|
|
|
|
// 将时间格式化为 SRT 格式
|
|
|
|
|
|
const formatTime = (ms) => {
|
|
|
|
|
|
const date = new Date(ms);
|
|
|
|
|
|
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
|
|
|
|
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
|
|
|
|
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
|
|
|
|
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
|
|
|
|
return `${hours}:${minutes}:${seconds},${milliseconds}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 生成 SRT 片段
|
|
|
|
|
|
srtContent += `${index}\n${formatTime(startTime)} --> ${formatTime(
|
|
|
|
|
|
endTime
|
|
|
|
|
|
)}\n${cleanedPart}\n\n`;
|
|
|
|
|
|
index++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(srtContent);
|
|
|
|
|
|
// 将数据写入srt文件
|
|
|
|
|
|
let srtPath = path.join(define.tts_path, `${ttsHistory.id}/${ttsHistory.id}.srt`)
|
|
|
|
|
|
await fs.promises.writeFile(srtPath, srtContent, 'utf-8')
|
|
|
|
|
|
// 更新配音历史
|
|
|
|
|
|
this.ttsService.UpdetateTTSHistory(ttsHistory.id, { srtPath: path.relative(define.tts_path, srtPath) })
|
|
|
|
|
|
// 返回成功
|
|
|
|
|
|
return srtPath;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw error
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
|
|
//#region 配音历史记录相关
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取配音的历史记录
|
|
|
|
|
|
* @param queryCondition 查询的条件
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
|
|
|
|
|
async GetTTSHistoryData(queryCondition: tts.TTSHistoryQueryParams): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.InitService()
|
|
|
|
|
|
let res = this.ttsService.GetTTSHistory(queryCondition);
|
|
|
|
|
|
return successMessage(res, "获取配音历史任务成功", 'TTS_GetTTSHistoryData')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return errorMessage('查询配音历史失败,错误信息:' + error.message, 'TTS_GetTTSHistoryData')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除配音历史
|
|
|
|
|
|
* @param ttsId 要删除的ID
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
|
|
|
|
|
async DeleteTTSHistory(ttsId: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.InitService()
|
|
|
|
|
|
// 先删除数据库中数据,然后删除文件
|
|
|
|
|
|
let ttsHistory = this.ttsService.GetTTSHistoryById(ttsId)
|
|
|
|
|
|
|
|
|
|
|
|
this.ttsService.DeleteTTSHistory(ttsId);
|
|
|
|
|
|
// 删除文件
|
|
|
|
|
|
let ttsDir = path.join(define.tts_path, ttsId)
|
|
|
|
|
|
await DeleteFolderAllFile(ttsDir, true);
|
|
|
|
|
|
return successMessage(ttsId, '删除配音历史成功', 'TTS_DeleteTTSHistory')
|
2024-08-03 12:46:12 +08:00
|
|
|
|
} catch (error) {
|
2024-08-12 16:26:08 +08:00
|
|
|
|
return errorMessage('删除配音历史失败,错误信息:' + error.message, 'TTS_DeleteTTSHistory')
|
2024-08-03 12:46:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-12 16:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
//#endregion
|
2024-08-03 12:46:12 +08:00
|
|
|
|
}
|