LaiTool/src/main/Service/Subtitle/subtitleService.ts
lq1405 51deef0c09 V3.2.0
修复聚合推文剪映抽帧时,导入srt没有输入
(聚合推文) SD 反推,MJ反推添加单句洗稿功能
(聚合推文)小说任务列表,添加进入出图文件夹的菜单
(聚合推文)新增一键出图
(聚合推文)新增默认出图方式设置,
5.1只对原创生效,SD反推默认是SD,MJ反推默认是MJ
5.2添加批次的默认出图方式同 a点说明,但是选择了 选择旧批次,新批次的默认出图方式会继承
后台任务,完成后台任务的可视化界面
(聚合推文)小说详情界面,添加小说和批次名称显示
2024-11-02 18:18:55 +08:00

418 lines
17 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 { isEmpty } from "lodash";
import { GetSubtitleType, SubtitleSavePositionType } from "../../../define/enum/waterMarkAndSubtitle"
import { errorMessage, successMessage } from "../../Public/generalTools"
import { SoftWareServiceBasic } from "../ServiceBasic/softwareServiceBasic"
import { ValidateJson } from "../../../define/Tools/validate";
import { GeneralResponse } from "../../../model/generalResponse";
import { SubtitleModel } from "../../../model/subtitle";
import { define } from '../../../define/define'
import path from 'path'
import fs from 'fs'
import { CheckFileOrDirExist } from "../../../define/Tools/file";
import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic";
import { Subtitle } from "./subtitle";
import { LogScheduler } from "../task/logScheduler";
import { BookTaskStatus, BookType, OperateBookType } from "../../../define/enum/bookEnum";
import { Book } from "../../../model/book";
import { TimeStringToMilliseconds } from "../../../define/Tools/time";
export class SubtitleService {
softWareServiceBasic: SoftWareServiceBasic
bookServiceBasic: BookServiceBasic
subtitle: Subtitle
logScheduler: LogScheduler
constructor() {
this.softWareServiceBasic = new SoftWareServiceBasic();
this.bookServiceBasic = new BookServiceBasic();
this.subtitle = new Subtitle();
}
//#region 设置相关的方法
/**
* 初始化字幕设置
*/
async InitSubtitleSetting(): Promise<SubtitleModel.subtitleSettingModel> {
let defauleSetting = {
selectModel: GetSubtitleType.LAI_WHISPER,
laiWhisper: {
url: 'https://api.laitool.cc/',
apiKey: '你的LAI API KEY',
syncGPTAPIKey: false,
prompt: undefined
}
} as SubtitleModel.subtitleSettingModel
await this.softWareServiceBasic.SaveSoftwarePropertyData("subtitleSetting", JSON.stringify(defauleSetting));
return defauleSetting
}
/**
* 获取提起字幕的设置
*/
async GetSubtitleSetting(): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let subtitleSetting = undefined as SubtitleModel.subtitleSettingModel
let subtitleSettingString = await this.softWareServiceBasic.GetSoftWarePropertyData('subtitleSetting');
if (isEmpty(subtitleSettingString)) {
// 初始化
subtitleSetting = await this.InitSubtitleSetting();
} else {
if (ValidateJson(subtitleSettingString)) {
subtitleSetting = JSON.parse(subtitleSettingString)
} else {
throw new Error("提起字幕设置解析失败,请重置后重新配置")
}
}
return successMessage(subtitleSetting, '获取提取字幕设置成功', "SubtitleService_GetSubtitleSetting")
} catch (error) {
return errorMessage("获取字幕设置失败,失败信息如下:" + error.message, "SubtitleService_GetSubtitleSetting")
}
}
/**
* 重置识别字幕设置
*/
async ResetSubtitleSetting(): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let subtitleSetting = await this.InitSubtitleSetting();
return successMessage(subtitleSetting, "重置字幕设置成功", "SubtitleService_ResetSubtitleSetting")
} catch (error) {
return errorMessage("重置字幕设置失败,失败信息如下:" + error.message, "SubtitleService_ResetSubtitleSetting")
}
}
/**
* 保存提取字幕设置,并作相应的一些简单的检查
* @param subtitleSetting 要保存的数据结构体
*/
async SaveSubtitleSetting(subtitleSetting: SubtitleModel.subtitleSettingModel): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
// 判断模式,通过不同的模式判断是不是又必要检查
if (subtitleSetting.selectModel == GetSubtitleType.LOCAL_OCR) {
let localOcrPath = path.join(define.scripts_path, 'LaiOcr/LaiOcr.exe');
let fileIsExists = await CheckFileOrDirExist(localOcrPath);
if (!fileIsExists) {
throw new Error("当前模式未本地OCR但是没有检查到对应的执行文件请查看教程安装对应的拓展");
}
} else if (subtitleSetting.selectModel == GetSubtitleType.LOCAL_WHISPER) {
// let localWhisper = path.join(define.scripts_path,'')
// 这个好像没有什么可以检查的
} else if (subtitleSetting.selectModel == GetSubtitleType.LAI_WHISPER) {
// 判断是不是laitool的不是的话报错
if (!subtitleSetting.laiWhisper.url.includes('laitool')) {
throw new Error('该模式只能试用LAI API的接口请求');
}
if (isEmpty(subtitleSetting.laiWhisper.apiKey)) {
throw new Error("当前模式为LAI API的接口请求请输入LAI API KEY")
}
if (isEmpty(subtitleSetting.laiWhisper.url)) {
throw new Error("当前模式为LAI API的接口请求请输入LAI API URL")
}
} else {
throw new Error("未知的识别字幕模式")
}
// 检查做完,开始保存数据
await this.softWareServiceBasic.SaveSoftwarePropertyData('subtitleSetting', JSON.stringify(subtitleSetting))
return successMessage(null, "保存提取文案设置成功", "SubtitleService_SaveSubtitleSetting");
} catch (error) {
return errorMessage("保存提取文案设置失败,失败信息如下:" + error.message, "SubtitleService_SaveSubtitleSetting")
}
}
//#endregion
//#region 语音转文案或者是字幕识别
/**
* 反推提取文案的入口方法
* @param bookId 小说ID
* @param bookTaskId 小说批次任务ID
* @param operateBookType 操作的小说类型
* @param coverData 是不是要覆盖旧的数据
* @returns
*/
async GetCopywriting(bookId: string, bookTaskId: string, operateBookType: OperateBookType, coverData: boolean): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let subtitleSettingRes = await this.GetSubtitleSetting();
if (subtitleSettingRes.code == 0) {
throw new Error(subtitleSettingRes.message)
}
let subtitleSetting = subtitleSettingRes.data as SubtitleModel.subtitleSettingModel;
let res = undefined as GeneralResponse.ErrorItem | GeneralResponse.SuccessItem
let bookTaskDetails = undefined as Book.SelectBookTaskDetail[]
let tempBookTaskId = bookTaskId
if (operateBookType == OperateBookType.BOOKTASK) {
bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
bookId: bookId,
bookTaskId: bookTaskId
})
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let tempBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskId)
tempBookTaskId = tempBookTaskDetail.bookTaskId
bookTaskDetails = [tempBookTaskDetail]
} else {
throw new Error("未知的操作类型")
}
if (!coverData) { // 不覆盖数据,将已经有的数据过滤掉
bookTaskDetails = bookTaskDetails.filter(item => isEmpty(item.afterGpt) && isEmpty(item.word))
}
if (bookTaskDetails.length <= 0) {
throw new Error("分镜信息不存在 / 已经有文案,无需提取");
}
let { book, bookTask } = await this.bookServiceBasic.GetBookAndTask(bookId, tempBookTaskId)
switch (subtitleSetting.selectModel) {
case GetSubtitleType.LOCAL_OCR:
res = await this.subtitle.GetCopywritingByLocalOcr(book, bookTask, bookTaskDetails)
break;
case GetSubtitleType.LOCAL_WHISPER:
throw new Error("本地Whisper暂时不支持")
break;
case GetSubtitleType.LAI_WHISPER:
res = await this.subtitle.GetCopywritingByLaiWhisper(book, bookTask, bookTaskDetails, subtitleSetting)
break;
default:
throw new Error("未知的识别字幕模式")
}
if (res.code == 0) {
throw new Error(res.message)
}
if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskId)
return successMessage(bookTaskDetail.afterGpt, "获取文案成功", "ReverseBook_GetCopywriting")
} else {
return res
}
} catch (error) {
return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'ReverseBook_GetCopywriting')
}
}
//#endregion
//#region 文案相关的操作
/**
* 导出指定导出指定小说的文案
* @param bookTaskId 小说批次任务ID
* @returns
*/
async ExportCopywriting(bookTaskId: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId)
let book = await this.bookServiceBasic.GetBookDataById(bookTask.bookId)
let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
bookId: book.id,
bookTaskId: bookTaskId
})
let emptyList = []
let content = []
// 检查是不是所有的里面都有文案
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i];
if (isEmpty(element.afterGpt)) {
emptyList.push(element.name)
} else {
content.push(element.afterGpt)
}
}
if (emptyList.length > 0) {
throw new Error(`以下分镜没有文案:${emptyList.join("\n")}`);
}
// 写出文案
let contentStr = content.join("。\n");
contentStr = contentStr + '。'
let wordPath = path.join(book.bookFolderPath, "文案.txt")
await fs.promises.writeFile(wordPath, contentStr, 'utf-8')
return successMessage(wordPath, "导出文案成功", "ReverseBook_ExportCopywriting")
} catch (error) {
return errorMessage("导出文案失败,失败信息如下:" + error.message, 'ReverseBook_ExportCopywriting')
}
}
/**
* 导入修改过后的文案数据
* @param bookId
* @param bookTaskId
* @param txtPath
* @returns
*/
async ImportCopywriting(bookId: string, bookTaskId: string, txtPath: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let word = await fs.promises.readFile(txtPath, 'utf-8');
let wordLines = word.split('\n').map(line => line.trim()).filter(line => line.length > 0);
// 判断小说类型,是反推的话,文案和分镜数据要对应
let book = await this.bookServiceBasic.GetBookDataById(bookId)
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailData({
bookId: bookId,
bookTaskId: bookTaskId
})
if (book.type == BookType.MJ_REVERSE || book.type == BookType.SD_REVERSE) {
if (wordLines.length != bookTaskDetail.length) {
throw new Error("文案行数和分镜数据不对应,请检查")
}
} else if (book.type == BookType.ORIGINAL) {
// 原创这边也要做些判断,待定
throw new Error("原创小说暂时不支持导入文案");
} else {
throw new Error("未知的小说类型,请检查")
}
let result = []
// 这边开始导入文案,这边使用事务
this.bookServiceBasic.transaction((realm) => {
for (let i = 0; i < bookTaskDetail.length; i++) {
const element = bookTaskDetail[i];
let btd = realm.objectForPrimaryKey("BookTaskDetail", element.id);
let ag = wordLines[i] ? wordLines[i] : ''
btd.afterGpt = ag
result.push({
bookTaskDetailId: element.id,
afterGpt: ag
});
}
})
return successMessage(result, "导入文案成功", "ReverseBook_ImportCopywriting")
} catch (error) {
return errorMessage("导入文案失败,失败信息如下:" + error.message, 'ReverseBook_ImportCopywriting')
}
}
/**
* 清除导入的对齐后的文案
* @param bookTaskId 小说批次任务ID
*/
async ClearImportWord(bookTaskId: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
bookTaskId: bookTaskId
})
if (bookTaskDetails.length <= 0) {
throw new Error("没有找到对应小说批次任务的的分镜数据")
}
let book = await this.bookServiceBasic.GetBookDataById(bookTaskDetails[0].bookId)
let originalTimePath = path.join(book.bookFolderPath, `data/${book.id}.mp4.json`);
let originalTime = [] as any[];
if (await CheckFileOrDirExist(originalTimePath)) {
let originalTimeString = await fs.promises.readFile(originalTimePath, 'utf-8');
if (ValidateJson(originalTimeString)) {
originalTime = JSON.parse(originalTimeString)
}
}
// 判断分镜数据和批次数据是不是相同的 好好办办
if (originalTime.length != bookTaskDetails.length) {
originalTime = []
}
// 开始删除,需要做的操作,删除 bookTaskDetail 众的subValue 数据,将时间数据复原
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i];
let updateObj = {
subValue: undefined
} as Book.SelectBookTaskDetail
// 开始重置时间
if (originalTime[i]) {
let startTime = TimeStringToMilliseconds(originalTime[i][0])
let endTime = TimeStringToMilliseconds(originalTime[i][1])
updateObj.startTime = startTime;
updateObj.endTime = endTime;
updateObj.timeLimit = undefined
}
await this.bookServiceBasic.UpdateBookTaskDetail(element.id, updateObj);
}
let returnBookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailData({
bookTaskId: bookTaskId
})
// 成功
return successMessage(returnBookTaskDetail, "重置导入文案对齐数据成功", "ReverseBook_ClearImportWord")
} catch (error) {
return errorMessage("清除导入的文案失败,失败信息如下:" + error.message, 'ReverseBook_ClearImportWord')
}
}
/**
* 保存文案的对齐信息,这边会做一些判断
* @param bookTaskId 小说任务ID
* @param copywritingData 要保存的文案数据
* @param operateBookType 操作的小说类型
* @returns
*/
async SaveCopywriting(bookTaskId: string, copywritingData: SubtitleModel.SaveCopywritingData[], operateBookType: OperateBookType): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
if (operateBookType != OperateBookType.BOOKTASK) {
throw new Error('目前只支持对小说任务的文案保存')
}
let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId)
let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
bookTaskId: bookTaskId
}, true)
// 获取SD设置
let sdConifg = JSON.parse(await fs.promises.readFile(define.sd_setting, 'utf-8'));
let adetailer = false;
if (sdConifg && sdConifg?.webui?.adetailer) {
adetailer = true;
}
if (bookTaskDetails.length == 0) {
// 新增
for (let i = 0; i < copywritingData.length; i++) {
const element = copywritingData[i];
await this.bookServiceBasic.AddBookTaskDetail({
bookTaskId: bookTaskId,
bookId: bookTask.bookId,
startTime: element.start_time,
endTime: element.end_time,
status: BookTaskStatus.WAIT,
word: element.word,
afterGpt: element.after_gpt,
subValue: JSON.stringify(element.subValue),
timeLimit: element.timeLimit,
// 新增修脸跟随
adetailer: adetailer
})
}
} else {
// 修改,这边就要判断是不是数量一致了
if (bookTaskDetails.length != copywritingData.length) {
throw new Error("已有文案,再导入的时候,文案函数数量和分镜数据不一致,请检查")
}
// 开始修改。修改使用事务吧
this.bookServiceBasic.transaction((realm) => {
for (let i = 0; i < copywritingData.length; i++) {
const element = copywritingData[i];
let btd = realm.objectForPrimaryKey("BookTaskDetail", bookTaskDetails[i].id);
if (btd == null) {
throw new Error("未找到对应的分镜数据,请检查")
}
// 开始修改
btd.startTime = element.start_time;
btd.endTime = element.end_time;
btd.word = element.word;
btd.afterGpt = element.after_gpt;
btd.subValue = JSON.stringify(element.subValue);
btd.timeLimit = element.timeLimit
}
})
}
} catch (error) {
return errorMessage("保存文案数据失败,失败信息如下:" + error.toString(), 'SubtitleService_SaveCopywriting')
}
}
//#endregion
}