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/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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 }