import { errorMessage, successMessage } from '../Public/generalTools' import { SoftwareService } from '../../define/db/service/SoftWare/softwareService' import path from 'path' import fs from 'fs' import { define } from '../../define/define' import { isEmpty } from 'lodash' import { ValidateJson } from '../../define/Tools/validate' import { CheckFileOrDirExist, CheckFolderExistsOrCreate, DeleteFolderAllFile } from '../../define/Tools/file' import { TTSSelectModel } from '../../define/enum/tts' const { EdgeTTS } = require('node-edge-tts') const { v4: uuidv4 } = require('uuid') import { TTSService } from '../../define/db/tts/ttsService' import { tts } from '../../model/tts' import { GeneralResponse } from '../../model/generalResponse' export class TTS { softService: SoftwareService ttsService: TTSService constructor() { } /** * 初始化TTS服务 */ async InitService() { if (!this.softService) { this.softService = await SoftwareService.getInstance() } if (!this.ttsService) { this.ttsService = await TTSService.getInstance() } } //#region 设置相关 /** * 初始化TTS设置 */ async InitTTSSetting() { let defaultData = { selectModel: 'edge-tts', edgeTTS: { value: 'zh-CN-XiaoxiaoNeural', gender: 'Female', label: '晓晓', lang: 'zh-CN', saveSubtitles: true, pitch: 0, // 语调 rate: 10, // 倍速 volumn: 0 // 音量 } } await this.SaveTTSConfig(defaultData) return defaultData } /** * 获取TTS配置 */ // @ts-ignore async GetTTSCOnfig(): Promise { try { await this.InitService() let res = this.softService.GetSoftWarePropertyData('ttsSetting') let resObj = undefined if (isEmpty(res)) { // 没有数据,需要初始化 resObj = await this.InitTTSSetting() } 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') } } //#endregion //#region 合成音频相关 /** * 生成音频 * @param text 要生成的文本 */ async GenerateAudio(text: string) { try { await this.InitService() let ttsSetting = await this.GetTTSCOnfig() if (ttsSetting.code === 0) { return ttsSetting } let res = undefined // 生成对应的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 switch (selectModel) { case TTSSelectModel.edgeTTS: hasSrt = ttsSetting.data.edgeTTS.saveSubtitles res = await this.GenerateAudioByEdgeTTS(text, ttsSetting.data.edgeTTS, audioPath) break default: throw new Error('未知的TTS模式') } 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 }) return res } catch (error) { return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio') } } /** * 使用EdgeTTS生成音频的方法,生成完成,返回生成的音频路径 * @param text 要生成的文本 * @param edgeTTS edgetts的设置 * @returns */ async GenerateAudioByEdgeTTS(text: string, edgeTTS: TTSSettingModel.EdgeTTSSetting, mp3Path: string) { try { const tts = new EdgeTTS({ voice: edgeTTS.value, lang: edgeTTS.lang, outputFormat: 'audio-24khz-96kbitrate-mono-mp3', saveSubtitles: true, pitch: `${edgeTTS.pitch}%`, rate: `${edgeTTS.rate}%`, volumn: `${edgeTTS.volumn}%` }) let ttsRes = await tts.ttsPromise(text, mp3Path) console.log(ttsRes) return { mp3Path: mp3Path, srtJsonPath: mp3Path + '.json' }; } catch (error) { throw error } } //#endregion //#region 合成字幕 /** * 通过配音历史ID生成字幕 * @param ttsId 配音历史ID */ async GenerateSRT(ttsId: string): Promise { 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 { 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 { 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') } catch (error) { return errorMessage('删除配音历史失败,错误信息:' + error.message, 'TTS_DeleteTTSHistory') } } //#endregion }