479 lines
17 KiB
TypeScript
479 lines
17 KiB
TypeScript
import Realm from 'realm'
|
||
import { RealmBaseService } from '../base/realmBase'
|
||
import { Book } from '@/define/model/book/book'
|
||
import { BookTaskModel } from '../../model/bookTask'
|
||
import { getProjectPath } from '@/main/service/option/optionCommonService'
|
||
import { ImageCategory } from '@/define/data/imageData'
|
||
import { CheckFolderExistsOrCreate, CopyFileOrFolder, JoinPath } from '@/define/Tools/file'
|
||
import { BookTaskStatus, CopyImageType } from '@/define/enum/bookEnum'
|
||
import path from 'path'
|
||
import { ImageToVideoModels } from '@/define/enum/video'
|
||
import { cloneDeep, isEmpty } from 'lodash'
|
||
|
||
export class BookTaskService extends RealmBaseService {
|
||
static instance: BookTaskService | null = null
|
||
declare realm: Realm
|
||
|
||
private constructor() {
|
||
super()
|
||
}
|
||
|
||
/**
|
||
* 获取当前实例对象,为空则创建一个新的
|
||
* @returns
|
||
*/
|
||
public static async getInstance() {
|
||
if (BookTaskService.instance === null) {
|
||
BookTaskService.instance = new BookTaskService()
|
||
await super.getInstance()
|
||
}
|
||
await BookTaskService.instance.open()
|
||
return BookTaskService.instance
|
||
}
|
||
|
||
/**
|
||
* 查询满足条件的小说子任务信息
|
||
* @param bookTaskCondition 查询条件 id,bookId,name,no,page, pageSize
|
||
*/
|
||
async GetBookTaskDataByCondition(
|
||
bookTaskCondition: Book.QueryBookTaskCondition
|
||
): Promise<Book.QueryBookTaskConditionResponse> {
|
||
try {
|
||
// 获取所有的小说数据,并进行时间降序排序
|
||
let bookTasks = this.realm.objects<BookTaskModel>('BookTask')
|
||
|
||
// 开始开始筛选
|
||
if (bookTaskCondition.id) {
|
||
// 查询对应的小说ID的数据
|
||
bookTasks = bookTasks.filtered('id = $0', bookTaskCondition.id)
|
||
}
|
||
if (bookTaskCondition.bookId) {
|
||
// 查询对应的小说ID的数据
|
||
bookTasks = bookTasks.filtered('bookId = $0', bookTaskCondition.bookId)
|
||
}
|
||
if (bookTaskCondition.name) {
|
||
// 查询对应的小说ID的数据
|
||
bookTasks = bookTasks.filtered('name = $0', bookTaskCondition.name)
|
||
}
|
||
if (bookTaskCondition.no) {
|
||
// 查询对应的小说ID的数据
|
||
bookTasks = bookTasks.filtered('no = $0', bookTaskCondition.no)
|
||
}
|
||
let bookTask_length = bookTasks.length
|
||
|
||
// bookTasks = bookTasks.sorted('updateTime', true)
|
||
// 判断是不是有page和pageSize,有的话对查询返回的信息做分页
|
||
if (bookTaskCondition.page && bookTaskCondition.pageSize) {
|
||
bookTasks = bookTasks.slice(
|
||
(bookTaskCondition.page - 1) * bookTaskCondition.pageSize,
|
||
bookTaskCondition.page * bookTaskCondition.pageSize
|
||
) as unknown as Realm.Results<BookTaskModel>
|
||
}
|
||
let projectPath: string = await getProjectPath()
|
||
// 做一下数据转换
|
||
// 将realm对象数组转换为普通对象数组
|
||
// 将realm对象数组转换为普通对象数组,并处理异步操作
|
||
let res_bookTasks = Array.from(bookTasks).map((bookTask) => {
|
||
// 直接操作普通对象
|
||
return {
|
||
...bookTask,
|
||
imageStyle: bookTask.imageStyle ? Array.from(bookTask.imageStyle) : [],
|
||
customizeImageStyle: bookTask.customizeImageStyle
|
||
? Array.from(bookTask.customizeImageStyle)
|
||
: [],
|
||
generateVideoPath: JoinPath(projectPath, bookTask.generateVideoPath),
|
||
srtPath: JoinPath(projectPath, bookTask.srtPath),
|
||
audioPath: JoinPath(projectPath, bookTask.audioPath),
|
||
imageFolder: JoinPath(projectPath, bookTask.imageFolder),
|
||
cacheImageList: bookTask.cacheImageList
|
||
? Array.from(bookTask.cacheImageList).map((item) => JoinPath(projectPath, item))
|
||
: [],
|
||
imageCategory: bookTask.imageCategory ? bookTask.imageCategory : ImageCategory.Midjourney // 默认使用MJ出图
|
||
} as Book.SelectBookTask
|
||
})
|
||
|
||
return {
|
||
bookTasks: JSON.parse(JSON.stringify(res_bookTasks)),
|
||
bookTaskLength: bookTask_length
|
||
} as Book.QueryBookTaskConditionResponse
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过ID获取小说批次任务的数据
|
||
* @param bookTaskId
|
||
*/
|
||
async GetBookTaskDataById(bookTaskId: string): Promise<Book.SelectBookTask> {
|
||
try {
|
||
if (bookTaskId == null) {
|
||
throw new Error('小说任务ID不能为空')
|
||
}
|
||
let bookTasks = await this.GetBookTaskDataByCondition({ id: bookTaskId })
|
||
if (bookTasks.bookTasks.length <= 0) {
|
||
throw new Error('未找到对应的小说任务')
|
||
} else {
|
||
return bookTasks.bookTasks[0]
|
||
}
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修改小说批次任务的状态
|
||
* @param bookTaskId 小说批次任务Id
|
||
* @param status 目标状态
|
||
*/
|
||
ModifyBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg?: string): void {
|
||
try {
|
||
this.transaction(() => {
|
||
// 修改对应小说批次任务的状态
|
||
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
|
||
if (bookTask == null) {
|
||
throw new Error('未找到对应的小说任务')
|
||
}
|
||
bookTask.status = status
|
||
bookTask.updateTime = new Date()
|
||
if (errorMsg != null) {
|
||
bookTask.errorMsg = errorMsg
|
||
}
|
||
})
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修改小说批次任务数据
|
||
* @param bookTaskId 小说批次任务ID
|
||
* @param data 要修改的数据
|
||
*/
|
||
async ModifyBookTaskDataById(
|
||
bookTaskId: string,
|
||
data: Book.SelectBookTask
|
||
): Promise<Book.SelectBookTask> {
|
||
try {
|
||
this.transaction(() => {
|
||
let updateData = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
|
||
if (updateData == null) {
|
||
throw new Error('未找到对应的小说任务详细信息')
|
||
}
|
||
// 开始修改
|
||
for (let key in data) {
|
||
updateData[key] = data[key]
|
||
}
|
||
})
|
||
let res = await this.GetBookTaskDataById(bookTaskId)
|
||
return res
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 添加一条数据
|
||
async AddBookTask(bookTask: Book.SelectBookTask): Promise<Book.SelectBookTask> {
|
||
try {
|
||
// 新增
|
||
if (bookTask.bookId == '' || bookTask.bookId == null) {
|
||
throw new Error('小说ID不能为空')
|
||
}
|
||
|
||
bookTask.id = crypto.randomUUID()
|
||
|
||
// 获取当前bookID对应的最大的no
|
||
let maxNo = this.realm.objects('BookTask').filtered('bookId = $0', bookTask.bookId).max('no')
|
||
bookTask.no = maxNo == null ? 1 : Number(maxNo) + 1
|
||
bookTask.name = 'output_0000' + bookTask.no
|
||
bookTask.status = BookTaskStatus.WAIT
|
||
|
||
bookTask.updateTime = new Date()
|
||
bookTask.createTime = new Date()
|
||
|
||
this.realm.write(() => {
|
||
this.realm.create('BookTask', bookTask)
|
||
})
|
||
// 处理完毕,返回结果
|
||
let res = await this.GetBookTaskDataById(bookTask.id)
|
||
return res
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
async CopyNewBookTask(
|
||
sourceBookTask: Book.SelectBookTask,
|
||
sourceBookTaskDetail: Book.SelectBookTaskDetail[],
|
||
copyCount: number,
|
||
copyImageType: CopyImageType
|
||
) {
|
||
try {
|
||
let addBookTask = [] as Book.SelectBookTask[]
|
||
let addBookTaskDetail = [] as Book.SelectBookTaskDetail[]
|
||
let book = this.realm.objectForPrimaryKey('Book', sourceBookTask.bookId as string)
|
||
if (book == null) {
|
||
throw new Error('未找到对应的小说')
|
||
}
|
||
|
||
let projectPath = await getProjectPath()
|
||
// 先处理文件夹的创建,包括小说任务的和小说任务分镜的
|
||
for (let i = 0; i < copyCount; i++) {
|
||
let no = this.GetMaxBookTaskNo(sourceBookTask.bookId as string) + i
|
||
let name = book.name + '_0000' + no
|
||
let imageFolder = path.join(projectPath, `${sourceBookTask.bookId}/tmp/${name}`)
|
||
await CheckFolderExistsOrCreate(imageFolder)
|
||
// 创建对应的文件夹
|
||
let addOneBookTask = {
|
||
id: crypto.randomUUID(),
|
||
bookId: sourceBookTask.bookId,
|
||
no: no,
|
||
name: name,
|
||
generateVideoPath: sourceBookTask.generateVideoPath,
|
||
srtPath: sourceBookTask.srtPath,
|
||
audioPath: sourceBookTask.audioPath,
|
||
draftSrtStyle: sourceBookTask.draftSrtStyle,
|
||
backgroundMusic: sourceBookTask.backgroundMusic,
|
||
friendlyReminder: sourceBookTask.friendlyReminder,
|
||
imageFolder: path.relative(projectPath, imageFolder),
|
||
status: sourceBookTask.status,
|
||
errorMsg: sourceBookTask.errorMsg,
|
||
updateTime: new Date(),
|
||
createTime: new Date(),
|
||
isAuto: sourceBookTask.isAuto,
|
||
imageStyle: sourceBookTask.imageStyle,
|
||
autoAnalyzeCharacter: sourceBookTask.autoAnalyzeCharacter,
|
||
customizeImageStyle: sourceBookTask.customizeImageStyle,
|
||
videoConfig: sourceBookTask.videoConfig,
|
||
prefixPrompt: sourceBookTask.prefixPrompt,
|
||
suffixPrompt: sourceBookTask.suffixPrompt,
|
||
version: sourceBookTask.version,
|
||
imageCategory: sourceBookTask.imageCategory,
|
||
videoCategory: sourceBookTask.videoCategory ?? ImageToVideoModels.MJ_VIDEO,
|
||
openVideoGenerate:
|
||
sourceBookTask.openVideoGenerate == null ? false : sourceBookTask.openVideoGenerate
|
||
} as Book.SelectBookTask
|
||
|
||
addBookTask.push(addOneBookTask)
|
||
|
||
for (let j = 0; j < sourceBookTaskDetail.length; j++) {
|
||
const element = sourceBookTaskDetail[j]
|
||
|
||
let outImagePath: string | undefined
|
||
let subImagePath: string[] | undefined
|
||
|
||
if (element.outImagePath == null || isEmpty(element.outImagePath)) {
|
||
throw new Error('部分分镜的输出图片路径为空')
|
||
}
|
||
|
||
if (copyImageType == CopyImageType.ALL) {
|
||
// 直接全部复制
|
||
outImagePath = element.outImagePath
|
||
subImagePath = element.subImagePath
|
||
} else if (copyImageType == CopyImageType.ONE) {
|
||
if (!element.subImagePath || element.subImagePath.length <= 1) {
|
||
throw new Error('部分分镜的子图片路径数量不足或为空')
|
||
}
|
||
|
||
// 只复制对应的
|
||
let oldImage = element.subImagePath[i + 1]
|
||
outImagePath = path.join(imageFolder, path.basename(element.outImagePath as string))
|
||
await CopyFileOrFolder(oldImage, outImagePath)
|
||
|
||
subImagePath = []
|
||
} else if (copyImageType == CopyImageType.NONE) {
|
||
outImagePath = undefined
|
||
subImagePath = []
|
||
} else {
|
||
throw new Error('无效的图片复制类型')
|
||
}
|
||
if (outImagePath) {
|
||
// 单独处理一下显示的图片
|
||
let imageBaseName = path.basename(element.outImagePath)
|
||
let newImageBaseName = path.join(
|
||
projectPath,
|
||
`${sourceBookTask.bookId}/tmp/${name}/${imageBaseName}`
|
||
)
|
||
await CopyFileOrFolder(outImagePath, newImageBaseName)
|
||
}
|
||
// 处理SD设置
|
||
let sdConifg = undefined
|
||
if (element.sdConifg) {
|
||
let sdConifg = cloneDeep(element.sdConifg)
|
||
if (sdConifg.webuiConfig) {
|
||
let tempSdConfig = cloneDeep(sdConifg.webuiConfig)
|
||
tempSdConfig.id = crypto.randomUUID()
|
||
sdConifg.webuiConfig = tempSdConfig
|
||
}
|
||
}
|
||
|
||
let reverseId = crypto.randomUUID()
|
||
// 处理反推数据
|
||
let reverseMessage = [] as Book.ReversePrompt[]
|
||
if (element.reversePrompt && element.reversePrompt.length > 0) {
|
||
reverseMessage = cloneDeep(element.reversePrompt)
|
||
for (let k = 0; k < reverseMessage.length; k++) {
|
||
reverseMessage[k].id = crypto.randomUUID()
|
||
reverseMessage[k].bookTaskDetailId = reverseId
|
||
}
|
||
}
|
||
|
||
let addOneBookTaskDetail = {} as Book.SelectBookTaskDetail
|
||
addOneBookTaskDetail.id = reverseId
|
||
addOneBookTaskDetail.no = element.no
|
||
addOneBookTaskDetail.name = element.name
|
||
addOneBookTaskDetail.bookId = sourceBookTask.bookId
|
||
addOneBookTaskDetail.bookTaskId = addOneBookTask.id
|
||
addOneBookTaskDetail.videoPath = element.videoPath
|
||
? path.relative(projectPath, element.videoPath)
|
||
: undefined
|
||
addOneBookTaskDetail.word = element.word
|
||
addOneBookTaskDetail.oldImage = element.oldImage
|
||
? path.relative(projectPath, element.oldImage)
|
||
: undefined
|
||
addOneBookTaskDetail.afterGpt = element.afterGpt
|
||
addOneBookTaskDetail.startTime = element.startTime
|
||
addOneBookTaskDetail.endTime = element.endTime
|
||
addOneBookTaskDetail.timeLimit = element.timeLimit
|
||
addOneBookTaskDetail.subValue = (
|
||
element.subValue && element.subValue.length > 0
|
||
? JSON.stringify(element.subValue)
|
||
: undefined
|
||
) as string
|
||
addOneBookTaskDetail.characterTags =
|
||
element.characterTags && element.characterTags.length > 0
|
||
? cloneDeep(element.characterTags)
|
||
: []
|
||
addOneBookTaskDetail.gptPrompt = element.gptPrompt
|
||
addOneBookTaskDetail.outImagePath = outImagePath
|
||
? path.relative(projectPath, outImagePath)
|
||
: undefined
|
||
addOneBookTaskDetail.subImagePath = subImagePath || []
|
||
addOneBookTaskDetail.prompt = element.prompt
|
||
addOneBookTaskDetail.adetailer = element.adetailer
|
||
addOneBookTaskDetail.sdConifg = sdConifg
|
||
addOneBookTaskDetail.createTime = new Date()
|
||
addOneBookTaskDetail.updateTime = new Date()
|
||
addOneBookTaskDetail.audioPath = element.audioPath
|
||
addOneBookTaskDetail.subtitlePosition = element.subtitlePosition
|
||
addOneBookTaskDetail.status = element.status
|
||
addOneBookTaskDetail.reversePrompt = reverseMessage
|
||
addOneBookTaskDetail.imageLock = false // 默认不锁定
|
||
addBookTaskDetail.push(addOneBookTaskDetail)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取最大的小说批次任务的编号
|
||
* @param bookId 小说ID
|
||
*/
|
||
GetMaxBookTaskNo(bookId: string): number {
|
||
let maxNo = this.realm.objects('BookTask').filtered('bookId = $0', bookId).max('no')
|
||
let no = maxNo == null ? 1 : Number(maxNo) + 1
|
||
return no
|
||
}
|
||
|
||
/**
|
||
* 重置小说批次数据,清除小说的详细信息
|
||
* @param bookTaskId 小说批次ID
|
||
*/
|
||
async ResetBookTaskDataById(
|
||
bookTaskId: string,
|
||
resetBase: boolean = true
|
||
): Promise<Book.SelectBookTask> {
|
||
try {
|
||
// 开始重置数据,先重置小说批次数据,在重置其他
|
||
this.transaction(() => {
|
||
this.ResetBookTaskDataInfo(bookTaskId, resetBase)
|
||
})
|
||
let res = await this.GetBookTaskDataById(bookTaskId)
|
||
return res
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除对应的小说批次数据
|
||
* @param bookTaskId 要删除的批次的ID
|
||
*/
|
||
DeleteBookTaskDataById(bookTaskId: string): void {
|
||
try {
|
||
this.transaction(() => {
|
||
// 先调用清除数据的方法
|
||
this.ResetBookTaskDataInfo(bookTaskId)
|
||
// 删除批次数据
|
||
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
|
||
if (bookTask == null) {
|
||
throw new Error('未找到对应的小说任务,无法执行删除操作')
|
||
}
|
||
this.realm.delete(bookTask)
|
||
})
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/** 重置小说批次任务数据 */
|
||
private ResetBookTaskDataInfo(bookTaskId: string, resetBase: boolean = true) {
|
||
try {
|
||
let modifyBookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
|
||
if (modifyBookTask == null) {
|
||
throw new Error('未找到对应的小说批次任务,无法执行重置操作')
|
||
}
|
||
|
||
let book = this.realm.objectForPrimaryKey('Book', modifyBookTask.bookId)
|
||
if (book == null) {
|
||
throw new Error('未找到对应的小说,无法执行重置操作')
|
||
}
|
||
|
||
if (resetBase) {
|
||
// 基础数据的重置
|
||
modifyBookTask.errorMsg = ''
|
||
modifyBookTask.updateTime = new Date()
|
||
modifyBookTask.imageStyle = []
|
||
modifyBookTask.autoAnalyzeCharacter = undefined
|
||
modifyBookTask.customizeImageStyle = []
|
||
modifyBookTask.videoConfig = undefined
|
||
modifyBookTask.prefixPrompt = undefined
|
||
modifyBookTask.suffixPrompt = undefined
|
||
modifyBookTask.subImageFolder = []
|
||
modifyBookTask.srtPath = book.srtPath ?? undefined
|
||
modifyBookTask.audioPath = book.audioPath ?? undefined
|
||
}
|
||
|
||
// 继承小说的srt和配音文件
|
||
modifyBookTask.imageCategory = ImageCategory.Midjourney // 默认使用MJ出图
|
||
modifyBookTask.status = BookTaskStatus.WAIT
|
||
|
||
// 开始删除 分镜信息
|
||
let bookTaskDetails = this.realm
|
||
.objects('BookTaskDetail')
|
||
.filtered('bookTaskId = $0', bookTaskId)
|
||
// 开始删除数据
|
||
for (const bookTaskDetail of bookTaskDetails) {
|
||
// 删除MJMessage
|
||
if (bookTaskDetail.mjMessage) {
|
||
this.realm.delete(bookTaskDetail.mjMessage)
|
||
}
|
||
|
||
if (bookTaskDetail.reversePrompt) {
|
||
;(bookTaskDetail.reversePrompt as any[]).forEach((item) => {
|
||
this.realm.delete(item)
|
||
})
|
||
}
|
||
if (bookTaskDetail.sdConifg) {
|
||
this.realm.delete(bookTaskDetail.sdConifg)
|
||
}
|
||
this.realm.delete(bookTaskDetail)
|
||
}
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
}
|