import path from 'path' import fs from 'fs' const util = require('util') 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 './taskScheduler' import { LoggerStatus, LoggerType, OtherData } from '../../define/enum/softwareEnum' import { errorMessage, successMessage } from '../generalTools' import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from '../../define/Tools/file' import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService' import { BookTaskService } from '../../define/db/service/Book/bookTaskService' import { isEmpty, set } from 'lodash' import ffmpeg from 'fluent-ffmpeg' import { SetFfmpegPath } from '../setting/ffmpegSetting' import { TimeStringToMilliseconds, MillisecondsToTimeString } from '../../define/Tools/time' import { BookTaskStatus } from '../../define/enum/bookEnum' SetFfmpegPath() const fspromises = fs.promises /** * 后台执行的任务函数,直接调用改函数即可,抽帧,分镜,提取字幕等 */ export class BasicReverse { constructor() { this.taskScheduler = new TaskScheduler() } //#region ffmpeg的一些操作 /** * FFmpeg裁剪视频,将一个视频将裁剪指定的时间内的片段 * @param {*} book 小说对象类 * @param {*} bookTask 小说批次任务对象类 * @param {*} startTime 开始时间 * @param {*} endTime 结束时间 * @param {*} videoPath 视频地址 * @param {*} outVideoFile 输出地址 * @returns */ async FfmpegCutVideo(book, bookTask, startTime, endTime, videoPath, outVideoFile) { try { // 判断视频地址是不是存在 let videoIsExist = CheckFileOrDirExist(videoPath) if (!videoIsExist) { throw new Error('视频地址对应的文件不存在') } // 判断开始时间和结束时间是不是合法 if (isEmpty(startTime) || isEmpty(endTime)) { throw new Error('开始时间和结束时间不能为空') } // 判断输出文件夹是不是存在 let outputFolder = path.dirname(outVideoFile) await CheckFolderExistsOrCreate(outputFolder) // 将时间转换为字符串 startTimeString = MillisecondsToTimeString(startTime) endTimeString = MillisecondsToTimeString(endTime) // 设置视频编码器 let videoCodec = 'libx264' // 默认编码器 if (global.gpu.type === 'NVIDIA') { videoCodec = 'h264_nvenc' } else if (global.gpu.type === 'AMD') { videoCodec = 'h264_amf' } // 判断分镜是不是和数据库中的数据匹配的上 return new Promise((resolve, reject) => { ffmpeg(videoPath) .setStartTime(startTimeString) .setEndTime(endTimeString) .videoCodec(videoCodec) .addOption('-preset', 'fast') .audioCodec('copy') .output(outVideoFile) .on('end', async function () { let res_msg = `视频裁剪完成,输出地址:${outVideoFile}` // 修改数据库中的输出地址 await this.taskScheduler.AddLogToDB( book.id, book.type, res_msg, OtherData.DEFAULT, LoggerStatus.SUCCESS ) return successMessage(null, res_msg, 'BasicReverse_FfmpegCutVideo') }) .on('error', async function (err) { let res_msg = `视频裁剪失败,错误信息如下:${err.toString()}` await this.taskScheduler.AddLogToDB( book.id, book.type, res_msg, OtherData.DEFAULT, LoggerStatus.FAIL ) return errorMessage(res_msg, 'BasicReverse_FfmpegCutVideo') }) .run() }) // 开始裁剪视频 } catch (error) { return errorMessage( '裁剪视频失败,错误信息如下: ' + error.message, 'BasicReverse_FfmpegCutVideo' ) } } /** * * @param {*} videoPath * @param {*} audioPath */ async FfmpegExtractAudio(videoPath, outAudioPath) { try { // 判断视频地址是不是存在 let videoIsExist = CheckFileOrDirExist(videoPath) if (!videoIsExist) { throw new Error('视频地址对应的文件不存在') } // 开始提取音频 return new Promise((resolve, reject) => { ffmpeg(videoPath) .output(outAudioPath) .audioCodec('libmp3lame') .audioBitrate('128k') .on('end', async function () { let res_msg = `音频提取完成,输出地址:${outAudioPath}` return successMessage(outAudioPath, res_msg, 'BasicReverse_FfmpegExtractAudio') }) .on('error', async function (err) { let res_msg = `音频提取失败,错误信息如下:${err.toString()}` return errorMessage(res_msg, 'BasicReverse_FfmpegExtractAudio') }) }) } catch (error) { return errorMessage( '提取音频失败,错误信息如下: ' + error.message, 'BasicReverse_FfmpegExtractAudio' ) } } //#endregion /** * 分镜(通过传入的bookId) * @param {*} bookId 传入的bookId * @returns */ async GetFrameData(bookId) { try { let _bookService = await BookService.getInstance() let _bookTaskDetailService = await BookTaskDetailService.getInstance() let _bookTaskService = await BookTaskService.getInstance() // 获取对应的小说小说数据,找到对应的小说视频地址 let bookQuery = { bookId: bookId } let bookData = _bookService.GetBookData(bookQuery) if (bookData.code == 0) { return bookData } if (bookData.data.book_length <= 0 || bookData.data.res_book.length <= 0) { throw new Error('没有找到对应的小说数据,请检查bookId是否正确') } // 获取小说对应的批次任务数据,默认初始化为第一个 let bookTaskRes = await _bookTaskService.GetBookTaskData({ bookId: bookId, name: 'output_00001' }) if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) { throw new Error('没有找到对应的小说批次任务数据,请检查bookId是否正确') } // 获取小说的视频地址 let book = bookData.data.res_book[0] let bookTask = bookTaskRes.data.bookTasks[0] _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.STORYBOARD) // 分镜之前,删除之前的老数据 let deleteBookTaskRes = _bookTaskDetailService.DeleteBookTaskDetail({ bookId: bookId, bookTaskId: bookTask.id }) let oldVideoPath = book.oldVideoPath let frameJson = oldVideoPath + '.json' let sensitivity = 30 // 开始之前,推送日志 let log_content = `开始进行分镜操作,视频地址:${oldVideoPath},敏感度:${sensitivity},正在调用程序进行处理` await this.taskScheduler.AddLogToDB( bookId, book.type, log_content, OtherData.DEFAULT, LoggerStatus.DOING ) // 小说进行分镜(python进行,将结果写道一个json里面) // 使用异步的方法调用一个python程序,然后写入到指定的json文件中k let command = `"${path.join( define.scripts_path, 'Lai.exe' )}" "-ka" "${oldVideoPath}" "${frameJson}" "${sensitivity}"` const output = await execAsync(command, { maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8' }) // 有错误输出 if (output.stderr != '') { let error_msg = `分镜失败,错误信息如下:${output.stderr}` _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.STORYBOARD_FAIL, error_msg ) await this.taskScheduler.AddLogToDB( bookId, book.type, error_msg, OtherData.DEFAULT, LoggerStatus.FAIL ) throw new Error(output.stderr) } // 分镜成功,处理输出 let josnIsExist = CheckFileOrDirExist(frameJson) if (!josnIsExist) { let error_message = `分镜失败,没有找到对应的分镜输出文件:${frameJson}` _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.STORYBOARD_FAIL, error_message ) await this.taskScheduler.AddLogToDB( bookId, book.type, error_message, OtherData.DEFAULT, LoggerStatus.FAIL ) throw new Error(error_message) } let frameJsonData = JSON.parse(await fspromises.readFile(frameJson, 'utf-8')) if (frameJsonData.length <= 0) { let error_msg = `分镜失败,没有找到对应的分镜数据` _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.STORYBOARD_FAIL, error_msg ) await this.taskScheduler.AddLogToDB( bookId, book.type, error_msg, OtherData.DEFAULT, LoggerStatus.FAIL ) throw new Error(error_msg) } // 循环写入小说人物详细数据 for (let i = 0; i < frameJsonData.length; i++) { let dataArray = frameJsonData[i] let bookTaskDetail = { bookId: bookId, bookTaskId: bookTask.id } // 将字符串转换为number bookTaskDetail.startTime = TimeStringToMilliseconds(dataArray[0]) bookTaskDetail.endTime = TimeStringToMilliseconds(dataArray[1]) let res = _bookTaskDetailService.AddBookTaskDetail(bookTaskDetail) if (res.code == 0) { throw new Error(res.message) } } _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.STORYBOARD_DONE) // 分镜成功,推送日志 await this.taskScheduler.AddLogToDB( bookId, book.type, `分镜成功,分镜数据如下:${frameJsonData}`, OtherData.DEFAULT, LoggerStatus.SUCCESS ) return successMessage(null, `分镜成功,分镜信息在 ${frameJson}`, 'BasicReverse_GetFrameData') } catch (error) { return errorMessage(error.message, 'BasicReverse_GetFrameData') } } /** * 裁剪视频 * @param {*} bookId 小说ID * @param {*} frameJson 存放分镜数据的json文件地址 */ async CutVideoData(bookId) { try { if (isEmpty(bookId)) { throw new Error('bookId不能为空') } // 判断小说是不是存在 let _bookService = await BookService.getInstance() let _bookTaskService = await BookTaskService.getInstance() let _bookTaskDetailService = await BookTaskDetailService.getInstance() let book = _bookService.GetBookDataById(bookId) if (book == null) { throw new Error('没有找到对应的小说数据') } // 找到对应的小说ID和对应的小说批次任务ID,判断是不是有分镜数据 let bookTaskRes = await _bookTaskService.GetBookTaskData({ bookId: bookId, name: 'output_00001' }) if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) { throw new Error('没有找到对应的小说批次任务数据,请检查bookId是否正确') } let bookTask = bookTaskRes.data.bookTasks[0] let bookTaskDetail = _bookTaskDetailService.GetBookTaskData({ bookId: bookId, bookTaskId: bookTask.id }) if (bookTaskDetail.data.length <= 0) { // 传入的分镜数据为空,需要重新获取 await this.taskScheduler.AddLogToDB( bookId, book.type, `没有传入分镜数据,开始调用分镜方法`, OtherData.DEFAULT, LoggerStatus.DOING ) let frameRes = this.GetFrameData(bookId) if (frameRes.code == 0) { throw new Error((await frameRes).message) } } bookTaskDetail = _bookTaskDetailService.GetBookTaskData({ bookId: bookId, bookTaskId: bookTask.id }) if (bookTaskDetail.data.length <= 0) { _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.SPLIT_FAIL, '重新调用分镜方法还是没有分镜数据,请检查' ) throw new Error('重新调用分镜方法还是没有分镜数据,请检查') } _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.SPLIT) // 有分镜数据,开始处理 await this.taskScheduler.AddLogToDB( bookId, book.type, `成功获取分镜数据,开始裁剪视频`, OtherData.DEFAULT, LoggerStatus.SUCCESS ) for (let i = 0; i < bookTaskDetail.length; i++) { const element = bookTaskDetail[i] let startTime = element.startTime let endTime = element.endTime if (startTime == null || endTime == null) { _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.SPLIT_FAIL, '开始时间和结束时间不能为空' ) throw new Error('开始时间和结束时间不能为空') } let outVideoFile = path.join(book.bookFolderPath, `data/frame/${element.name}.mp4`) let res = await this.FfmpegCutVideo( book, startTime, endTime, book.oldVideoPath, outVideoFile ) if (res.code == 0) { _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.SPLIT_FAIL, res.message) throw new Error(res.message) } // 视频裁剪完成,要将裁剪后的视频地址写入到数据库中 _bookTaskDetailService.UpdateBookTaskDetail(element.id, { videoPath: path.relative(define.project_path, outVideoFile) }) } // 小改小说批次的状态 _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.SPLIT_DONE) // 结束,分镜完毕,推送日志,返回成功 await this.taskScheduler.AddLogToDB( bookId, book.type, `全部视频裁剪完成`, OtherData.DEFAULT, LoggerStatus.SUCCESS ) return successMessage(null, '全部视频裁剪完成', 'BasicReverse_CutVideoData') } catch (error) { await this.taskScheduler.AddLogToDB( bookId, book.type, error.message, OtherData.DEFAULT, LoggerStatus.FAIL ) return errorMessage( '裁剪视频失败,错误信息如下: ' + error.message, 'BasicReverse_CutVideoData' ) } } /** * 分离视频片段的音频, * 当没有传入bookTaskId,分离默认的第一个, * 有传入的时候,分离对应的bookTaskId的数据 * @param {*} bookId * @param {*} bookTaskId */ async SplitAudioData(bookId, bookTaskId = null) { try { let _bookService = await BookService.getInstance() let _bookTaskService = await BookTaskService.getInstance() let _bookTaskDetailService = await BookTaskDetailService.getInstance() let book = _bookService.GetBookDataById(bookId) if (book == null) { throw new Error('没有找到对应的小说数据') } let bookTask if (bookTaskId != null) { bookTaskId = _bookTaskService.GetBookTaskData({ id: bookTaskId }) } else { bookTask = _bookTaskService.GetBookTaskData({ bookId: bookId, name: 'output_00001' }) } if (bookTask.data.bookTasks.length <= 0 || bookTask.data.total <= 0) { throw new Error('没有找到对应的小说批次任务数据,请检查bookId是否正确') } bookTask = bookTask.data.bookTasks[0] // 获取对应小说批次任务的分镜信息 let bookTaskDetails = _bookTaskDetailService.GetBookTaskData({ bookId: bookId, bookTaskId: bookTask.id }) if (bookTaskDetails.data.length <= 0) { throw new Error('没有找到对应的小说批次任务数据,请检查bookId是否正确,或者手动执行') } await this.taskScheduler.AddLogToDB( bookId, book.type, `开始分离音频`, OtherData.DEFAULT, LoggerStatus.DOING ) _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.AUDIO) for (let i = 0; i < bookTaskDetails.length; i++) { const element = bookTaskDetails[i] let videoPath = element.videoPath let audioPath = path.join(book.bookFolderPath, `data/audio/${element.name}.mp3`) await CheckFolderExistsOrCreate(path.dirname(audioPath)) // 开始分离音频 let audioRes = await this.FfmpegExtractAudio(videoPath, audioPath) if (audioRes.code == 0) { let errorMessage = `分离音频失败,错误信息如下:${audioRes.message}` _bookTaskService.UpdateBookTaskStatus( bookTask.id, BookTaskStatus.AUDIO_FAIL, errorMessage ) throw new Error(audioRes.message) } _bookTaskDetailService.UpdateBookTaskDetail(element.id, { audioPath: path.relative(define.project_path, audioPath) }) // 推送成功消息 await this.taskScheduler.AddLogToDB( bookId, book.type, `${element.name}分离音频成功,输出地址:${audioPath}`, OtherData.DEFAULT, LoggerStatus.SUCCESS ) } // 修改状态为分离音频成功 _bookTaskService.UpdateBookTaskStatus(bookTask.id, BookTaskStatus.AUDIO_DONE) // 所有音频分离成功,推送日志 await this.taskScheduler.AddLogToDB( bookId, book.type, `${element.name}分离音频成功,输出地址:${audioPath}`, OtherData.DEFAULT, LoggerStatus.SUCCESS ) return successMessage(null, '所有音频分离成功', 'BasicReverse_SplitAudioData') } catch (error) { let errorMessage = `分离音频失败,错误信息如下:${error.message}` await this.taskScheduler.AddLogToDB( bookId, book.type, errorMessage, OtherData.DEFAULT, LoggerStatus.FAIL ) return errorMessage(errorMessage, 'BasicReverse_SplitAudioData') } } /** * 提取字幕 * @param {*} bookId * @param {*} bookTaskId * @returns */ async ExtractSubtitlesData(bookId, bookTaskId = null) { try { } catch (error) { let errorMessage = `提取字幕失败,错误信息如下:${error.message}` await this.taskScheduler.AddLogToDB( bookId, book.type, errorMessage, OtherData.DEFAULT, LoggerStatus.FAIL ) return errorMessage(errorMessage, 'BasicReverse_ExtractSubtitlesData') } } }