import { isEmpty } from 'lodash' import { BookService } from '../../define/db/service/Book/bookService' import { errorMessage, successMessage } from '../generalTools' import { FfmpegOptions } from './ffmpegOptions' import { SubtitleSavePositionType } from '../../define/enum/waterMarkAndSubtitle' import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService' import { define } from '../../define/define' import path from 'path' import { CheckFileOrDirExist, DeleteFolderAllFile, GetFilesWithExtensions } from '../../define/Tools/file' import { shell } from 'electron' import fs from 'fs' const util = require('util') const { exec } = require('child_process') const execAsync = util.promisify(exec) const fspromises = fs.promises /** * 去除水印和获取字幕相关操作 */ export class WatermarkAndSubtitle { constructor() {} async InitService() { this.bookService = await BookService.getInstance() this.bookTaskDetailService = await BookTaskDetailService.getInstance() this.FfmpegOptions = new FfmpegOptions() } //#region 通用方法 /** * 拆分视频总帧数,每秒多少帧,平分视频总帧数,后截取 * @param {*} videoDurationMs 视频的总时长(毫秒) * @param {*} framesPerSecond 每秒截取多少帧 * @returns */ GenerateFrameTimes(videoDurationMs, framesPerSecond) { // 直接使用视频总时长(毫秒),不进行向下取整 const videoDurationSec = videoDurationMs / 1000 // 计算总共需要抽取的帧数,考虑到视频时长可能不是完整秒数,使用 Math.ceil 来确保至少获取到最后一秒内的帧 const totalFrames = Math.ceil(videoDurationSec * framesPerSecond) // 计算两帧之间的时间间隔(毫秒) const interval = 1000 / framesPerSecond // 生成对应的时间点数组 const frameTimes = [] for (let i = 0; i < totalFrames; i++) { // 使用 Math.min 确保最后一个时间点不会超过视频总时长 let timePoint = Math.min(Math.round(interval * i), videoDurationMs) frameTimes.push(timePoint) } return frameTimes } //#endregion //#region 字幕 /** * 获取当前视频中所有的字幕信息 * @param {*} value 需要的参数的对象,包含下面的参数 * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) * @param {*} value.videoPath 视频路径 */ async GetVideoFrameText(value) { try { await this.InitService() let videoPath let tempImageFolder let position if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { let bookRes = this.bookService.GetBookDataById(value.id) if (bookRes.data == null) { throw new Error('没有找到小说对应的的视频地址') } let book = bookRes.data tempImageFolder = path.join(define.project_path, `${book.id}/data/subtitle/${book.id}/temp`) if (isEmpty(book.subtitlePosition)) { throw new Error('请先保存位置信息') } position = JSON.parse(book.subtitlePosition) videoPath = book.oldVideoPath } // // 判断文件夹是不是存在,存在的话,将里面的所有文件删除 // await DeleteFolderAllFile(tempImageFolder) // // 将视频进行抽帧,(目前是每秒1帧,时间小于一秒,抽一帧) // let getDurationRes = await this.FfmpegOptions.FfmpegGetVideoDuration(videoPath) // if (getDurationRes.code == 0) { // throw new Error(getDurationRes.message) // } // let videoDuration = getDurationRes.data // let frameTime = this.GenerateFrameTimes(videoDuration, 1) // for (let i = 0; i < frameTime.length; i++) { // const item = frameTime[i] // let name = i.toString().padStart(6, '0') // let imagePath = path.join(tempImageFolder, `frame_${name}.png`) // // 开始裁剪抽, // let res = await this.FfmpegOptions.FfmpegGetVideoFramdAndClip( // videoPath, // item, // imagePath, // position // ) // // 开始识别 // if (res.code == 0) { // throw new Error(res.message) // } // } // 截取完毕,删除大的图片 // 开始识别 let textRes = await this.GetCurrentFrameText({ id: value.id, type: value.type, imageFolder : tempImageFolder }) let allTextData = [] // 开始获取所有的数据 let jsonPaths = await GetFilesWithExtensions(tempImageFolder, ['.json']) for (let i = 0; i < jsonPaths.length; i++) { const element = jsonPaths[i] // 开始拼接 let texts = JSON.parse(await fspromises.readFile(element, 'utf-8')) for (let j = 0; j < texts.length; j++) { const text = texts[j][1][0] allTextData.includes(text) ? null : allTextData.push(text) } } console.log(allTextData.join('\n')) } catch (error) { return errorMessage( '提取视频的的文案信息失败,错误消息如下:' + error.toString(), 'WatermarkAndSubtitle_GetCurrentFrameText' ) } } /** * 获取当前帧的文字信息 * @param {*} value 需要的参数的对象,必须包含以下参数 * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) */ async GetCurrentFrameText(value) { try { await this.InitService() let iamgePaths = [] let imageFolder if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { // 判断是不是有位置信息 imageFolder = value.imageFolder ? value.imageFolder : path.join(define.project_path, `${value.id}/data/subtitle/${value.id}`) let imageFolderIsExist = await CheckFileOrDirExist(imageFolder) if (!imageFolderIsExist) { throw new Error('请先保存位置信息') } let images = await GetFilesWithExtensions(imageFolder, ['.png']) let regex = /.*frame_.*\.png$/ images.forEach((element) => { // 使用正则表达式测试文件名 if (regex.test(element)) { iamgePaths.push(element) } }) } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { } // 开始识别 for (let i = 0; i < iamgePaths.length; i++) { const imagePath = iamgePaths[i] let scriptPath = path.join(define.scripts_path, 'LaiOcr/LaiOcr.exe') let script = `cd "${path.dirname(scriptPath)}" && "${scriptPath}" "${imagePath}"` let scriptRes = await execAsync(script, { maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8' }) console.log(scriptRes) if (scriptRes.error) { throw new Error(scriptRes.error) } } // 处理所有的图片完毕,遍历所有的数据返回 let textData = [] let jsonPath = await GetFilesWithExtensions(imageFolder, ['.json']) for (let i = 0; i < jsonPath.length; i++) { const element = jsonPath[i] // 开始拼接 let texts = JSON.parse(await fspromises.readFile(element, 'utf-8')) for (let j = 0; j < texts.length; j++) { const text = texts[j][1][0] textData.includes(text) ? null : textData.push(text) } } return successMessage( textData.join('\n'), '获取当前帧的文字信息成功', 'WatermarkAndSubtitle_GetCurrentFrameText' ) } catch (error) { return errorMessage( '获取当前帧的文字信息失败,错误消息如下:' + error.toString(), 'WatermarkAndSubtitle_GetCurrentFrameText' ) } } /** * 打开对应的ID的字幕提取的图片文件夹 * @param {*} value 需要的参数的对象,必须包含以下参数 * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) */ async OpenBookSubtitlePositionScreenshot(value) { try { let folder if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { folder = path.join(define.project_path, `${value.id}/data/subtitle/${value.id}`) } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { folder = path.join(define.project_path, `${value.id}/data/subtitle/${value.id}`) } // 判断文件夹是不是存在 let folderIsExist = await CheckFileOrDirExist(folder) if (!folderIsExist) { throw new Error('文件夹不存在,请先保存字幕位置信息') } // 打开文件夹\ shell.openPath(folder) return successMessage( null, '打开对应的文件夹成功', 'WatermarkAndSubtitle_OpenBookSubtitlePositionScreenshot' ) } catch (error) { return errorMessage( '打开字幕位置信息失败,错误消息如下:' + error.toString(), 'WatermarkAndSubtitle_OpenBookSubtitlePositionScreenshot' ) } } /** * 保存反推的视频的文案位置信息(可以保存多个) * @param {*} value 需要的参数的对象,必须包含以下参数 * @param {*} value.id 小说ID/小说分镜详细信息ID/null * @param {*} value.bookSubtitlePosition 小说文案对应的位置 * @param {*} value.currentTime 视频当前保存的时间 * @param {*} value.type 保存的类型(主视频/分镜视频/后续会添加外部单独的视频提取) * @returns */ async SaveBookSubtitlePosition(value) { try { await this.InitService() let saveData = [] let videoPath let outImagePath // 小说视频保存 this.FfmpegOptions = new FfmpegOptions() if (value.type == SubtitleSavePositionType.MAIN_VIDEO) { if (value.id == null) { throw new Error('小说ID不能为空') } // 获取指定的小说 let bookRes = this.bookService.GetBookDataById(value.id) if (bookRes.data == null) { throw new Error(bookRes.message) } let book = bookRes.data if (value.bookSubtitlePosition.length <= 0) { throw new Error('没有获取到字幕信息') } videoPath = book.oldVideoPath if (isEmpty(videoPath)) { throw new Error('没有获取到视频路径') } outImagePath = path.join(book.bookFolderPath, `data/subtitle/${book.id}/frame.png`) // 获取视频的宽高数据 let videoSizeRes = await this.FfmpegOptions.FfmpegGetVideoSize(videoPath) if (videoSizeRes.code == 0) { throw new Error(videoSizeRes.message) } let videoSize = videoSizeRes.data // 开始计算比例 let videoWidth = videoSize.width let videoHeight = videoSize.height for (let i = 0; i < value.bookSubtitlePosition.length; i++) { const element = value.bookSubtitlePosition[i] let widthRate = videoWidth / element.videoWidth // 宽度比例 let heightRate = videoHeight / element.videoHeight // 高度比例 // 计算比例 let newStartX = widthRate * element.startX let newStartY = heightRate * element.startY let newWidth = widthRate * element.width let newHeight = heightRate * element.height saveData.push({ startX: newStartX, startY: newStartY, width: newWidth, height: newHeight, videoWidth: videoWidth, videoHeight: videoHeight }) } // 数据保存 let saveRes = await this.bookService.UpdateBookData(value.id, { subtitlePosition: JSON.stringify(saveData) }) if (saveRes.code == 0) { throw new Error(saveRes.message) } } else if (value.type == SubtitleSavePositionType.STORYBOARD_VIDEO) { // 小说分镜详细信息保存 if (value.id == null) { throw new Error('小说分镜详细信息ID不能为空') } // 获取指定的小说分镜详细信息 let bookStoryboardRes = this.bookTaskDetailService.GetBookTaskDetailDataById(value.id) if (bookStoryboardRes.data == null) { throw new Error('没有找到小说分镜信息') } let bookStoryboard = bookStoryboardRes.data if (value.bookSubtitlePosition.length <= 0) { throw new Error('没有获取到字幕信息') } videoPath = bookStoryboard.videoPath if (isEmpty(videoPath)) { throw new Error('没有获取到视频路径') } outImagePath = path.join( define.project_path, `${bookStoryboard.bookId}/data/subtitle/${bookStoryboard.id}/frame.png` ) // 获取视频的宽高数据 this.FfmpegOptions = new FfmpegOptions() let videoSizeRes = await this.FfmpegOptions.FfmpegGetVideoSize(videoPath) if (videoSizeRes.code == 0) { throw new Error(videoSizeRes.message) } let videoSize = videoSizeRes.data // 开始计算比例 let videoWidth = videoSize.width let videoHeight = videoSize.height for (let i = 0; i < value.bookSubtitlePosition.length; i++) { const element = value.bookSubtitlePosition[i] let widthRate = videoWidth / element.videoWidth // 宽度比例 let heightRate = videoHeight / element.videoHeight // 高度比例 // 计算比例 let newStartX = widthRate * element.startX let newStartY = heightRate * element.startY let newWidth = widthRate * element.width let newHeight = heightRate * element.height saveData.push({ startX: newStartX, startY: newStartY, width: newWidth, height: newHeight, videoWidth: videoWidth, videoHeight: videoHeight }) } // 数据保存 let saveRes = this.bookTaskDetailService.UpdateBookTaskDetail(bookStoryboard.value.id, { subtitlePosition: JSON.stringify(saveData) }) if (saveRes.code == 0) { throw new Error(saveRes.message) } } // 开始设置裁剪出来的图片位置 // 裁剪一个示例图片 let saveImagePath = await this.FfmpegOptions.FfmpegGetVideoFramdAndClip( videoPath, value.currentTime * 1000, outImagePath, saveData ) if (saveImagePath.code == 0) { throw new Error(saveImagePath.message) } return successMessage( saveImagePath.data, '保存字幕位置信息成功', 'WatermarkAndSubtitle_SaveBookSubtitlePosition' ) } catch (error) { return errorMessage( '保存字幕位置信息失败,错误消息如下:' + error.toString(), 'WatermarkAndSubtitle_SaveBookSubtitlePosition' ) } } //#endregion }