lq1405 b0eb7795e4 V 3.2.3
1.优化文案处理逻辑,重构界面
2.修复批量导出草稿只能导出一个的bug
3.添加自动 推理人物 场景 方便快速生成标签
4.(聚合推文) 修复删除数据bug
5.新增推理国内转发接口(包括翻译)
6.新增文案导入时导入SRT后可手动校验一遍时间数据,简化简单过程
7.语音服务那边添加字符不生效,格式化不生效
8.优化语音服务(数据结构优化,可设置合成超时时间)
2025-02-17 18:26:47 +08:00

349 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<any>} 返回一个包含TTS配置选项的Promise对象
*
* @throws {Error} 如果请求失败或返回的数据无效,将抛出错误
*
* @example
* ```typescript
* const options = await GetTTSOptions('someKey');
* console.log(options);
* ```
*/
async GetTTSOptions(key: string): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
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<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) => {
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<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')
} catch (error) {
return errorMessage('删除配音历史失败,错误信息:' + error.message, 'TTS_DeleteTTSHistory')
}
}
//#endregion
}