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' import { EdgeTTS } from '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' import axios from 'axios' import { GetEdgeTTSRole } from '../../define/tts/ttsDefine' import { OptionServices } from "@/main/Service/Options/optionServices" import { OptionKeyName } from '@/define/enum/option' import { OptionModel } from '@/model/option/option' export class TTS { softService: SoftwareService ttsService: TTSService optionServices: OptionServices constructor() { this.optionServices = new OptionServices() } /** * 初始化TTS服务 */ async InitService() { if (!this.softService) { this.softService = await SoftwareService.getInstance() } if (!this.ttsService) { this.ttsService = await TTSService.getInstance() } } //#region 设置相关 /** * 获取TTS配置选项 * * @param {string} key - 用于获取TTS配置的键值 * @returns {Promise} 返回一个包含TTS配置选项的Promise对象 * * @throws {Error} 如果请求失败或返回的数据无效,将抛出错误 * * @example * ```typescript * const options = await GetTTSOptions('someKey'); * console.log(options); * ``` */ async GetTTSOptions(key: string): Promise { try { // 开始请求数据 let res = await axios.get(define.lms + '/lms/LaitoolOptions/GetSimpleOptions/ttsrole'); if (res.data.code != 1) { throw new Error(res.data.message) } let data = res.data.data.filter(item => item.key == "EdgeTTsRoles") if (data.length != 1) { throw new Error("获取TTS角色配置失败") } if (isEmpty(data[0].value) || !ValidateJson(data[0].value)) { return successMessage(GetEdgeTTSRole(), "获取远程配置失败,获取默认配音角色", "TTS_GetTTSOptions"); // 使用默认值 } // 返回远程值 return successMessage(JSON.parse(data[0].value), '获取TTS配置成功', 'TTS_GetTTSOptions') } catch (error) { return errorMessage('获取TTS配置失败,错误信息如下:' + error.toString(), 'TTS_GetTTSOptions') } } //#endregion //#region 合成音频相关 /** * 生成音频 * @param text 要生成的文本 */ async GenerateAudio() { try { await this.InitService() let TTS_GlobalSetting = await this.optionServices.GetOptionByKey(OptionKeyName.TTS_GlobalSetting); if (TTS_GlobalSetting.code == 0) { throw new Error(TTS_GlobalSetting.message); } let TTS_GlobalSettingData = JSON.parse(TTS_GlobalSetting.data.value) as OptionModel.TTS_GlobalSettingModel let text = TTS_GlobalSettingData.ttsText; if (isEmpty(text)) { throw new Error('生成音频失败,文本为空') } 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 = TTS_GlobalSettingData.selectModel; let hasSrt = true switch (selectModel) { case TTSSelectModel.edgeTTS: hasSrt = TTS_GlobalSettingData.edgeTTS.saveSubtitles res = await this.GenerateAudioByEdgeTTS(text, TTS_GlobalSettingData.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 successMessage(res, '生成音频成功', 'TTS_GenerateAudio') } catch (error) { return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio') } } /** * 使用EdgeTTS生成音频的方法,生成完成,返回生成的音频路径 * @param text 要生成的文本 * @param edgeTTS edgetts的设置 * @returns */ async GenerateAudioByEdgeTTS(text: string, edgeTTS: OptionModel.TTS_EdgeTTSModel, mp3Path: string) { try { const edgeTts = new EdgeTTS({ voice: edgeTTS.value, lang: edgeTTS.lang, outputFormat: 'audio-24khz-96kbitrate-mono-mp3', saveSubtitles: true, pitch: `${edgeTTS.pitch}%`, rate: `${edgeTTS.rate}%`, volume: `${edgeTTS.volumn}%`, timeout: edgeTTS.timeOut ?? 100000 }) await edgeTts.ttsPromise(text, mp3Path) 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) => { return text.replace(/[\s,。!?;:“”()《》,.!?;:"()\n]+$/, ""); }; // 函数用于将毫秒格式化为 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}`; }; // 合并文本和时间轴 let combinedPart = ""; let startTime = null; let endTime = null; let j = 0; let count = 0; for (let i = 0; i < parts.length; i++) { let partText = removeTrailingPunctuation(parts[i].trim()); // partText = partText.trim() let tempText = partText.replaceAll(' ', ''); for (; j < srtJson.length; j++) { let timePart = srtJson[j].part; timePart = removeTrailingPunctuation(timePart.trim()); if (tempText.includes(timePart)) { // 如果匹配到,合并文本片段并更新结束时间 combinedPart += timePart; if (startTime === null) { startTime = srtJson[j].start; // 设置开始时间 } endTime = srtJson[j].end; // 更新结束时间 // 如果下一个文本片段开始匹配下一个标点,则跳出内层循环 if (combinedPart == tempText) { j++; // 准备下一次对比从下一个时间片段开始 break; } } else { j++; // 准备下一次对比从下一个时间片段开始 break; } count++; console.log(count); count = 0; } // 生成SRT段落 srtContent += `${index}\n${formatTime(startTime)} --> ${formatTime( endTime )}\n${partText}\n\n`; // 重置变量 combinedPart = ""; startTime = null; endTime = null; 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 }