V 3.1.7
1. 移除软件包自带的本地 whisper(需单独安装) 2. 重构版本底层依赖,移除外部依赖 3. 修复 首页 暗黑模式不兼容的问题 4. 修复 SD 合并提示词报错
This commit is contained in:
@@ -8,7 +8,7 @@ import { DEFINE_STRING } from "../../../define/define_string";
|
||||
import path from 'path'
|
||||
import { BasicReverse } from './basicReverse'
|
||||
import { BookTaskDetailService } from '../../../define/db/service/Book/bookTaskDetailService'
|
||||
import { TaskScheduler } from "../task/taskScheduler"
|
||||
import { LogScheduler } from "../task/logScheduler"
|
||||
import { Book } from '../../../model/book'
|
||||
import { LoggerStatus, OtherData, ResponseMessageType } from '../../../define/enum/softwareEnum'
|
||||
import { GeneralResponse } from '../../../model/generalResponse'
|
||||
@@ -28,7 +28,7 @@ import { isEmpty } from 'lodash'
|
||||
*/
|
||||
export class ReverseBook {
|
||||
basicReverse: BasicReverse
|
||||
taskScheduler: TaskScheduler
|
||||
logScheduler: LogScheduler
|
||||
mjOpt: MJOpt = new MJOpt()
|
||||
sdOpt: SDOpt = new SDOpt()
|
||||
tagDefine: TagDefine
|
||||
@@ -42,7 +42,7 @@ export class ReverseBook {
|
||||
this.tagDefine = new TagDefine()
|
||||
this.subtitle = new Subtitle()
|
||||
this.watermark = new Watermark()
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.bookServiceBasic = new BookServiceBasic()
|
||||
this.bookBasic = new BookBasic()
|
||||
}
|
||||
@@ -301,7 +301,7 @@ export class ReverseBook {
|
||||
await this.bookServiceBasic.AddBookBackTask(book.id, task_type, TaskExecuteType.AUTO, bookTaskDetail.bookTaskId, bookTaskDetail.id, DEFINE_STRING.BOOK.REVERSE_PROMPT_RETURN
|
||||
);
|
||||
// 添加返回日志
|
||||
await this.taskScheduler.AddLogToDB(book.id, book.type, `添加 ${task_type} 反推任务成功`, bookTaskDetail.bookTaskId, LoggerStatus.SUCCESS)
|
||||
await this.logScheduler.AddLogToDB(book.id, book.type, `添加 ${task_type} 反推任务成功`, bookTaskDetail.bookTaskId, LoggerStatus.SUCCESS)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -5,7 +5,7 @@ const { exec } = require('child_process')
|
||||
const execAsync = util.promisify(exec)
|
||||
import { define } from '../../../define/define'
|
||||
import { BookService } from '../../../define/db/service/Book/bookService'
|
||||
import { TaskScheduler } from '../task/taskScheduler'
|
||||
import { LogScheduler } from '../task/logScheduler'
|
||||
import { LoggerStatus, LoggerType, OtherData } from '../../../define/enum/softwareEnum'
|
||||
import { errorMessage, successMessage } from '../../Public/generalTools'
|
||||
import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from '../../../define/Tools/file'
|
||||
@@ -35,11 +35,11 @@ export class BasicReverse {
|
||||
bookTaskDetailService: BookTaskDetailService
|
||||
bookBackTaskListService: BookBackTaskListService
|
||||
|
||||
taskScheduler: TaskScheduler
|
||||
logScheduler: LogScheduler
|
||||
ffmpegOptions: FfmpegOptions
|
||||
|
||||
constructor() {
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.ffmpegOptions = new FfmpegOptions()
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export class BasicReverse {
|
||||
if (taskRes.code == 0) {
|
||||
throw new Error(taskRes.message)
|
||||
}
|
||||
this.taskScheduler.AddLogToDB(
|
||||
this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`添加分镜任务成功`,
|
||||
@@ -149,7 +149,7 @@ export class BasicReverse {
|
||||
let sensitivity = 30
|
||||
// 开始之前,推送日志
|
||||
let log_content = `开始进行分镜操作,视频地址:${oldVideoPath},敏感度:${sensitivity},正在调用程序进行处理`
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
log_content,
|
||||
@@ -170,7 +170,7 @@ export class BasicReverse {
|
||||
// 有错误输出
|
||||
if (output.stderr != '') {
|
||||
let error_msg = `分镜成功,但有警告提示:${output.stderr}`
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
error_msg,
|
||||
@@ -187,7 +187,7 @@ export class BasicReverse {
|
||||
BookTaskStatus.STORYBOARD_FAIL,
|
||||
error_message
|
||||
)
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
error_message,
|
||||
@@ -205,7 +205,7 @@ export class BasicReverse {
|
||||
BookTaskStatus.STORYBOARD_FAIL,
|
||||
error_msg
|
||||
)
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
error_msg,
|
||||
@@ -237,7 +237,7 @@ export class BasicReverse {
|
||||
|
||||
this.bookTaskService.UpdateBookTaskStatus(bookTaskId, BookTaskStatus.STORYBOARD_DONE)
|
||||
// 分镜成功,推送日志
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`分镜成功,分镜数据如下:${frameJsonData}`,
|
||||
@@ -310,7 +310,7 @@ export class BasicReverse {
|
||||
|
||||
if (bookTaskDetail.data.length <= 0) {
|
||||
// 传入的分镜数据为空,需要重新获取
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`没有传入分镜数据,开始调用分镜方法`,
|
||||
@@ -339,7 +339,7 @@ export class BasicReverse {
|
||||
|
||||
this.bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.SPLIT)
|
||||
// 有分镜数据,开始处理
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`成功获取分镜数据,开始添加裁剪视频任务`,
|
||||
@@ -363,7 +363,7 @@ export class BasicReverse {
|
||||
}
|
||||
}
|
||||
// 添加日志
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`添加视频裁剪任务成功`,
|
||||
@@ -424,7 +424,7 @@ export class BasicReverse {
|
||||
// 小改小说批次的状态
|
||||
this.bookTaskService.UpdateBookTaskStatus(bookTaskDetail.bookTaskId, BookTaskStatus.SPLIT_DONE)
|
||||
// 结束,分镜完毕,推送日志,返回成功
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookTaskDetail.bookId,
|
||||
book.type,
|
||||
`${bookTaskDetail.name}_视频裁剪完成`,
|
||||
@@ -490,7 +490,7 @@ export class BasicReverse {
|
||||
})
|
||||
}
|
||||
if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) {
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`没有找到对应的小说批次任务数据,请检查bookId是否正确`,
|
||||
@@ -508,7 +508,7 @@ export class BasicReverse {
|
||||
bookTaskId: bookTask.id
|
||||
})
|
||||
if (bookTaskDetails.data.length <= 0) {
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`没有找到对应的小说批次任务数据,请检查bookId是否正确,或者手动执行`,
|
||||
@@ -531,7 +531,7 @@ export class BasicReverse {
|
||||
throw new Error(taskRes.message)
|
||||
}
|
||||
// 添加日志
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`添加音频 ${taskRes.data.name} 分离任务成功`,
|
||||
@@ -588,7 +588,7 @@ export class BasicReverse {
|
||||
})
|
||||
|
||||
// 推送成功消息
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
task.bookId,
|
||||
book.type,
|
||||
`${bookTaskDetail.name}分离音频成功,输出地址:${audioPath}`,
|
||||
@@ -656,7 +656,7 @@ export class BasicReverse {
|
||||
throw new Error(taskRes.message)
|
||||
}
|
||||
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`添加 ${taskRes.data.name} 抽帧任务成功`,
|
||||
@@ -701,7 +701,7 @@ export class BasicReverse {
|
||||
})
|
||||
|
||||
// 推送成功消息
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
book.id,
|
||||
book.type,
|
||||
`${bookTaskDetail.name}抽帧成功,输出地址:${outputFramePath}`,
|
||||
@@ -797,7 +797,7 @@ export class BasicReverse {
|
||||
}
|
||||
}
|
||||
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`添加提取字幕任务成功`,
|
||||
@@ -821,9 +821,9 @@ export class BasicReverse {
|
||||
// 判断是不是用本地的wisper服务
|
||||
if (isWisper) {
|
||||
// 开始调用wisper
|
||||
// 使用异步的方法调用一个python程序,然后写入到指定的json文件中k
|
||||
// 使用异步的方法调用一个python程序,然后写入到指定的json文件中
|
||||
let out_dir = path.dirname(bookTaskDetail.videoPath)
|
||||
|
||||
// #TODO -t 被移除
|
||||
let command = `"${path.join(define.scripts_path, 'Lai.exe')}" "-t" "${out_dir}" "${bookTaskDetail.audioPath
|
||||
}" "${bookTaskDetail.name}"`
|
||||
const output = await execAsync(command, {
|
||||
@@ -833,7 +833,7 @@ export class BasicReverse {
|
||||
// 有错误输出
|
||||
if (output.stderr != '') {
|
||||
let error_msg = `提取字幕成功,但有警告提示:${output.stderr}`
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
book.id,
|
||||
book.type,
|
||||
error_msg,
|
||||
@@ -856,7 +856,7 @@ export class BasicReverse {
|
||||
})
|
||||
|
||||
// 提取字幕成功,推送日志
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
book.id,
|
||||
book.type,
|
||||
`${bookTaskDetail.name} 提取字幕成功`,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FfmpegOptions } from "../ffmpegOptions";
|
||||
import { CheckFileOrDirExist, CopyFileOrFolder, DeleteFolderAllFile } from "../../../define/Tools/file";
|
||||
import fs from 'fs';
|
||||
import { Book } from "../../../model/book";
|
||||
import { TaskScheduler } from '../task/taskScheduler';
|
||||
import { LogScheduler } from '../task/logScheduler';
|
||||
import { BookBasic } from "./BooKBasic";
|
||||
import { LoggerStatus, OtherData } from "../../../define/enum/softwareEnum";
|
||||
import { BasicReverse } from "./basicReverse";
|
||||
@@ -15,18 +15,17 @@ import { BasicReverse } from "./basicReverse";
|
||||
export class BookFrame {
|
||||
bookServiceBasic: BookServiceBasic
|
||||
ffmpegOptions: FfmpegOptions
|
||||
taskScheduler: TaskScheduler
|
||||
logScheduler: LogScheduler
|
||||
basicReverse: BasicReverse
|
||||
bookBasic: BookBasic
|
||||
constructor() {
|
||||
this.bookServiceBasic = new BookServiceBasic();
|
||||
this.ffmpegOptions = new FfmpegOptions();
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.bookBasic = new BookBasic()
|
||||
this.basicReverse = new BasicReverse()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 替换指定分镜的视频当前帧
|
||||
* @param bookTaskDetailId 指定的小说分镜ID
|
||||
@@ -138,7 +137,7 @@ export class BookFrame {
|
||||
})
|
||||
} catch (error) {
|
||||
// 传入的分镜数据为空,需要重新获取
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
`没有传入分镜数据,请先进行分镜计算`,
|
||||
@@ -164,7 +163,7 @@ export class BookFrame {
|
||||
}
|
||||
let res = await this.basicReverse.FrameDataToCutVideoData(item, shortClipData[i]);
|
||||
}
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
bookId,
|
||||
book.type,
|
||||
"所有的视频裁剪完成,开始抽帧",
|
||||
|
||||
@@ -173,6 +173,12 @@ export class BookVideo {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加剪映草稿
|
||||
* @param id
|
||||
* @param operateBookType
|
||||
* @returns
|
||||
*/
|
||||
async AddJianyingDraft(id: string, operateBookType: OperateBookType): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
|
||||
try {
|
||||
await this.InitService();
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MJSetting } from "../../../model/Setting/mjSetting";
|
||||
import { GeneralResponse } from "../../../model/generalResponse"
|
||||
import { LoggerStatus, ResponseMessageType } from "../../../define/enum/softwareEnum";
|
||||
import { ImageStyle } from "../Book/imageStyle";
|
||||
import { TaskScheduler } from "../task/taskScheduler";
|
||||
import { LogScheduler } from "../task/logScheduler";
|
||||
import { Tools } from "../../../main/tools"
|
||||
import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic";
|
||||
import { PresetService } from "../presetService";
|
||||
@@ -28,14 +28,14 @@ export class MJOpt {
|
||||
mjApi: MJApi;
|
||||
mjSetting: MJSetting.MjSetting
|
||||
imageStyle: ImageStyle;
|
||||
taskScheduler: TaskScheduler;
|
||||
logScheduler: LogScheduler;
|
||||
tools: Tools;
|
||||
bookServiceBasic: BookServiceBasic
|
||||
presetService: PresetService
|
||||
softWareServiceBasic: SoftWareServiceBasic
|
||||
constructor() {
|
||||
this.imageStyle = new ImageStyle()
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.tools = new Tools()
|
||||
this.bookServiceBasic = new BookServiceBasic();
|
||||
this.presetService = new PresetService()
|
||||
@@ -528,7 +528,7 @@ export class MJOpt {
|
||||
let taskRes = await this.bookServiceBasic.AddBookBackTask(element.bookId, BookBackTaskType.MJ_IMAGE, TaskExecuteType.AUTO, element.bookTaskId, element.id, responseMessageName
|
||||
);
|
||||
// 添加返回日志
|
||||
await this.taskScheduler.AddLogToDB(element.bookId, BookBackTaskType.MJ_IMAGE, `添加 ${element.name} MJ生成任务成功`, element.bookTaskId, LoggerStatus.SUCCESS)
|
||||
await this.logScheduler.AddLogToDB(element.bookId, BookBackTaskType.MJ_IMAGE, `添加 ${element.name} MJ生成任务成功`, element.bookTaskId, LoggerStatus.SUCCESS)
|
||||
}
|
||||
// 全部完毕
|
||||
return successMessage(null, "MJ添加生成图片任务成功", "MJOpt_AddGenerateImageTask")
|
||||
|
||||
@@ -48,8 +48,10 @@ export class SDOpt {
|
||||
const id = ids[i];
|
||||
let scene = await this.presetService.GetScenePresetDetailById(id)
|
||||
if (scene.code == 1) {
|
||||
// 这边开始拼接
|
||||
result += scene.data.prompt + ', '
|
||||
if (scene.data) {
|
||||
// 这边开始拼接
|
||||
result += scene.data.prompt + ', '
|
||||
}
|
||||
} else {
|
||||
throw new Error(scene.message)
|
||||
}
|
||||
@@ -68,9 +70,11 @@ export class SDOpt {
|
||||
const id = ids[i];
|
||||
let character = await this.presetService.GetCharacterPresetDetailById(id)
|
||||
if (character.code == 1) {
|
||||
result += character.data.prompt + ', '
|
||||
if (character.data.lora && character.data.lora != '无' && character.data.loraWeight) {
|
||||
result += `, <lora:${character.data.lora}:${character.data.lora_weight}>`
|
||||
if (character.data) {
|
||||
result += character.data.prompt + ', '
|
||||
if (character.data.lora && character.data.lora != '无' && character.data.loraWeight) {
|
||||
result += `, <lora:${character.data.lora}:${character.data.lora_weight}>`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(character.message)
|
||||
|
||||
@@ -69,7 +69,7 @@ class BookServiceBasic {
|
||||
GetBookTaskDetailDataById = async (bookTaskDetailId: string) => await this.bookTaskDetailServiceBasic.GetBookTaskDetailDataById(bookTaskDetailId);
|
||||
GetBookTaskDetailData = async (condition: Book.QueryBookTaskDetailCondition, returnEmpty: boolean = false) => await this.bookTaskDetailServiceBasic.GetBookTaskDetailData(condition, returnEmpty);
|
||||
UpdateBookTaskDetail = async (bookTaskDetailId: string, data: Book.SelectBookTaskDetail) => await this.bookTaskDetailServiceBasic.UpdateBookTaskDetail(bookTaskDetailId, data);
|
||||
UpdateBookTaskStatus = async (bookTaskDetailId: string, status: BookTaskStatus) => await this.bookTaskDetailServiceBasic.UpdateBookTaskStatus(bookTaskDetailId, status);
|
||||
UpdateBookTaskStatus = async (bookTaskDetailId: string, status: BookTaskStatus,errorMsg? : string) => await this.bookTaskDetailServiceBasic.UpdateBookTaskStatus(bookTaskDetailId, status,errorMsg);
|
||||
DeleteBookTaskDetailReversePromptById = async (bookTaskDetailId: string, reversePromptId: string) => await this.bookTaskDetailServiceBasic.DeleteBookTaskDetailReversePromptById(bookTaskDetailId);
|
||||
DeleteBoookTaskDetailGenerateImage = async (bookTaskDetailId: string) => await this.bookTaskDetailServiceBasic.DeleteBoookTaskDetailGenerateImage(bookTaskDetailId);
|
||||
UpdateBookTaskDetailReversePrompt = async (bookTaskDetailId: string, reversePromptId: string, data: Book.ReversePrompt) => await this.bookTaskDetailServiceBasic.UpdateBookTaskDetailReversePrompt(bookTaskDetailId, reversePromptId, data);
|
||||
|
||||
@@ -16,7 +16,7 @@ import fs from 'fs'
|
||||
import { GeneralResponse } from '../../../model/generalResponse'
|
||||
import { BookServiceBasic } from '../ServiceBasic/bookServiceBasic'
|
||||
import { LoggerStatus, OtherData, ResponseMessageType } from '../../../define/enum/softwareEnum'
|
||||
import { TaskScheduler } from '../task/taskScheduler'
|
||||
import { LogScheduler } from '../task/logScheduler'
|
||||
import { SubtitleModel } from '../../../model/subtitle'
|
||||
import { BookTaskStatus, OperateBookType } from '../../../define/enum/bookEnum'
|
||||
import axios from 'axios'
|
||||
@@ -24,8 +24,9 @@ import { GptService } from '../GPT/gpt'
|
||||
import FormData from 'form-data'
|
||||
import { RetryWithBackoff } from '../../../define/Tools/common'
|
||||
import { DEFINE_STRING } from '../../../define/define_string'
|
||||
import { DraftTimeLineJson } from '../jianying/jianyingService'
|
||||
const util = require('util')
|
||||
const { exec } = require('child_process')
|
||||
const { spawn, exec } = require('child_process')
|
||||
const execAsync = util.promisify(exec)
|
||||
const fspromises = fs.promises
|
||||
|
||||
@@ -36,12 +37,12 @@ const fspromises = fs.promises
|
||||
export class Subtitle {
|
||||
ffmpegOptions: FfmpegOptions
|
||||
bookServiceBasic: BookServiceBasic
|
||||
taskScheduler: TaskScheduler
|
||||
logScheduler: LogScheduler
|
||||
gptService: GptService
|
||||
|
||||
constructor() {
|
||||
this.bookServiceBasic = new BookServiceBasic()
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.ffmpegOptions = new FfmpegOptions()
|
||||
this.gptService = new GptService()
|
||||
}
|
||||
@@ -74,28 +75,6 @@ export class Subtitle {
|
||||
return frameTimes
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定的的小说相关的所有的数据
|
||||
* @param bookId 小说ID
|
||||
* @param bookTaskId 小说任务ID
|
||||
* @returns
|
||||
*/
|
||||
async GetBookAllData(bookId: string, bookTaskId: string = null): Promise<{ book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetails: Book.SelectBookTaskDetail[] }> {
|
||||
let { book, bookTask } = await this.bookServiceBasic.GetBookAndTask(bookId, bookTaskId ? bookTaskId : 'output_00001')
|
||||
if (isEmpty(book.subtitlePosition)) {
|
||||
throw new Error("请先设置小说的字幕位置")
|
||||
}
|
||||
// 获取所有的分镜数据
|
||||
let bookTaskDetails = await this.bookServiceBasic.GetBookTaskDetailData({
|
||||
bookId: bookId,
|
||||
bookTaskId: bookTask.id
|
||||
})
|
||||
if (bookTaskDetails.length <= 0) {
|
||||
throw new Error("没有找到对应的分镜数据,请先执行对应的操作")
|
||||
}
|
||||
return { book, bookTask, bookTaskDetails }
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的小说获取分案的返回方法
|
||||
* @param content 获取的文案内容
|
||||
@@ -123,7 +102,7 @@ export class Subtitle {
|
||||
}, DEFINE_STRING.BOOK.GET_COPYWRITING_RETURN)
|
||||
|
||||
// 添加日志
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
book.id,
|
||||
book.type,
|
||||
`${bookTaskDetail.name} 识别文案成功`,
|
||||
@@ -562,7 +541,7 @@ export class Subtitle {
|
||||
})
|
||||
|
||||
// 推送成功消息
|
||||
await this.taskScheduler.AddLogToDB(
|
||||
await this.logScheduler.AddLogToDB(
|
||||
book.id,
|
||||
book.type,
|
||||
`${bookTaskDetail.name}分离音频成功,输出地址:${audioPath}`,
|
||||
@@ -667,4 +646,73 @@ export class Subtitle {
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region 本地Whisper识别字幕相关操作
|
||||
|
||||
async GetTextByLocalWhisper(frameTimeList: DraftTimeLineJson[], outDir: string, mp3Dir: string, localWhisperPath?: string): Promise<void> {
|
||||
try {
|
||||
let localWhisperPathExePath = localWhisperPath
|
||||
if (isEmpty(localWhisperPathExePath)) {
|
||||
localWhisperPathExePath = path.join(define.scripts_path, 'localWhisper/local_whisper.exe')
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let child = spawn(
|
||||
localWhisperPathExePath,
|
||||
['-ts', outDir, mp3Dir],
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
child.on('error', (error) => {
|
||||
console.log('error=', error)
|
||||
this.logScheduler.ReturnLogger(errorMessage("使用localWhisper识别字幕失败输出,失败信息如下:" + error.message))
|
||||
reject(new Error(error.message))
|
||||
})
|
||||
child.stdout.on('data', (data) => {
|
||||
console.log(data.toString())
|
||||
this.logScheduler.ReturnLogger(successMessage(data.toString(), "使用localWhisper识别字幕输出"))
|
||||
})
|
||||
child.stderr.on('data', (data) => {
|
||||
console.log('stderr=', data.toString())
|
||||
this.logScheduler.ReturnLogger(errorMessage("使用localWhisper识别字幕失败输出,失败信息如下:stderr = " + data.toString()))
|
||||
reject(new Error(data.toString()))
|
||||
})
|
||||
|
||||
child.on('close', async (data) => {
|
||||
console.log('data=', data.toString())
|
||||
this.logScheduler.ReturnLogger(successMessage(data.toString(), "使用localWhisper识别字幕完成"))
|
||||
let textPath = path.join(outDir, '文案.txt')
|
||||
if (!await CheckFileOrDirExist(textPath)) {
|
||||
throw new Error('没有找到识别输出的文案文件')
|
||||
}
|
||||
let text = await fspromises.readFile(textPath, 'utf-8')
|
||||
let textLines = text.split(/\r?\n/)
|
||||
let lastLine = textLines[textLines.length - 1]
|
||||
// 丢掉最后一行
|
||||
textLines = textLines.slice(0, -1)
|
||||
|
||||
if (textLines.length != frameTimeList.length) {
|
||||
throw new Error('分镜和识别文案数量不一致')
|
||||
}
|
||||
// 保存文案
|
||||
for (let i = 0; i < textLines.length; i++) {
|
||||
const element = textLines[i];
|
||||
frameTimeList[i].text = element
|
||||
}
|
||||
// 写出
|
||||
await fspromises.writeFile(path.join(global.config.project_path, '文案.txt'), textLines.join('\n'), 'utf-8')
|
||||
if (data == 0) {
|
||||
this.logScheduler.ReturnLogger(successMessage(null, "使用localWhisper识别字幕完成"))
|
||||
} else {
|
||||
this.logScheduler.ReturnLogger(errorMessage("使用localWhisper识别字幕失败,失败信息请查看日志"))
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
this.logScheduler.ReturnLogger(errorMessage("使用localWhisper识别字幕失败,失败信息如下:" + error.message))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import fs from 'fs'
|
||||
import { CheckFileOrDirExist } from "../../../define/Tools/file";
|
||||
import { BookServiceBasic } from "../ServiceBasic/bookServiceBasic";
|
||||
import { Subtitle } from "./subtitle";
|
||||
import { TaskScheduler } from "../task/taskScheduler";
|
||||
import { LogScheduler } from "../task/logScheduler";
|
||||
import { BookTaskStatus, BookType, OperateBookType } from "../../../define/enum/bookEnum";
|
||||
import { Book } from "../../../model/book";
|
||||
import { TimeStringToMilliseconds } from "../../../define/Tools/time";
|
||||
@@ -20,7 +20,7 @@ export class SubtitleService {
|
||||
softWareServiceBasic: SoftWareServiceBasic
|
||||
bookServiceBasic: BookServiceBasic
|
||||
subtitle: Subtitle
|
||||
taskScheduler: TaskScheduler
|
||||
logScheduler: LogScheduler
|
||||
constructor() {
|
||||
this.softWareServiceBasic = new SoftWareServiceBasic();
|
||||
this.bookServiceBasic = new BookServiceBasic();
|
||||
|
||||
@@ -98,8 +98,6 @@ export class FfmpegOptions {
|
||||
|
||||
/**
|
||||
* FFmpeg裁剪视频,将一个视频将裁剪指定的时间内的片段
|
||||
* @param {*} book 小说对象类
|
||||
* @param {*} bookTask 小说批次任务对象类
|
||||
* @param {*} startTime 开始时间
|
||||
* @param {*} endTime 结束时间
|
||||
* @param {*} videoPath 视频地址
|
||||
@@ -225,7 +223,7 @@ export class FfmpegOptions {
|
||||
// 判断分镜是不是和数据库中的数据匹配的上
|
||||
let res = await new Promise((resolve, reject) => {
|
||||
Ffmpeg(videoPath)
|
||||
.inputOptions([`-ss ${MillisecondsToTimeString(frameTime)}`])
|
||||
.inputOptions([`-ss ${MillisecondsToTimeString(Math.ceil(frameTime))}`])
|
||||
.output(outFramePath)
|
||||
.frames(1)
|
||||
.on('end', async function () {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import path from 'path';
|
||||
import { CheckFileOrDirExist, DeleteFolderAllFile } from '../../../define/Tools/file';
|
||||
import fs from "fs";
|
||||
import { ValidateJson } from '../../../define/Tools/validate';
|
||||
import { FfmpegOptions } from '../ffmpegOptions';
|
||||
|
||||
/**
|
||||
* 存放剪映草稿的时间轴数据
|
||||
*/
|
||||
export type DraftTimeLineJson = {
|
||||
name: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
durationTime: number;
|
||||
middleTime: number;
|
||||
videoPath: string;
|
||||
text: string;
|
||||
framePath: string;
|
||||
subVideoPath?: string;
|
||||
audioPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 剪映的一些服务
|
||||
*/
|
||||
class JianyingService {
|
||||
draftTimeLine: DraftTimeLineJson[];
|
||||
draftJson: any;
|
||||
ffmpegOptions: FfmpegOptions
|
||||
constructor() {
|
||||
this.draftTimeLine = [];
|
||||
this.ffmpegOptions = new FfmpegOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剪映草稿的关键帧和文本
|
||||
* @param draftDir 草稿目录
|
||||
* @param projectDir 项目目录
|
||||
* @param packagePath 包路径
|
||||
*/
|
||||
async GetDraftFrameAndText(draftDir: string, projectDir: string, packagePath: string) {
|
||||
try {
|
||||
// 获取草稿文件路径
|
||||
let draftJsonPath = path.resolve(draftDir, "draft_content.json");
|
||||
if (!await CheckFileOrDirExist(draftJsonPath)) {
|
||||
throw new Error("剪映草稿文件不存在,请先检查");
|
||||
}
|
||||
// 读取草稿文件内容
|
||||
let draftJsonString = await fs.promises.readFile(draftJsonPath, "utf-8");
|
||||
if (!ValidateJson(draftJsonString)) {
|
||||
throw new Error("剪映草稿文件格式错误,请检查");
|
||||
}
|
||||
this.draftJson = JSON.parse(draftJsonString);
|
||||
|
||||
// 检查输出文件夹是否存在
|
||||
let projectTmp = path.resolve(projectDir, "tmp");
|
||||
if (await CheckFileOrDirExist(projectTmp)) {
|
||||
// 删除文件夹
|
||||
await DeleteFolderAllFile(projectTmp);
|
||||
}
|
||||
// 创建输出文件夹
|
||||
let projectInput = path.resolve(projectTmp, "input_crop");
|
||||
console.log("projectInput", projectInput);
|
||||
|
||||
// 获取剪映的轨道,并且判断是否包含一个video轨道和一个text轨道
|
||||
let draftTracks = this.draftJson.tracks;
|
||||
if (!draftTracks) {
|
||||
throw new Error("剪映草稿文件格式错误,没有轨道,请检查");
|
||||
}
|
||||
let hasVideo = draftTracks.some((track: any) => track.type === "video");
|
||||
let hasText = draftTracks.some((track: any) => track.type === "text");
|
||||
|
||||
if (!(this.draftJson.tracks && hasVideo && hasText)) {
|
||||
throw new Error("没有检测到剪映草稿文件中的video和text轨道,请检查");
|
||||
}
|
||||
|
||||
// 获取视频节点
|
||||
let videoNodes = draftTracks.filter((track: any) => track.type === "video")[0].segments;
|
||||
this.GetVideoTime(videoNodes);
|
||||
|
||||
// 获取文本节点
|
||||
let textNodes = draftTracks.filter((track: any) => track.type === "text")[0].segments;
|
||||
this.GetTextTime(textNodes);
|
||||
|
||||
console.log("场景数:", this.draftTimeLine.length);
|
||||
|
||||
// 将数据写入到文件中
|
||||
let txtData = this.draftTimeLine.map((item) => item.text);
|
||||
let txtPath = path.resolve(projectDir, "文案.txt");
|
||||
await fs.promises.writeFile(txtPath, txtData.join("\n"), "utf-8");
|
||||
|
||||
// 开始抽取关键帧
|
||||
await this.GetDraftFrame(projectInput);
|
||||
|
||||
// 将数据写入到json文件中
|
||||
let jsonPath = path.resolve(projectDir, "draftFrameData.json");
|
||||
await fs.promises.writeFile(jsonPath, JSON.stringify(this.draftTimeLine), "utf-8");
|
||||
console.log("GetDraftFrameAndText", jsonPath);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在节点数组中查找指定类型和值的节点
|
||||
* @param nodes 节点数组
|
||||
* @param type 节点类型
|
||||
* @param value 节点值
|
||||
* @returns 找到的节点
|
||||
* @throws 如果没有找到对应的节点则抛出错误
|
||||
*/
|
||||
private FindNode(nodes: any[], type: string, value: any) {
|
||||
let res = nodes.filter((node: any) => node[type] === value);
|
||||
if (res.length === 0) {
|
||||
throw new Error("没有找到对应的节点");
|
||||
}
|
||||
return res[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文本是否在时间轴内
|
||||
* @param draftTimeObject 时间轴对象
|
||||
* @param textStartTime 文本开始时间
|
||||
* @param textEndTile 文本结束时间
|
||||
* @returns 如果文本在时间轴内则返回 true,否则返回 false
|
||||
*/
|
||||
private TextIsInTimeLine(draftTimeObject: DraftTimeLineJson, textStartTime: number, textEndTile: number) {
|
||||
return textStartTime >= draftTimeObject.startTime && textEndTile <= draftTimeObject.endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽取剪映草稿的关键帧
|
||||
* @param projectInput 项目输入目录
|
||||
*/
|
||||
private async GetDraftFrame(projectInput: string): Promise<void> {
|
||||
for (let i = 0; i < this.draftTimeLine.length; i++) {
|
||||
const element = this.draftTimeLine[i];
|
||||
let outImagePath = path.resolve(projectInput, (i + 1).toString().padStart(5, "0") + ".png");
|
||||
|
||||
// 使用 ffmpeg 抽取关键帧
|
||||
let frameRes = await this.ffmpegOptions.FfmpegGetFrame(element.middleTime / 1000, element.videoPath, outImagePath);
|
||||
if (frameRes.code == 0) {
|
||||
throw new Error(frameRes.message);
|
||||
}
|
||||
this.draftTimeLine[i].framePath = outImagePath;
|
||||
console.log("已经抽取第", i + 1, "帧");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本时间
|
||||
* @param textNodes 文本节点数组
|
||||
*/
|
||||
private GetTextTime(textNodes: any[]): void {
|
||||
let tempText = "";
|
||||
let count = 0;
|
||||
for (let i = 0; i < textNodes.length; i++) {
|
||||
const element = textNodes[i];
|
||||
let textStartTime = element.target_timerange.start;
|
||||
let textEndTime = textStartTime + element.target_timerange.duration;
|
||||
let textMaterialId = element.material_id;
|
||||
let textMaterialNode = this.FindNode(this.draftJson.materials.texts, "id", textMaterialId);
|
||||
let textContent = textMaterialNode.content;
|
||||
let textContentJson = JSON.parse(textContent);
|
||||
|
||||
let text = textContentJson.text + "。";
|
||||
|
||||
// 不在视频的时间轴内,丢弃
|
||||
if (count > this.draftTimeLine.length - 1) {
|
||||
break;
|
||||
}
|
||||
if (this.TextIsInTimeLine(this.draftTimeLine[count], textStartTime, textEndTime)) {
|
||||
tempText += text;
|
||||
if (i == textNodes.length - 1) {
|
||||
this.draftTimeLine[count].text = tempText;
|
||||
}
|
||||
} else {
|
||||
this.draftTimeLine[count].text = tempText;
|
||||
tempText = text;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频时间
|
||||
* @param videoNodes 视频节点数组
|
||||
*/
|
||||
private GetVideoTime(videoNodes: any[]): void {
|
||||
for (let i = 0; i < videoNodes.length; i++) {
|
||||
const element = videoNodes[i];
|
||||
let startTime = element.target_timerange.start;
|
||||
let endTime = startTime + element.target_timerange.duration;
|
||||
let durationTime = element.target_timerange.duration;
|
||||
let middleTime = startTime + ((endTime - startTime) / 2);
|
||||
|
||||
let videoId = element.material_id;
|
||||
let materialNode = this.FindNode(this.draftJson.materials.videos, "id", videoId);
|
||||
let videoPath = materialNode.path;
|
||||
this.draftTimeLine.push({
|
||||
name: (i + 1).toString().padStart(5, "0"),
|
||||
startTime,
|
||||
endTime,
|
||||
durationTime,
|
||||
middleTime,
|
||||
videoPath,
|
||||
text: "",
|
||||
framePath: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default JianyingService;
|
||||
@@ -4,7 +4,7 @@ import { LoggerStatus, OtherData } from '../../../define/enum/softwareEnum'
|
||||
import { successMessage, errorMessage } from '../../Public/generalTools'
|
||||
import { GeneralResponse } from '../../../model/generalResponse'
|
||||
|
||||
export class TaskScheduler {
|
||||
export class LogScheduler {
|
||||
constructor() { }
|
||||
/**
|
||||
* 添加日志到数据库,然后返回日志信息到前端,日志记录失败不会报错
|
||||
@@ -20,7 +20,7 @@ export class TaskScheduler {
|
||||
type: string,
|
||||
content: string,
|
||||
bookTaskId: string,
|
||||
status = LoggerStatus.DOING
|
||||
status: LoggerStatus = LoggerStatus.DOING
|
||||
): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
|
||||
try {
|
||||
let log = {
|
||||
@@ -38,7 +38,16 @@ export class TaskScheduler {
|
||||
|
||||
return res
|
||||
} catch (error) {
|
||||
return errorMessage(error.message, 'TaskScheduler_AddLogToDB')
|
||||
return errorMessage(error.message, 'LogScheduler_AddLogToDB')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将日志返回到前端
|
||||
* @param {*} log 日志信息
|
||||
* @returns
|
||||
*/
|
||||
ReturnLogger(log: GeneralResponse.ErrorItem | GeneralResponse.SuccessItem) {
|
||||
global.newWindow[0].win.webContents.send(DEFINE_STRING.SYSTEM.RETURN_LOGGER, log)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
|
||||
import path from 'path';
|
||||
import { CheckFileOrDirExist, DeleteFolderAllFile } from '../../../define/Tools/file';
|
||||
import fs from 'fs';
|
||||
import { LogScheduler } from '../task/logScheduler';
|
||||
import { successMessage } from '../../Public/generalTools';
|
||||
import { define } from '../../../define/define';
|
||||
import util from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { ValidateJson } from '../../../define/Tools/validate';
|
||||
import { DraftTimeLineJson } from '../jianying/jianyingService';
|
||||
const execAsync = util.promisify(exec)
|
||||
import { FfmpegOptions } from '../ffmpegOptions';
|
||||
import { TimeStringToMilliseconds } from '../../../define/Tools/time';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Subtitle } from '../Subtitle/subtitle';
|
||||
|
||||
|
||||
type VideoHandleShortVideoTimeLine = {
|
||||
name: string,
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
videoPath: string,
|
||||
duration: number
|
||||
}
|
||||
|
||||
class VideoHandle {
|
||||
logScheduler: LogScheduler;
|
||||
ffmpegOptions: FfmpegOptions;
|
||||
subtitle: Subtitle;
|
||||
constructor() {
|
||||
this.logScheduler = new LogScheduler()
|
||||
this.ffmpegOptions = new FfmpegOptions();
|
||||
this.subtitle = new Subtitle();
|
||||
}
|
||||
|
||||
|
||||
public async StartStoryboarding(videoPath: string, sensitivity: number) {
|
||||
// 检查抽帧文件是不是存在
|
||||
let framePath = path.resolve(global.config.project_path, "data/frame");
|
||||
if (await CheckFileOrDirExist(framePath)) {
|
||||
await DeleteFolderAllFile(framePath);
|
||||
} else {
|
||||
await fs.promises.mkdir(framePath, { recursive: true })
|
||||
}
|
||||
// 检查输入文件是不是存在
|
||||
let inputPath = path.resolve(global.config.project_path, "tmp/input_crop");
|
||||
if (await CheckFileOrDirExist(inputPath)) {
|
||||
await DeleteFolderAllFile(inputPath);
|
||||
} else {
|
||||
await fs.promises.mkdir(inputPath, { recursive: true })
|
||||
}
|
||||
|
||||
// 检查本事localwhisper是不是存在
|
||||
let localwhisperPath = path.resolve(define.scripts_path, "localWhisper/local_whisper.exe");
|
||||
if (!await CheckFileOrDirExist(localwhisperPath)) {
|
||||
throw new Error('localwhisper 不存在,请查看文档安装localwhisper插件环境');
|
||||
}
|
||||
|
||||
// 判断输出文件是不是存在,存在删除
|
||||
let frameJson = path.resolve(global.config.project_path, "data/frameTimeLine.json");
|
||||
if (await CheckFileOrDirExist(frameJson)) {
|
||||
await fs.promises.unlink(frameJson);
|
||||
}
|
||||
|
||||
// 开始计算分镜
|
||||
let frameTimeList = await this.ComputedFrameTime(videoPath, frameJson, sensitivity);
|
||||
|
||||
// 开始对视频进行切割
|
||||
// 先计算时间点
|
||||
let shortVideo = [] as VideoHandleShortVideoTimeLine[];
|
||||
shortVideo = await this.VideoShortClip(frameTimeList, videoPath, 0.5 * 60 * 1000);
|
||||
|
||||
// 检查长度
|
||||
if (shortVideo.length != frameTimeList.length) {
|
||||
throw new Error('分镜数据和切割视频数据不一致,请检查');
|
||||
}
|
||||
|
||||
// 开始切割视频
|
||||
console.log(shortVideo);
|
||||
let subVideoPath = await this.CutViodeToShortClip(shortVideo, frameTimeList) as string[];
|
||||
|
||||
// 开始抽帧
|
||||
await this.GetFrameFromCutVideo(shortVideo, inputPath, subVideoPath, frameTimeList);
|
||||
|
||||
// 开始分离音频
|
||||
await this.SplitAudio(frameTimeList, framePath);
|
||||
|
||||
// 开始提取字幕
|
||||
await this.subtitle.GetTextByLocalWhisper(frameTimeList, framePath, framePath, localwhisperPath);
|
||||
|
||||
console.log(frameTimeList);
|
||||
await fs.promises.writeFile(frameJson, JSON.stringify(frameTimeList), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 分离音频
|
||||
* @param frameTimeList
|
||||
* @param framePath
|
||||
*/
|
||||
async SplitAudio(frameTimeList: DraftTimeLineJson[], framePath: string) {
|
||||
for (let i = 0; i < frameTimeList.length; i++) {
|
||||
const element = frameTimeList[i];
|
||||
if (isEmpty(element.subVideoPath)) {
|
||||
throw new Error('没有找到待分离的视频数据,请检查');
|
||||
}
|
||||
if (!await CheckFileOrDirExist(element.subVideoPath)) {
|
||||
throw new Error(`视频片段 ${element.subVideoPath} 不存在,请检查`);
|
||||
}
|
||||
let audioPath = path.resolve(framePath, `${element.name}.mp3`);
|
||||
if (await CheckFileOrDirExist(audioPath)) {
|
||||
await fs.promises.unlink(audioPath);
|
||||
}
|
||||
let res = await this.ffmpegOptions.FfmpegExtractAudio(element.subVideoPath, audioPath)
|
||||
if (res.code == 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
if (!await CheckFileOrDirExist(audioPath)) {
|
||||
throw new Error(`分离音频 ${audioPath} 失败,没有找到分离后的音频文件,请检查`);
|
||||
}
|
||||
element.audioPath = audioPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async GetFrameFromCutVideo(shortVideo: VideoHandleShortVideoTimeLine[], inputPath: string, subVideoPath: string[], frameTimeList: DraftTimeLineJson[]) {
|
||||
if (shortVideo.length != subVideoPath.length) {
|
||||
throw new Error('视频片段和分镜数据不一致');
|
||||
}
|
||||
if (shortVideo.length != frameTimeList.length) {
|
||||
throw new Error('视频片段和分镜数据不一致');
|
||||
}
|
||||
|
||||
let imagePath = [] as string[];
|
||||
for (let i = 0; i < shortVideo.length; i++) {
|
||||
const element = shortVideo[i];
|
||||
if (!frameTimeList[i]) {
|
||||
throw new Error('分镜数据和切割视频数据不一致,请检查');
|
||||
}
|
||||
|
||||
let middleTime = element.startTime + ((element.endTime - element.startTime) / 2);
|
||||
let outImagePath = path.resolve(inputPath, `${element.name}.png`);
|
||||
let res = await this.ffmpegOptions.FfmpegGetFrame(middleTime, element.videoPath, outImagePath);
|
||||
if (res.code == 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
imagePath.push(outImagePath);
|
||||
// 检查图片是否存在
|
||||
if (!await CheckFileOrDirExist(outImagePath)) {
|
||||
throw new Error(`抽取的图片 ${outImagePath} 不存在,请检查`);
|
||||
}
|
||||
frameTimeList[i].framePath = outImagePath;
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将长视频按照指定的视频长度,给分割为一个个的小视频片段
|
||||
* @param shortVideo
|
||||
*/
|
||||
async CutViodeToShortClip(shortVideo: VideoHandleShortVideoTimeLine[], frameTimeList: DraftTimeLineJson[]): Promise<string[]> {
|
||||
let subVideoPaths = [] as string[];
|
||||
for (let i = 0; i < shortVideo.length; i++) {
|
||||
const element = shortVideo[i];
|
||||
if (!frameTimeList[i]) {
|
||||
throw new Error('分镜数据和切割视频数据不一致,请检查');
|
||||
}
|
||||
let subVideoPath = path.resolve(global.config.project_path, `data/frame/${element.name}.mp4`);
|
||||
// 开始截取视频
|
||||
let res = await this.ffmpegOptions.FfmpegCutVideo(
|
||||
element.startTime,
|
||||
element.endTime,
|
||||
element.videoPath,
|
||||
subVideoPath
|
||||
)
|
||||
subVideoPaths.push(subVideoPath);
|
||||
|
||||
if (res.code == 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
if (!await CheckFileOrDirExist(subVideoPath)) {
|
||||
throw new Error(`截取视频片段 ${subVideoPath} 不存在,请检查`);
|
||||
}
|
||||
frameTimeList[i].subVideoPath = subVideoPath;
|
||||
}
|
||||
return subVideoPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理视频,将视频切割成小段,减少计算时间
|
||||
* @param frameTimeList 要处理的时间线,是个json数组
|
||||
* @param videoPath 要处理的视频地址
|
||||
* @param duration 视频的持续时间
|
||||
* @returns
|
||||
*/
|
||||
public async VideoShortClip(frameTimeList: any[], videoPath: string, duration: number): Promise<VideoHandleShortVideoTimeLine[]> {
|
||||
let shortVideo = [] as VideoHandleShortVideoTimeLine[];
|
||||
let durationTime = 0; // 小视频片段的持续时间
|
||||
// let duration = 5 * 60 * 1000; // 5分钟
|
||||
let tempCount = 0;
|
||||
let shotVideoPath = path.resolve(global.config.project_path, `data/temp_frame_${tempCount}.mp4`); // 新的视频路径
|
||||
let startTime = 0; // 开始时间
|
||||
let endTime = 0; // 结束时间
|
||||
let lastEndTime = 0; // 上一个结束时间
|
||||
for (let i = 0; i < frameTimeList.length; i++) {
|
||||
const item = frameTimeList[i];
|
||||
let temRes = {
|
||||
name: (i + 1).toString().padStart(5, "0"),
|
||||
startTime: item.startTime - lastEndTime,
|
||||
endTime: item.endTime - lastEndTime,
|
||||
videoPath: shotVideoPath,
|
||||
duration: item.endTime - item.startTime
|
||||
}
|
||||
endTime = item.endTime;
|
||||
durationTime += item.endTime - item.startTime;
|
||||
if (durationTime > duration) { // 判断条件切割视频
|
||||
// 开始切割视频
|
||||
let res = await this.ffmpegOptions.FfmpegCutVideo(
|
||||
startTime,
|
||||
endTime,
|
||||
videoPath,
|
||||
shotVideoPath
|
||||
)
|
||||
if (res.code == 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
lastEndTime = item.endTime;
|
||||
tempCount++;
|
||||
durationTime = 0;
|
||||
startTime = endTime;
|
||||
endTime = 0;
|
||||
shotVideoPath = path.resolve(global.config.project_path, `data/temp_frame_${tempCount}.mp4`);
|
||||
}
|
||||
shortVideo.push(temRes)
|
||||
}
|
||||
// 最后一个也要切割
|
||||
if (durationTime > 0) {
|
||||
let res = await this.ffmpegOptions.FfmpegCutVideo(
|
||||
startTime,
|
||||
endTime,
|
||||
videoPath,
|
||||
shotVideoPath
|
||||
)
|
||||
if (res.code == 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
}
|
||||
// 将数据写出
|
||||
let shortVideoJson = path.resolve(global.config.project_path, "data/shortVideo.json");
|
||||
await fs.promises.writeFile(shortVideoJson, JSON.stringify(shortVideo), 'utf-8');
|
||||
return shortVideo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算视频的帧时间。
|
||||
*
|
||||
* @param {string} videoPath - 视频文件的路径。
|
||||
* @param {number} sensitivity - 分镜的敏感度。
|
||||
* @throws {Error} 如果视频文件不存在或分镜失败,将抛出错误。
|
||||
*
|
||||
* @remarks
|
||||
* 该方法首先检查视频文件是否存在,如果不存在则抛出错误。
|
||||
* 然后检查输出文件是否存在,如果存在则删除。
|
||||
* 接着调用外部程序进行分镜处理,并记录日志。
|
||||
* 如果分镜成功但有警告信息,将记录警告日志。
|
||||
* 最后检查分镜输出文件是否存在并读取数据,如果没有找到输出文件或数据为空,将抛出错误。
|
||||
*/
|
||||
public async ComputedFrameTime(videoPath: string, frameJson: string, sensitivity: number): Promise<DraftTimeLineJson[]> {
|
||||
|
||||
if (!await CheckFileOrDirExist(videoPath)) {
|
||||
throw new Error('视频文件不存在,请检查');
|
||||
}
|
||||
|
||||
this.logScheduler.ReturnLogger(successMessage(null, "前置检查结束,开始进行分镜", "VideoHandle_StartStoryboarding"));
|
||||
|
||||
// 开始调用分镜
|
||||
let command = `"${path.join(
|
||||
define.scripts_path,
|
||||
'Lai.exe'
|
||||
)}" "-ka" "${videoPath}" "${frameJson}" "${sensitivity}"`
|
||||
const output = await execAsync(command, {
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
// 有错误输出
|
||||
if (output.stderr != '') {
|
||||
let error_msg = `分镜成功,但有警告提示:${output.stderr}`
|
||||
this.logScheduler.ReturnLogger(successMessage(null, error_msg, "VideoHandle_StartStoryboarding"));
|
||||
}
|
||||
|
||||
// 分镜成功,处理输出
|
||||
let josnIsExist = await CheckFileOrDirExist(frameJson);
|
||||
if (!josnIsExist) {
|
||||
let error_message = `分镜失败,没有找到对应的分镜输出文件:${frameJson}`
|
||||
this.logScheduler.ReturnLogger(successMessage(null, error_message, "VideoHandle_StartStoryboarding"));
|
||||
throw new Error(error_message);
|
||||
}
|
||||
let frameJsonDataString = await fs.promises.readFile(frameJson, 'utf-8');
|
||||
let res = ValidateJson(frameJsonDataString);
|
||||
if (!res) {
|
||||
throw new Error('分镜数据不是有效的JSON格式,请检查');
|
||||
}
|
||||
let frameJsonData = JSON.parse(frameJsonDataString);
|
||||
if (frameJsonData.length <= 0) {
|
||||
let error_msg = `分镜失败,没有找到对应的分镜数据`
|
||||
this.logScheduler.ReturnLogger(successMessage(null, error_msg, "VideoHandle_StartStoryboarding"));
|
||||
throw new Error(error_msg);
|
||||
}
|
||||
let result = [] as DraftTimeLineJson[];
|
||||
// 这边将分镜的数据进行一个处理
|
||||
for (let i = 0; i < frameJsonData.length; i++) {
|
||||
const element = frameJsonData[i];
|
||||
let st = TimeStringToMilliseconds(element[0]);
|
||||
let et = TimeStringToMilliseconds(element[1]);
|
||||
let tempObject = {
|
||||
name: (i + 1).toString().padStart(5, "0"),
|
||||
startTime: st,
|
||||
endTime: et,
|
||||
middleTime: st + ((et - st) / 2),
|
||||
videoPath: videoPath,
|
||||
framePath: '',
|
||||
text: "",
|
||||
durationTime: et - st
|
||||
} as DraftTimeLineJson;
|
||||
result.push(tempObject);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VideoHandle;
|
||||
@@ -13,7 +13,7 @@ import { define } from '../../define/define'
|
||||
import { LOGGER_DEFINE } from '../../define/logger_define'
|
||||
import axios from 'axios'
|
||||
import { Base64ToFile, GetImageBase64 } from '../../define/Tools/image'
|
||||
import { TaskScheduler } from './task/taskScheduler';
|
||||
import { LogScheduler } from './task/logScheduler';
|
||||
import { LoggerStatus, OtherData, ResponseMessageType } from '../../define/enum/softwareEnum';
|
||||
import { basicApi } from '../../api/apiBasic';
|
||||
import { FfmpegOptions } from './ffmpegOptions';
|
||||
@@ -28,7 +28,7 @@ import { BookTaskService } from '../../define/db/service/Book/bookTaskService';
|
||||
|
||||
export class Watermark {
|
||||
softwareService: SoftwareService
|
||||
taskScheduler: TaskScheduler;
|
||||
logScheduler: LogScheduler;
|
||||
bookService: BookService
|
||||
bookTaskDetailService: BookTaskDetailService
|
||||
bookTaskService: BookTaskService
|
||||
@@ -39,8 +39,8 @@ export class Watermark {
|
||||
if (!this.softwareService) {
|
||||
this.softwareService = await SoftwareService.getInstance()
|
||||
}
|
||||
if (!this.taskScheduler) {
|
||||
this.taskScheduler = new TaskScheduler()
|
||||
if (!this.logScheduler) {
|
||||
this.logScheduler = new LogScheduler()
|
||||
}
|
||||
if (!this.bookService) {
|
||||
this.bookService = await BookService.getInstance()
|
||||
@@ -449,7 +449,7 @@ export class Watermark {
|
||||
}, DEFINE_STRING.BOOK.REMOVE_WATERMARK_RETURN)
|
||||
|
||||
|
||||
this.taskScheduler.AddLogToDB(book.id, book.type, `${element.name} 去除水印完成`, element.bookTaskId, LoggerStatus.SUCCESS)
|
||||
this.logScheduler.AddLogToDB(book.id, book.type, `${element.name} 去除水印完成`, element.bookTaskId, LoggerStatus.SUCCESS)
|
||||
}
|
||||
// 全部完毕
|
||||
if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
|
||||
|
||||
+870
-870
File diff suppressed because it is too large
Load Diff
@@ -73,9 +73,8 @@ const api = {
|
||||
},
|
||||
|
||||
// 分镜语音识别消息
|
||||
StartStoryboarding: async (value) => {
|
||||
let res = await ipcRenderer.invoke(DEFINE_STRING.START_STORY_BOARDING, value)
|
||||
},
|
||||
StartStoryboarding: async (value) =>
|
||||
await ipcRenderer.invoke(DEFINE_STRING.START_STORY_BOARDING, value),
|
||||
|
||||
// 获取设置的初始数据
|
||||
getSettingDafultData: async (callback) =>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
label-placement="left"
|
||||
inline
|
||||
:model="frameValue"
|
||||
:rules="rules"
|
||||
size="medium"
|
||||
>
|
||||
<n-form-item label="分割敏感度" path="sensitivity" style="width: 300px">
|
||||
@@ -68,7 +67,7 @@
|
||||
<n-code style="padding: 0; font-size: small" :code="code" language="js" word-wrap />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { defineComponent, ref, onMounted, toRaw } from 'vue'
|
||||
import {
|
||||
NSelect,
|
||||
@@ -83,161 +82,142 @@ import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NSlider,
|
||||
NInput
|
||||
NInput,
|
||||
useDialog
|
||||
} from 'naive-ui'
|
||||
import { DEFINE_STRING } from '../../../../define/define_string'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
NSelect,
|
||||
NButton,
|
||||
NSpin,
|
||||
NDivider,
|
||||
NCode,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NSpace,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NSlider,
|
||||
NInput
|
||||
},
|
||||
setup() {
|
||||
let options = []
|
||||
let out_dir = ref(null)
|
||||
let selectedValue = ref(null)
|
||||
let show = ref(false)
|
||||
let code = ref('')
|
||||
const message = useMessage()
|
||||
let storyLoading = ref(false)
|
||||
|
||||
let frameValue = ref({
|
||||
sensitivity: 30,
|
||||
video_path: null
|
||||
})
|
||||
let options = []
|
||||
let out_dir = ref(null)
|
||||
let selectedValue = ref(null)
|
||||
let show = ref(false)
|
||||
let code = ref('')
|
||||
const message = useMessage()
|
||||
let storyLoading = ref(false)
|
||||
let dialog = useDialog()
|
||||
|
||||
onMounted(() => {
|
||||
window.api.getDraftFileList((value) => {
|
||||
value.forEach((element) => {
|
||||
let obj = {
|
||||
label: element,
|
||||
value: element
|
||||
}
|
||||
options.push(obj)
|
||||
})
|
||||
})
|
||||
window.api.setEventListen([DEFINE_STRING.GET_FRAME_RETUN], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error(value.message)
|
||||
code.value = code.value + '\n' + value.data
|
||||
return
|
||||
}
|
||||
// 完成
|
||||
if (value.type == 0) {
|
||||
storyLoading.value = false
|
||||
message.success('分镜抽帧完成')
|
||||
}
|
||||
code.value = code.value + '\n' + value.data
|
||||
})
|
||||
})
|
||||
|
||||
async function getFrameFunc(e) {
|
||||
out_dir = window.config.project_path
|
||||
if (selectedValue.value == null || selectedValue.value == undefined) {
|
||||
message.error('请选择剪映草稿和输出草稿')
|
||||
return
|
||||
}
|
||||
show.value = true
|
||||
// 抽帧
|
||||
await window.api.getFrame([selectedValue.value, out_dir], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error('抽帧失败')
|
||||
code.value = value.message
|
||||
return
|
||||
}
|
||||
message.success('抽帧成功')
|
||||
code.value = value.data
|
||||
show.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function selectExportFolder(e) {
|
||||
window.api.selectFolder(null, (value) => {
|
||||
if (value.length <= 0) {
|
||||
message.error('必须选择输出文件夹位置')
|
||||
return
|
||||
}
|
||||
out_dir.value = value[0]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择指定的视频文件
|
||||
*/
|
||||
async function GetVideoFile() {
|
||||
await window.api.SelectFile(['mp4'], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error(value.message)
|
||||
return
|
||||
}
|
||||
frameValue.value.video_path = value.value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始分镜执行分镜任务
|
||||
*/
|
||||
async function StartStoryboarding() {
|
||||
storyLoading.value = true
|
||||
if (frameValue.value.video_path == null) {
|
||||
message.error('选择分镜的视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
toRaw(frameValue.value)
|
||||
.video_path.split('.')
|
||||
[toRaw(frameValue.value).video_path.split('.').length - 1].toUpperCase() != 'MP4'
|
||||
) {
|
||||
message.error('目前只支持MP4格式')
|
||||
return
|
||||
}
|
||||
|
||||
await window.api.StartStoryboarding(toRaw(frameValue.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开环境安装网站
|
||||
*/
|
||||
function OpenTeachDoc() {
|
||||
window.api.OpenUrl(
|
||||
'https://pvwu1oahp5m.feishu.cn/docx/VrBVd2KUDosmNfxat3OceWuInjd?from=from_copylink'
|
||||
)
|
||||
}
|
||||
|
||||
function openExportFolder() {
|
||||
window.system.OpenFolder({
|
||||
folderPath: 'tmp/input_crop',
|
||||
baseProject: true
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
selectedValue,
|
||||
options,
|
||||
out_dir,
|
||||
getFrameFunc,
|
||||
selectExportFolder,
|
||||
show,
|
||||
code,
|
||||
frameValue,
|
||||
GetVideoFile,
|
||||
StartStoryboarding,
|
||||
OpenTeachDoc,
|
||||
storyLoading,
|
||||
openExportFolder
|
||||
}
|
||||
}
|
||||
let frameValue = ref({
|
||||
sensitivity: 30,
|
||||
video_path: null
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.api.getDraftFileList((value) => {
|
||||
value.forEach((element) => {
|
||||
let obj = {
|
||||
label: element,
|
||||
value: element
|
||||
}
|
||||
options.push(obj)
|
||||
})
|
||||
})
|
||||
window.api.setEventListen([DEFINE_STRING.GET_FRAME_RETUN], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error(value.message)
|
||||
code.value = code.value + '\n' + value.data
|
||||
return
|
||||
}
|
||||
// 完成
|
||||
if (value.type == 0) {
|
||||
storyLoading.value = false
|
||||
message.success('分镜抽帧完成')
|
||||
}
|
||||
code.value = code.value + '\n' + value.data
|
||||
})
|
||||
})
|
||||
|
||||
async function getFrameFunc(e) {
|
||||
out_dir = window.config.project_path
|
||||
if (selectedValue.value == null || selectedValue.value == undefined) {
|
||||
message.error('请选择剪映草稿和输出草稿')
|
||||
return
|
||||
}
|
||||
show.value = true
|
||||
// 抽帧
|
||||
await window.api.getFrame([selectedValue.value, out_dir], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error('抽帧失败')
|
||||
code.value = value.message
|
||||
return
|
||||
}
|
||||
message.success('抽帧成功')
|
||||
code.value = value.data
|
||||
show.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function selectExportFolder(e) {
|
||||
window.api.selectFolder(null, (value) => {
|
||||
if (value.length <= 0) {
|
||||
message.error('必须选择输出文件夹位置')
|
||||
return
|
||||
}
|
||||
out_dir.value = value[0]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择指定的视频文件
|
||||
*/
|
||||
async function GetVideoFile() {
|
||||
await window.api.SelectFile(['mp4'], (value) => {
|
||||
if (value.code == 0) {
|
||||
message.error(value.message)
|
||||
return
|
||||
}
|
||||
frameValue.value.video_path = value.value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始分镜执行分镜任务
|
||||
*/
|
||||
async function StartStoryboarding() {
|
||||
storyLoading.value = true
|
||||
if (frameValue.value.video_path == null) {
|
||||
message.error('选择分镜的视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
toRaw(frameValue.value)
|
||||
.video_path.split('.')
|
||||
[toRaw(frameValue.value).video_path.split('.').length - 1].toUpperCase() != 'MP4'
|
||||
) {
|
||||
message.error('目前只支持MP4格式')
|
||||
return
|
||||
}
|
||||
|
||||
let res = await window.api.StartStoryboarding(toRaw(frameValue.value))
|
||||
if (res.code == 0) {
|
||||
dialog.error({
|
||||
title: '抽帧错误输出',
|
||||
content: res.message
|
||||
})
|
||||
return
|
||||
} else {
|
||||
dialog.success({
|
||||
title: '抽帧成功',
|
||||
content: '视频分镜,抽帧,问题提取都已完成'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开环境安装网站
|
||||
*/
|
||||
function OpenTeachDoc() {
|
||||
window.api.OpenUrl(
|
||||
'https://pvwu1oahp5m.feishu.cn/docx/VrBVd2KUDosmNfxat3OceWuInjd?from=from_copylink'
|
||||
)
|
||||
}
|
||||
|
||||
function openExportFolder() {
|
||||
window.system.OpenFolder({
|
||||
folderPath: 'tmp/input_crop',
|
||||
baseProject: true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -47,7 +47,6 @@ export default defineComponent({
|
||||
onMounted(async () => {
|
||||
// 监听返回的日志数据
|
||||
window.api.setEventListen([DEFINE_STRING.SYSTEM.RETURN_LOGGER], (value) => {
|
||||
debuger
|
||||
if (value.code == 0) {
|
||||
message.error('添加日志输出失败,但是不影响使用')
|
||||
logger.value += value.message + '\n'
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<div
|
||||
id="showmessage"
|
||||
style="font-size: 15px; display: flex; justify-content: center; width: 100%; height: 100%"
|
||||
></div>
|
||||
<div style="font-size: 15px; display: flex; justify-content: center; width: 100%; height: 100%">
|
||||
<div id="showmessage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,6 +33,7 @@ async function GetRemoteSystemInformation() {
|
||||
iframe.style.padding = '0'
|
||||
iframe.style.height = '98vh' // Adjust the height as needed
|
||||
showMessageDiv.innerHTML = ''
|
||||
showMessageDiv.style.width = '100%'
|
||||
showMessageDiv.appendChild(iframe)
|
||||
} else {
|
||||
showMessageDiv.innerHTML = remoteHomePage
|
||||
|
||||
Reference in New Issue
Block a user