2025-08-19 14:33:59 +08:00
|
|
|
|
import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService'
|
|
|
|
|
|
import { BookTaskService } from '@/define/db/service/book/bookTaskService'
|
|
|
|
|
|
import { OptionRealmService } from '@/define/db/service/optionService'
|
|
|
|
|
|
import { BookService } from '@/define/db/service/book/bookService'
|
2025-09-12 14:52:28 +08:00
|
|
|
|
import { TaskListService } from '@/define/db/service/book/taskListService'
|
2025-09-23 15:20:47 +08:00
|
|
|
|
import { TaskModal } from '@/define/model/task'
|
|
|
|
|
|
import { Book } from '@/define/model/book/book'
|
|
|
|
|
|
import { getProjectPath } from '../../option/optionCommonService'
|
|
|
|
|
|
import path from 'path'
|
|
|
|
|
|
import { isEmpty } from 'lodash'
|
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
|
import { define } from '@/define/define'
|
|
|
|
|
|
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file'
|
|
|
|
|
|
import { DownloadFile } from '@/define/Tools/common'
|
|
|
|
|
|
import { MappingTaskTypeToVideoModel } from '@/define/enum/video'
|
|
|
|
|
|
import { BookBackTaskType } from '@/define/enum/bookEnum'
|
|
|
|
|
|
import { t } from '@/i18n'
|
2025-09-25 17:21:45 +08:00
|
|
|
|
import { PresetRealmService } from '@/define/db/service/presetService'
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
|
|
|
|
|
export class BookBasicHandle {
|
|
|
|
|
|
bookTaskDetailService!: BookTaskDetailService
|
|
|
|
|
|
bookTaskService!: BookTaskService
|
|
|
|
|
|
optionRealmService!: OptionRealmService
|
|
|
|
|
|
bookService!: BookService
|
2025-09-12 14:52:28 +08:00
|
|
|
|
taskListService!: TaskListService
|
2025-09-25 17:21:45 +08:00
|
|
|
|
presetRealmService!: PresetRealmService
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async InitBookBasicHandle() {
|
|
|
|
|
|
// 如果 bookTaskDetailService 已经初始化,则直接返回
|
|
|
|
|
|
if (!this.bookTaskDetailService) {
|
|
|
|
|
|
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.bookTaskService) {
|
|
|
|
|
|
this.bookTaskService = await BookTaskService.getInstance()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.optionRealmService) {
|
|
|
|
|
|
this.optionRealmService = await OptionRealmService.getInstance()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.bookService) {
|
|
|
|
|
|
this.bookService = await BookService.getInstance()
|
|
|
|
|
|
}
|
2025-09-12 14:52:28 +08:00
|
|
|
|
if (!this.taskListService) {
|
|
|
|
|
|
this.taskListService = await TaskListService.getInstance()
|
|
|
|
|
|
}
|
2025-09-25 17:21:45 +08:00
|
|
|
|
if (!this.presetRealmService) {
|
|
|
|
|
|
this.presetRealmService = await PresetRealmService.getInstance()
|
|
|
|
|
|
}
|
2025-08-19 14:33:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 15:20:47 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 检查所有的服务是否都已初始化
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
|
|
|
|
|
CheckInit() {
|
|
|
|
|
|
if (this.bookTaskDetailService
|
|
|
|
|
|
&& this.bookTaskService
|
|
|
|
|
|
&& this.optionRealmService
|
|
|
|
|
|
&& this.bookService
|
|
|
|
|
|
&& this.taskListService) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 执行事务的方法 */
|
2025-08-19 14:33:59 +08:00
|
|
|
|
async transaction(callback: (realm: any) => void) {
|
2025-09-23 15:20:47 +08:00
|
|
|
|
this.CheckInit() || await this.InitBookBasicHandle()
|
2025-08-19 14:33:59 +08:00
|
|
|
|
this.bookService.transaction(() => {
|
|
|
|
|
|
callback(this.bookService.realm)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-23 15:20:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 下载视频文件并处理路径映射
|
|
|
|
|
|
*
|
|
|
|
|
|
* 此方法负责从远程URL下载视频文件到本地,并处理文件的存储路径、转存服务等。
|
|
|
|
|
|
* 支持多种视频来源的处理,包括MidJourney、可灵等不同平台的视频文件。
|
|
|
|
|
|
* 会自动处理文件转存(除MJ官方CDN和可灵视频外),并更新数据库中的路径信息。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string[]} videoUrls - 需要下载的视频URL列表
|
|
|
|
|
|
* @param {TaskModal.Task} task - 当前执行的任务对象,包含任务类型等信息
|
|
|
|
|
|
* @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象,包含分镜信息
|
|
|
|
|
|
* @param {string} preffix - 文件名前缀,用于区分不同来源的视频文件
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Promise<{outVideoPath: string, subVideoPath: string[]}>} 返回下载结果
|
|
|
|
|
|
* - outVideoPath: 主输出视频的本地路径(第一个视频的副本)
|
|
|
|
|
|
* - subVideoPath: 所有子视频的路径信息数组(JSON字符串格式)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @throws {Error} 当服务未初始化时
|
|
|
|
|
|
* @throws {Error} 当文件下载失败时
|
|
|
|
|
|
* @throws {Error} 当数据库操作失败时
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
* ```typescript
|
|
|
|
|
|
* const result = await this.DownloadVideoUrls(
|
|
|
|
|
|
* ['http://example.com/video1.mp4', 'http://example.com/video2.mp4'],
|
|
|
|
|
|
* task,
|
|
|
|
|
|
* bookTaskDetail,
|
|
|
|
|
|
* 'MJ'
|
|
|
|
|
|
* );
|
|
|
|
|
|
* console.log('主视频路径:', result.outVideoPath);
|
|
|
|
|
|
* console.log('所有视频路径:', result.subVideoPath);
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* 处理流程:
|
|
|
|
|
|
* 1. 初始化服务并获取项目路径
|
|
|
|
|
|
* 2. 遍历每个视频URL进行下载
|
|
|
|
|
|
* 3. 根据任务类型决定是否使用转存服务
|
|
|
|
|
|
* 4. 创建本地存储目录并下载文件
|
|
|
|
|
|
* 5. 将第一个视频复制为主输出视频
|
|
|
|
|
|
* 6. 更新数据库中的路径信息
|
|
|
|
|
|
*
|
|
|
|
|
|
* @note
|
|
|
|
|
|
* - MidJourney官方CDN (cdn.midjourney.com) 的视频不支持转存
|
|
|
|
|
|
* - 可灵视频 (KLING_VIDEO, KLING_VIDEO_EXTEND) 不使用转存服务
|
|
|
|
|
|
* - 转存服务需要全局machineId配置
|
|
|
|
|
|
* - 视频文件按时间戳和索引命名以避免冲突
|
|
|
|
|
|
*/
|
|
|
|
|
|
async DownloadVideoUrls(videoUrls: string[], task: TaskModal.Task, bookTaskDetail: Book.SelectBookTaskDetail, preffix: string, videoIds?: string[]): Promise<{ outVideoPath: string, subVideoPath: string[] }> {
|
|
|
|
|
|
this.CheckInit() || await this.InitBookBasicHandle()
|
|
|
|
|
|
|
|
|
|
|
|
if (videoIds != undefined && videoIds.length != videoUrls.length) {
|
|
|
|
|
|
throw new Error(t("视频ID数量与视频链接数量不匹配"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let bookTask = await this.bookTaskService.GetBookTaskDataById(
|
|
|
|
|
|
bookTaskDetail.bookTaskId as string,
|
|
|
|
|
|
true
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let tempVideoUrls = bookTaskDetail.subVideoPath || []
|
|
|
|
|
|
let newVideoUrls: string[] = []
|
|
|
|
|
|
let outVideoPath: string = ''
|
|
|
|
|
|
|
|
|
|
|
|
const project_path = await getProjectPath()
|
|
|
|
|
|
|
|
|
|
|
|
// 开始下载所有视频
|
|
|
|
|
|
for (let i = 0; i < videoUrls.length; i++) {
|
|
|
|
|
|
const videoUrl = videoUrls[i]
|
|
|
|
|
|
// 处理文件地址和下载
|
|
|
|
|
|
let videoPath = path.join(
|
|
|
|
|
|
bookTask.imageFolder as string,
|
|
|
|
|
|
`video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4`
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let remoteUrl = videoUrl
|
|
|
|
|
|
|
|
|
|
|
|
// 开始处理下载 mj 官方的图片不支持转存
|
|
|
|
|
|
if (global.machineId
|
|
|
|
|
|
&& !isEmpty(global.machineId)
|
|
|
|
|
|
&& !videoUrl.startsWith('https://cdn.midjourney.com')
|
|
|
|
|
|
&& task.type != BookBackTaskType.KLING_VIDEO
|
|
|
|
|
|
&& task.type != BookBackTaskType.KLING_VIDEO_EXTEND
|
2025-09-25 17:21:45 +08:00
|
|
|
|
&& task.type != BookBackTaskType.HAILUO_TEXT_TO_VIDEO
|
|
|
|
|
|
&& task.type != BookBackTaskType.HAILUO_IMAGE_TO_VIDEO
|
|
|
|
|
|
&& task.type != BookBackTaskType.HAILUO_FIRST_LAST_FRAME
|
2025-09-23 15:20:47 +08:00
|
|
|
|
) {
|
|
|
|
|
|
// 转存一下视频文件
|
|
|
|
|
|
// 获取当前url的文件名
|
|
|
|
|
|
let fileName = preffix + "_" + path.basename(videoUrl)
|
|
|
|
|
|
let transferRes = await axios.post(define.lms_url + `/lms/FileUpload/UrlUpload/${global.machineId}`, {
|
|
|
|
|
|
url: videoUrl,
|
|
|
|
|
|
fileName: fileName
|
|
|
|
|
|
})
|
|
|
|
|
|
if (transferRes.status == 200 && transferRes.data.code == 1) {
|
|
|
|
|
|
remoteUrl = transferRes.data.data.url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isEmpty(remoteUrl)) {
|
|
|
|
|
|
remoteUrl = videoUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await CheckFolderExistsOrCreate(path.dirname(videoPath))
|
|
|
|
|
|
await DownloadFile(remoteUrl, videoPath)
|
|
|
|
|
|
|
|
|
|
|
|
// 处理返回数据信息
|
|
|
|
|
|
// 开始修改信息
|
|
|
|
|
|
// 将信息添加到里面
|
|
|
|
|
|
let a = {
|
|
|
|
|
|
localPath: path.relative(project_path, videoPath),
|
|
|
|
|
|
remotePath: remoteUrl,
|
|
|
|
|
|
taskId: bookTaskDetail.videoMessage?.taskId,
|
|
|
|
|
|
videoId: videoIds != undefined && videoIds[i] ? videoIds[i] : "",
|
|
|
|
|
|
index: i,
|
|
|
|
|
|
type: MappingTaskTypeToVideoModel(task.type as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
newVideoUrls.push(JSON.stringify(a))
|
|
|
|
|
|
if (i == 0) {
|
|
|
|
|
|
outVideoPath = path.join(
|
|
|
|
|
|
bookTask.imageFolder as string,
|
|
|
|
|
|
'video',
|
|
|
|
|
|
bookTaskDetail.name + path.extname(videoPath)
|
|
|
|
|
|
)
|
|
|
|
|
|
await CopyFileOrFolder(videoPath, outVideoPath as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始处理数据
|
|
|
|
|
|
// 将原有的视频路径合并到新数组中
|
|
|
|
|
|
newVideoUrls.push(...tempVideoUrls)
|
|
|
|
|
|
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetail.id as string, {
|
|
|
|
|
|
subVideoPath: newVideoUrls,
|
|
|
|
|
|
generateVideoPath: outVideoPath != '' ? outVideoPath : ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
outVideoPath: outVideoPath,
|
|
|
|
|
|
subVideoPath: newVideoUrls
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-19 14:33:59 +08:00
|
|
|
|
}
|