From d94e21b3b21e9a9e7124c339af0feee2f0a64a34 Mon Sep 17 00:00:00 2001 From: lq1405 <2769838458@qq.com> Date: Sun, 14 Sep 2025 16:25:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20MJ=20=E8=BD=AC=E8=A7=86?= =?UTF-8?q?=E9=A2=91=20=E4=BC=98=E5=8C=96=E8=A7=86=E9=A2=91=E7=9A=84?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=9A=84=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/define/Tools/file.ts | 2 +- .../db/service/book/bookTaskDetailService.ts | 2 +- src/define/enum/video.ts | 1 + src/define/ipc/subIpc/bookIPC/bookImageIpc.ts | 12 + src/define/ipc/subIpc/systemIpc.ts | 7 + .../subDefineString/bookDefineString.ts | 6 + .../subDefineString/systemDefineString.ts | 3 + src/define/model/book/bookTaskDetail.d.ts | 3 + src/define/model/book/bookVideo.d.ts | 36 +++ src/define/model/image.d.ts | 16 + src/i18n/locales/en.ts | 15 + src/i18n/locales/zh-cn.ts | 15 + .../book/bookIndex/bookImageEntrance.ts | 8 + .../book/subBookHandle/bookImageHandle.ts | 276 +++++++++++++++++- .../subBookHandle/bookVideoServiceHandle.ts | 2 + src/main/service/system/electronInterface.ts | 89 ++++++ src/main/service/video/index.ts | 5 + src/main/service/video/mjVideo.ts | 191 ++++++++++-- .../bookProload/bookImagePreload.ts | 10 +- src/preload/subPreload/system.ts | 4 + src/renderer/components.d.ts | 3 +- src/renderer/src/common/toolData.ts | 8 +- .../CopyWriting/CopyWritingCategoryMenu.vue | 2 +- .../MediaToVideoInfoMJVideoExtend.vue | 130 ++++++++- .../MediaToVideoInfoMJVideoImageToVideo.vue | 79 ++++- .../MediaToVideoInfoMJVideoInfo.vue | 79 ++++- .../MediaToVideoInfoTaskList.vue | 98 +------ .../MediaToVideoInfoVideoConfig.vue | 100 +++++-- .../MediaToVideoInfoVideoListInfo.vue | 2 +- .../BookTaskDetail/BookTaskDetailTable.vue | 17 +- .../BookTaskDetail/DatatableHeaderImage.vue | 129 +++++++- .../BookTaskDetail/MessageAndProgress.vue | 33 ++- .../MainHome/OriginalBookTaskCard.vue | 249 ---------------- .../Original/MainHome/OriginalTaskCard.vue | 31 +- .../components/common/LanguageSwitcher.vue | 0 .../src/components/common/TextEllipsis.vue | 52 ++-- .../src/components/common/VideoDisplay.vue | 173 +++++++++++ .../src/composables/useReactiveI18n.ts | 0 src/renderer/src/hooks/useFile.ts | 23 ++ src/renderer/src/hooks/useMD.js | 14 +- src/renderer/src/hooks/useSimple.ts | 30 ++ src/renderer/src/router/index.ts | 4 +- src/renderer/src/stores/locale.ts | 0 src/renderer/src/views/CopyWritingHome.vue | 5 +- src/renderer/src/views/ToolBoxHome.vue | 16 +- 46 files changed, 1504 insertions(+), 478 deletions(-) create mode 100644 src/define/model/image.d.ts delete mode 100644 src/renderer/src/components/Original/MainHome/OriginalBookTaskCard.vue create mode 100644 src/renderer/src/components/common/LanguageSwitcher.vue create mode 100644 src/renderer/src/components/common/VideoDisplay.vue create mode 100644 src/renderer/src/composables/useReactiveI18n.ts create mode 100644 src/renderer/src/hooks/useFile.ts create mode 100644 src/renderer/src/hooks/useSimple.ts create mode 100644 src/renderer/src/stores/locale.ts diff --git a/package.json b/package.json index 80d2075..33892eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "laitool-pro", "productName": "来推 Pro", - "version": "v3.4.5", + "version": "v3.4.6", "description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。", "main": "./out/main/index.js", "author": "xiangbei", diff --git a/src/define/Tools/file.ts b/src/define/Tools/file.ts index 078d4e2..0d09b12 100644 --- a/src/define/Tools/file.ts +++ b/src/define/Tools/file.ts @@ -9,7 +9,7 @@ const fspromises = fs.promises /** * 判断文件或目录是否存在 - * @param {*} path 文件或目录的路径 + * @param {*} filePath 文件或目录的路径 * @returns true表示存在,false表示不存在 */ export async function CheckFileOrDirExist(filePath) { diff --git a/src/define/db/service/book/bookTaskDetailService.ts b/src/define/db/service/book/bookTaskDetailService.ts index 068923d..b665246 100644 --- a/src/define/db/service/book/bookTaskDetailService.ts +++ b/src/define/db/service/book/bookTaskDetailService.ts @@ -67,7 +67,7 @@ export class BookTaskDetailService extends RealmBaseService { oldImage: JoinPath(projectPath, item.oldImage), outImagePath: JoinPath(projectPath, item.outImagePath), subImagePath: (item.subImagePath as string[])?.map((subImage) => { - return JoinPath(projectPath, subImage) + return JoinPath(projectPath, subImage) + '?t=' + new Date().getTime() }), subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()), subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => { diff --git a/src/define/enum/video.ts b/src/define/enum/video.ts index 8a655e6..fabc1d8 100644 --- a/src/define/enum/video.ts +++ b/src/define/enum/video.ts @@ -52,6 +52,7 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) = case ImageToVideoModels.PIKA: return 'Pika' case ImageToVideoModels.MJ_VIDEO: + case ImageToVideoModels.MJ_VIDEO_EXTEND: return t('MJ视频') default: return '未知' diff --git a/src/define/ipc/subIpc/bookIPC/bookImageIpc.ts b/src/define/ipc/subIpc/bookIPC/bookImageIpc.ts index ee4bf68..8c47d0d 100644 --- a/src/define/ipc/subIpc/bookIPC/bookImageIpc.ts +++ b/src/define/ipc/subIpc/bookIPC/bookImageIpc.ts @@ -48,5 +48,17 @@ export function bookImageIpc() { await bookHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl) ) + /** 同步主图文件到批次任务 */ + ipcMain.handle( + DEFINE_STRING.BOOK.SYNC_MAIN_IMAGE_FOR_BOOK_TASK, + async (_, bookTaskId: string) => await bookHandle.SyncMainImageForBookTask(bookTaskId) + ) + + /** 同步子图文件到批次任务 */ + ipcMain.handle( + DEFINE_STRING.BOOK.SYNC_SUB_IMAGE_FOR_BOOK_TASK, + async (_, bookTaskId: string) => await bookHandle.SyncSubImageForBookTask(bookTaskId) + ) + //#endregion } diff --git a/src/define/ipc/subIpc/systemIpc.ts b/src/define/ipc/subIpc/systemIpc.ts index 45afe0a..3bc8dbc 100644 --- a/src/define/ipc/subIpc/systemIpc.ts +++ b/src/define/ipc/subIpc/systemIpc.ts @@ -133,6 +133,13 @@ function SystemIpc() { async (_, filePath: string) => await electronInterface.ReadTextFile(filePath) ) + /** 上传图片到LaiTool云端 */ + ipcMain.handle( + DEFINE_STRING.SYSTEM.UPLOAD_IMAGE_TO_LAITOOL, + async (_, imagePath: string, type: "video" | "image") => + await electronInterface.UploadImageToLaiTool(imagePath, type) + ) + //#endregion } diff --git a/src/define/ipcDefineString/subDefineString/bookDefineString.ts b/src/define/ipcDefineString/subDefineString/bookDefineString.ts index f0be7ec..9497a01 100644 --- a/src/define/ipcDefineString/subDefineString/bookDefineString.ts +++ b/src/define/ipcDefineString/subDefineString/bookDefineString.ts @@ -136,6 +136,12 @@ const BOOK = { /** 下载图片并拆分处理应用到分镜 */ DOWNLOAD_IMAGE_URL_AND_SPLIT: 'DOWNLOAD_IMAGE_URL_AND_SPLIT', + + /** 同步主图文件到批次任务 */ + SYNC_MAIN_IMAGE_FOR_BOOK_TASK: 'SYNC_MAIN_IMAGE_FOR_BOOK_TASK', + + /** 同步子图文件到批次任务 */ + SYNC_SUB_IMAGE_FOR_BOOK_TASK: 'SYNC_SUB_IMAGE_FOR_BOOK_TASK', //#endregion //#region 导出相关 diff --git a/src/define/ipcDefineString/subDefineString/systemDefineString.ts b/src/define/ipcDefineString/subDefineString/systemDefineString.ts index 5f738d5..86d3c69 100644 --- a/src/define/ipcDefineString/subDefineString/systemDefineString.ts +++ b/src/define/ipcDefineString/subDefineString/systemDefineString.ts @@ -51,6 +51,9 @@ const SYSTEM = { /** 读取文本文件内容 */ READ_TEXT_FILE: 'READ_TEXT_FILE', + /** 上传图片到LaiTool云端 */ + UPLOAD_IMAGE_TO_LAITOOL: 'UPLOAD_IMAGE_TO_LAITOOL', + //#endregion /** 打开指定的url */ diff --git a/src/define/model/book/bookTaskDetail.d.ts b/src/define/model/book/bookTaskDetail.d.ts index f85fbb4..ca26c34 100644 --- a/src/define/model/book/bookTaskDetail.d.ts +++ b/src/define/model/book/bookTaskDetail.d.ts @@ -137,6 +137,9 @@ declare namespace BookTaskDetail { */ endImageUrl?: string + /** 视频拓展时的尾帧图片 */ + extendEndImageUrl?: string + /** * 是否首尾循环 */ diff --git a/src/define/model/book/bookVideo.d.ts b/src/define/model/book/bookVideo.d.ts index 848a322..9b88904 100644 --- a/src/define/model/book/bookVideo.d.ts +++ b/src/define/model/book/bookVideo.d.ts @@ -10,4 +10,40 @@ declare namespace BookVideo { bookId?: string // 小说ID bookTaskId?: string // 小说任务ID } + + /** + * 视频生成请求参数接口 + * + * @interface VideoGenerateRequest + * @property {string | null} [prompt] - 生成提示词,可选 + * @property {'low' | 'high'} motion - 运动幅度,必需 + * @property {'vid_1.1_i2v_480' | 'vid_1.1_i2v_720'} videoType - 视频类型,必需 + * @property {'url' | 'base64'} image - 首帧图片格式,扩展时可为空,必需 + * @property {string} [endImage] - 尾帧图片,可选 + * @property {boolean} [loop] - 是否循环播放,可选 + * @property {1 | 2 | 4} [batchSize] - 批次大小,可选,默认值为4 + * @property {'extend'} [action] - 对视频任务进行操作,不为空时index、taskId必填,可选 + * @property {number} [index] - 执行的视频索引号,范围0-3,可选 + * @property {string} [taskId] - 需要操作的视频父任务ID,可选 + * @property {string} [state] - 状态,可选 + * @property {string} [notifyHook] - 回调地址,可选 + * @property {boolean} [noStorage] - True时返回官方链接,可选 + */ + interface MJVideoGenerateRequest { + prompt?: string | null + motion: 'low' | 'high' + videoType: 'vid_1.1_i2v_480' | 'vid_1.1_i2v_720' + image?: string + endImage?: string + loop?: boolean + batchSize?: 1 | 2 | 4 + action?: 'extend' + index?: number // 0-3 + taskId?: string + state?: string + notifyHook?: string + noStorage?: boolean + } + + } diff --git a/src/define/model/image.d.ts b/src/define/model/image.d.ts new file mode 100644 index 0000000..0571ebf --- /dev/null +++ b/src/define/model/image.d.ts @@ -0,0 +1,16 @@ +declare namespace ImageModel { + + /** + * 文件上传的请求体 + */ + interface FileUploadRequest { + /** 文件内容 - Base64编码或二进制数据 */ + file: string + /** 文件名称 - 包含扩展名的完整文件名 */ + fileName: string + /** 文件内容类型 - MIME类型,如 'image/jpeg', 'image/png' 等 */ + contentType: string + /** 文件类型分类 - 用于服务器端分类处理,如 'image', 'video' 等 */ + type?: string + } +} \ No newline at end of file diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 800c889..1490e9e 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -2,6 +2,7 @@ export default { //#region 通用 "是": "Yes", "否": 'No', + "空": 'Empty', "操作": 'Action', "清空": 'Clear', "生成": 'Generate', @@ -227,6 +228,10 @@ export default { "初始视频消息失败,未找到指定小说批次任务的分镜数据": "Failed to initialize video message, storyboard data for specified novel batch task not found", '根据ID获取小说批次任务信息失败,ID不能为空!': 'Failed to retrieve novel batch task information by ID, ID cannot be empty!', "未找到对应的小说批次任务信息,请检查!": "Corresponding novel batch task information not found, please check!", + "同步主图文件失败,{error}": "Synchronization of main image file failed, {error}", + "同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}": "Storyboard main image files synchronized successfully, {count} images synchronized, storyboard names: {name}", + "同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}": "Storyboard sub-image files synchronized successfully, {count} images synchronized, storyboard names: {name}", + "同步分镜子图文件失败,{error}": "Synchronization of sub-image files failed, {error}", //#endregion //#region 小说分镜 @@ -298,6 +303,12 @@ export default { "反推提示词的ID不能为空": "Reverse prompt ID cannot be empty", "全部翻译完成": "All translation completed", "翻译失败,{error}": "Translation failed, {error}", + "检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!": "Check all storyboard information, check storyboards without main images, determine if there are images with the same name as the storyboard in the corresponding image output folder, if so, sync to the main image of the corresponding storyboard, if not, skip!", + '检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': 'Check all storyboard information, check storyboards without main images, determine if there are images with the same name as the storyboard in the corresponding image output folder, if so, sync to the main image of the corresponding storyboard, if not, skip!\n\n Note: This operation will not overwrite storyboards that already have main images!\n\n This operation is suitable for manually placing images into the output folder and then syncing to the storyboard! Click "Open Folder" on the right to open the current batch\'s image output folder', + "检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!": "Check all storyboard information, determine if the images in the storyboard's sub-image folder are all in the storyboard's sub-image list, if not, add them!", + '检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': 'Check all storyboard information, determine if the images in the storyboard\'s sub-image folder are all in the storyboard\'s sub-image list, if not, add them!\n\n Note: This operation will not delete existing sub-image information in the storyboard! Instead, it adds image information from the folder to the storyboard\'s sub-image data\n\n This operation is suitable for manually placing sub-images into the sub-image folder and then syncing to the storyboard! Click "Open Folder" on the right to open the current batch\'s image output folder', + '正在同步主图信息,请稍后...': 'Syncing main image information, please wait...', + '同步主图信息失败,{error}': 'Failed to sync main image information, {error}', //#endregion //#region 出图 @@ -550,6 +561,8 @@ export default { "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "Upload images to LaiTool image host, supports multiple image formats, get shareable links", "搜索工具...": "Search tools...", "上传图片到LaiTool图床,获取图片链接": "Upload images to LaiTool image host, get image links", + "上传成功": "Upload successful", + "上传后返回的链接为空!": "Returned link after upload is empty!", "上传失败,{error}": "Upload failed, {error}", '未找到机器ID,请重启软件后重试!!': 'Machine ID not found, please restart software and try again!!', '图片处理完毕,开始上传文件...': 'Image processing completed, starting file upload...', @@ -1326,6 +1339,7 @@ export default { '初始化小说分镜模块失败,{error}': 'Failed to initialize novel storyboard module, {error}', '正在初始化小说分镜模块,{bookName}_{bookTaskName}': 'Initializing novel storyboard module, {bookName}_{bookTaskName}', '分镜出图的进度:{progress}% ': 'Storyboard generation progress: {progress}% ', + "视频生成的进度:{progress}% ": "Video generation progress: {progress}% ", '正在删除小说数据...': 'Deleting novel data...', '是否删除该小说,删除后数据将无法恢复,请确认是否继续!': 'Are you sure you want to delete this novel? Data cannot be recovered after deletion, please confirm whether to continue!', '正在重置小说数据。。。': 'Resetting novel data...', @@ -1777,6 +1791,7 @@ export default { '复制失败:存在未生成的文本,请先生成文本!': 'Copy failed: Ungenerated text exists, please generate text first!', '复制成功': 'Copy successful', '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': 'Copy failed, please manually copy in the display generated text on the left, error details: {error}', + "复制失败,复制内容为空": "Copy failed, copied content is empty", '复制失败:{error}': 'Copy failed: {error}', "复制失败,请手动复制:{data}": "Copy failed, please copy manually: {data}", '取消复制': 'Cancel Copy', diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index f534375..5613eb7 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -2,6 +2,7 @@ export default { //#region 通用 "是": '是', "否": '否', + "空": '空', "操作": '操作', "清空": '清空', "生成": '生成', @@ -227,6 +228,10 @@ export default { "初始视频消息失败,未找到指定小说批次任务的分镜数据": "初始视频消息失败,未找到指定小说批次任务的分镜数据", '根据ID获取小说批次任务信息失败,ID不能为空!': '根据ID获取小说批次任务信息失败,ID不能为空!', "未找到对应的小说批次任务信息,请检查!": "未找到对应的小说批次任务信息,请检查!", + "同步主图文件失败,{error}": "同步主图文件失败,{error}", + "同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}": "同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}", + "同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}": "同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}", + "同步分镜子图文件失败,{error}": "同步分镜子图文件失败,{error}", //#endregion //#region 小说分镜 @@ -298,6 +303,12 @@ export default { "反推提示词的ID不能为空": "反推提示词的ID不能为空", "全部翻译完成": "全部翻译完成", "翻译失败,{error}": "翻译失败,{error}", + "检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!": "检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!", + '检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': '检查所有的分镜信息,检查没有主图的分镜,判断对应的图片输出文件夹中是否有和分镜同名的图片,若有,则同步到对应分镜的主图中,若没有,则跳过!\n\n 注意:该操作不会覆盖已经有主图的分镜!\n\n 该操作适用于手动将图片放入输出文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹', + "检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!": "检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!", + '检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹': '检查所有的分镜信息,判断分镜的子图文件夹中的图片是否都在分镜的子图列表中,若没有,则添加进去!\n\n 注意:该操作不会删除分镜中已经有的子图信息!而是根据文件中的图片信息添加到分镜的子图数据中\n\n 该操作适用于手动将子图放入子图文件夹后,同步到分镜中!点击右侧"打开文件夹"打开当前批次的图片输出文件夹', + '正在同步主图信息,请稍后...': '正在同步主图信息,请稍后...', + '同步主图信息失败,{error}': '同步主图信息失败,{error}', //#endregion //#region 出图 @@ -550,6 +561,8 @@ export default { "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接", "搜索工具...": "搜索工具...", "上传图片到LaiTool图床,获取图片链接": "上传图片到LaiTool图床,获取图片链接", + "上传成功": "上传成功", + "上传后返回的链接为空!": "上传后返回的链接为空!", "上传失败,{error}": "上传失败,{error}", '未找到机器ID,请重启软件后重试!!': '未找到机器ID,请重启软件后重试!!', '图片处理完毕,开始上传文件...': '图片处理完毕,开始上传文件...', @@ -1326,6 +1339,7 @@ export default { '初始化小说分镜模块失败,{error}': '初始化小说分镜模块失败,{error}', '正在初始化小说分镜模块,{bookName}_{bookTaskName}': '正在初始化小说分镜模块,{bookName}_{bookTaskName}', '分镜出图的进度:{progress}% ': '分镜出图的进度:{progress}% ', + "视频生成的进度:{progress}% ": "视频生成的进度:{progress}% ", '正在删除小说数据...': '正在删除小说数据...', '是否删除该小说,删除后数据将无法恢复,请确认是否继续!': '是否删除该小说,删除后数据将无法恢复,请确认是否继续!', '正在重置小说数据。。。': '正在重置小说数据。。。', @@ -1777,6 +1791,7 @@ export default { '复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!', '复制成功': '复制成功', '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}', + "复制失败,复制内容为空": "复制失败,复制内容为空", '复制失败:{error}': '复制失败:{error}', "复制失败,请手动复制:{data}": "复制失败,请手动复制:{data}", '取消复制': '取消复制', diff --git a/src/main/service/book/bookIndex/bookImageEntrance.ts b/src/main/service/book/bookIndex/bookImageEntrance.ts index c56842f..29c4e95 100644 --- a/src/main/service/book/bookIndex/bookImageEntrance.ts +++ b/src/main/service/book/bookIndex/bookImageEntrance.ts @@ -55,5 +55,13 @@ export class BookImageEntrance { DownloadImageUrlAndSplit = async (bookTaskDetailId: string, imageUrl: string) => await this.bookImageHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl) + /** 同步主图文件到批次任务 */ + SyncMainImageForBookTask = async (bookTaskId: string) => + await this.bookImageHandle.SyncMainImageForBookTask(bookTaskId) + + /** 同步子图文件到批次任务 */ + SyncSubImageForBookTask = async (bookTaskId: string) => + await this.bookImageHandle.SyncSubImageForBookTask(bookTaskId) + //#endregion } diff --git a/src/main/service/book/subBookHandle/bookImageHandle.ts b/src/main/service/book/subBookHandle/bookImageHandle.ts index d6f6732..5d0e0ad 100644 --- a/src/main/service/book/subBookHandle/bookImageHandle.ts +++ b/src/main/service/book/subBookHandle/bookImageHandle.ts @@ -5,7 +5,8 @@ import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, - DownloadImageFromUrl + DownloadImageFromUrl, + GetFilesWithExtensions } from '@/define/Tools/file' import { getProjectPath } from '../../option/optionCommonService' import { errorMessage, successMessage } from '@/public/generalTools' @@ -741,4 +742,277 @@ export class BookImageHandle extends BookBasicHandle { ) } } + + /** + * 同步主图文件到批次任务 + * + * 该方法会检查指定批次任务的所有分镜信息,并将图片输出文件夹中存在的分镜图片文件 + * 同步到对应的分镜数据中。只有当分镜在数据库中没有主图记录或主图文件不存在时, + * 才会进行同步操作。 + * + * 处理流程: + * 1. 验证批次任务和图片输出文件夹的有效性 + * 2. 获取批次任务下的所有分镜详情 + * 3. 遍历每个分镜,检查是否已有有效的主图文件 + * 4. 如果分镜没有主图或主图文件不存在,则在图片文件夹中查找对应的图片文件 + * 5. 找到对应图片后,更新分镜的主图路径和生成状态信息 + * 6. 返回同步结果,包含成功同步的分镜信息 + * + * 注意事项: + * - 只处理 .png 格式的图片文件 + * - 图片文件命名规则:分镜名称.png + * - 不会覆盖已存在且有效的主图文件 + * - 同步后会自动设置生成状态为成功完成 + * + * @param {string} bookTaskId - 小说批次任务ID + * @returns {Promise} + * 成功时返回同步结果数组,包含每个分镜的ID、名称和主图路径 + * 失败时返回错误信息 + * + * @throws {Error} 当找不到批次任务、图片文件夹不存在或分镜数据为空时抛出错误 + * + * @example + * // 同步批次任务的所有主图文件 + * const result = await bookImageHandle.SyncMainImageForBookTask("task-123"); + * if (result.code === 1) { + * console.log("同步成功:", result.data); + * // result.data 包含: [{ id: "detail-1", name: "第一章", outImagePath: "..." }] + * } + */ + public async SyncMainImageForBookTask( + bookTaskId: string + ): Promise { + try { + await this.InitBookBasicHandle() + + // 获取批次任务信息 + const bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true) + + // 获取图片输出文件夹 + const imageFolder = bookTask.imageFolder + if (!imageFolder || isEmpty(imageFolder)) { + throw new Error(t('没有找到对应的小说批次任务的图片输出地址,请检查')) + } + + // 检查图片文件夹是否存在 + if (!(await CheckFileOrDirExist(imageFolder))) { + throw new Error(t("目的文件/文件夹不存在,{data}", { + data: imageFolder + })) + } + + // 获取所有分镜详情 + const bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({ + bookTaskId: bookTaskId + }) + if (bookTaskDetails.length <= 0) { + throw new Error(t('没有找到对应的小说批次任务的分镜数据,请检查')) + } + let result: any = [] + + const projectPath = await getProjectPath() + + for (const detail of bookTaskDetails) { + // 跳过主图已经存在并且对应的文件也存在的分镜 + if (!isEmpty(detail.outImagePath) && await CheckFileOrDirExist(detail.outImagePath as string)) { + continue + } + + // 按照分镜名称获取默认的图片文件,并检查对应的文件是不是存在 + let foundImagePath = path.join(imageFolder, detail.name as string + '.png'); + const foundImagePathExist = await CheckFileOrDirExist(foundImagePath) + if (!foundImagePathExist) { + continue + } + + // 对应的图片存在,小改小说分镜信息 + await this.bookTaskDetailService.ModifyBookTaskDetailById(detail.id as string, { + outImagePath: path.relative(projectPath, foundImagePath), + } as any) + + // 修改出图信息 + let mjMessage = { + status: 'success', + progress: 100, + category: ImageGenerateMode.MJ_API, + messageId: '', + action: MJAction.IMAGINE + } + this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(detail.id as string, mjMessage) + // 成功 构建返回数据 + result.push({ + id: detail.id, + name: detail.name, + outImagePath: foundImagePath + '?t=' + new Date().getTime() + }) + } + + return successMessage( + result, + t("同步分镜主图文件成功,同步成功数 {count},同步分镜名称 {name}", { + count: result.length, + name: result.length == 0 ? t("空") : result.map((item: any) => item.name).join(',') + }), + 'BookImage_SyncMainImageForBookTask' + ) + } catch (error: any) { + return errorMessage( + t('同步主图文件失败,{error}', { + error: (error as Error).message + }), + 'BookImage_SyncMainImageForBookTask' + ) + } + } + + /** + * 同步子图文件到批次任务 + * + * 该方法会检查指定批次任务的所有分镜信息,扫描每个分镜对应的子图文件夹, + * 将文件夹中存在但数据库中没有记录的图片文件添加到分镜的子图列表中。 + * + * 处理流程: + * 1. 验证批次任务和图片输出文件夹的有效性 + * 2. 获取批次任务下的所有分镜详情 + * 3. 遍历每个分镜,构建对应的子图文件夹路径(imageFolder/subImage/分镜名称) + * 4. 扫描子图文件夹中的所有 .png 图片文件 + * 5. 比较文件夹中的图片与数据库中已有的子图记录 + * 6. 将新发现的图片添加到分镜的子图列表中 + * 7. 更新数据库并返回同步结果 + * + * 注意事项: + * - 只处理 .png 格式的图片文件 + * - 不会删除数据库中已有的子图记录,只做增量添加 + * - 子图文件夹路径固定为:图片输出目录/subImage/分镜名称 + * - 路径比较时会移除时间戳参数,确保准确匹配 + * + * @param {string} bookTaskId - 小说批次任务ID + * @returns {Promise} + * 成功时返回同步结果数组,包含每个分镜的ID、名称和更新后的子图路径列表 + * 失败时返回错误信息 + * + * @throws {Error} 当找不到批次任务、图片文件夹不存在或分镜数据为空时抛出错误 + * + * @example + * // 同步批次任务的所有子图文件 + * const result = await bookImageHandle.SyncSubImageForBookTask("task-123"); + * if (result.code === 1) { + * console.log("同步成功:", result.data); + * // result.data 包含: [{ id: "detail-1", name: "第一章", subImagePath: [...] }] + * } + */ + public async SyncSubImageForBookTask( + bookTaskId: string + ): Promise { + try { + // 初始化基础处理器 + await this.InitBookBasicHandle() + + // 获取批次任务信息 + const bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true) + + // 获取并验证图片输出文件夹路径 + const imageFolder = bookTask.imageFolder + if (!imageFolder || isEmpty(imageFolder)) { + throw new Error(t('没有找到对应的小说批次任务的图片输出地址,请检查')) + } + + // 检查图片文件夹是否存在 + if (!(await CheckFileOrDirExist(imageFolder))) { + throw new Error(t("目的文件/文件夹不存在,{data}", { + data: imageFolder + })) + } + + // 获取该批次任务下的所有分镜详情 + const bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({ + bookTaskId: bookTaskId + }) + + // 验证是否有分镜数据 + if (bookTaskDetails.length <= 0) { + throw new Error(t('没有找到对应的小说批次任务的分镜数据,请检查')) + } + + // 初始化结果数组和项目根路径 + const result: { id: string; name: string, subImagePath: string[] }[] = [] + let projectPath = await getProjectPath() + + // 遍历每个分镜进行子图同步 + for (const detail of bookTaskDetails) { + // 构建子图文件夹路径:图片输出目录/subImage/分镜名称 + let subImageFolderPath = path.join(imageFolder, 'subImage', detail.name as string) + + // 检查子图文件夹是否存在,不存在则跳过该分镜 + if (!(await CheckFileOrDirExist(subImageFolderPath))) { + continue + } + + // 获取子图文件夹中的所有 .png 图片文件 + let imageFiles = await GetFilesWithExtensions(subImageFolderPath, ['.png']); + if (imageFiles.length === 0) { + continue // 没有图片文件则跳过 + } + + // 获取当前分镜在数据库中已有的子图列表 + const currentSubImages = detail.subImagePath || [] + + // 将数据库中已有的子图路径转换为绝对路径,便于与文件夹中的图片进行比较 + const currentSubImagePaths = currentSubImages + .map(item => item.replace(/\?t=\d+$/, '')) // 移除时间戳参数 (?t=数字) + .map(item => path.resolve(projectPath, item)) // 相对路径转换为绝对路径 + + // 找出新图片:在文件夹中存在但在数据库记录中不存在的图片 + let newImages: string[] = []; + for (let i = 0; i < imageFiles.length; i++) { + const imageFile = imageFiles[i]; + const absoluteImagePath = path.resolve(imageFile); + + // 如果这个图片文件不在已有的子图列表中,则添加到新图片列表 + if (!currentSubImagePaths.includes(absoluteImagePath)) { + newImages.push(imageFile); + } + } + + // 如果没有新图片需要添加,跳过该分镜 + if (newImages.length === 0) { + continue + } + + // 合并已有子图和新发现的图片,形成完整的子图列表 + const allSubImages = [...currentSubImages, ...newImages] + + // 更新数据库中分镜的子图列表(存储相对路径) + await this.bookTaskDetailService.ModifyBookTaskDetailById(detail.id as string, { + subImagePath: allSubImages.map((item) => path.relative(projectPath, item)) + } as any) + + // 将同步结果添加到返回数组(返回绝对路径+时间戳) + result.push({ + id: detail.id as string, + name: detail.name as string, + subImagePath: allSubImages.map((item) => item + '?t=' + new Date().getTime()) + }) + } + + // 返回成功结果,包含统计信息和详细数据 + return successMessage( + result, + t("同步分镜子图文件成功,同步成功数 {count},同步分镜名称 {name}", { + count: result.length, + name: result.length == 0 ? t("空") : result.map((item: any) => item.name).join(',') + }), + 'BookImage_SyncSubImageForBookTask' + ) + + } catch (error: any) { + // 捕获并返回错误信息 + return errorMessage( + t('同步子图文件失败,{error}', { + error: (error as Error).message + }), + 'BookImage_SyncSubImageForBookTask' + ) + } + } } diff --git a/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts index e997804..ed16178 100644 --- a/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts +++ b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts @@ -308,6 +308,8 @@ export class BookVideoServiceHandle extends BookBasicHandle { switch (task.type) { case BookBackTaskType.MJ_VIDEO: return await videoHandle.MJImageToVideo(task) + case BookBackTaskType.MJ_VIDEO_EXTEND: + return await videoHandle.MJVideoExtendToVideo(task) default: throw new Error('未知的视频生成方式,请检查') } diff --git a/src/main/service/system/electronInterface.ts b/src/main/service/system/electronInterface.ts index 9049219..109a27a 100644 --- a/src/main/service/system/electronInterface.ts +++ b/src/main/service/system/electronInterface.ts @@ -5,6 +5,10 @@ import fs from 'fs/promises' import { errorMessage, successMessage } from '../../../public/generalTools' import { ErrorItem, SuccessItem } from '@/define/model/generalResponse' import { t } from '@/i18n' +import { GetImageBase64, GetMimeType } from '@/define/Tools/image' +import axios from 'axios' +import { define } from '@/define/define' +import { isEmpty } from 'lodash' /** 打开指定的文件夹的方法 */ export type OpenFolderParams = { @@ -318,4 +322,89 @@ export default class ElectronInterface { ) } } + + /** + * 上传图片到LaiTool云端 + * + * 该方法将本地图片文件上传到LaiTool云端服务器,返回可用于分享的网络链接。 + * 支持常见的图片格式,并可指定上传类型用于不同的业务场景。 + * + * @param {string} imagePath - 本地图片文件的绝对路径 + * @param {"video" | "image"} type - 上传类型分类 + * - "image": 普通图片上传,用于一般图片分享 + * - "video": 视频相关图片上传,用于视频生成、MJ垫图等功能 + * + * @returns {Promise} 返回上传结果 + * - 成功时:返回包含图片网络链接等信息的SuccessItem对象 + * - 失败时:返回包含错误信息的ErrorItem对象 + * + * @throws {Error} 当以下情况发生时会抛出错误: + * - 机器码获取失败或为空 + * - 指定的图片文件不存在 + * - 图片文件格式不支持 + * - 网络请求失败 + * - 服务器返回错误响应 + * + * @description 注意事项: + * 1. 每日上传限制:50次/天 + * 2. 支持格式:jpg, jpeg, png, gif, bmp, webp 等常见图片格式 + * 3. 上传后的图片会在LaiTool服务器保留,请确保图片内容合规 + * 4. 返回的网络链接为全球可访问的永久链接 + * 5. 需要有效的机器码和网络连接 + */ + public async UploadImageToLaiTool( + imagePath: string, + type: "video" | "image" + ): Promise { + try { + // 检查机器码是不是存在 + if (!global.machineId || isEmpty(global.machineId)) { + throw new Error(t('获取机器码失败,请重启软件或者检查对应权限!!')) + } + + // 检查文件是不是存在 + if (!(await CheckFileOrDirExist(imagePath))) { + throw new Error(t('文件不存在')) + } + + // 获取图片文件的base64 + let fileBase64 = await GetImageBase64(imagePath) + let contentType = GetMimeType(imagePath); + let body: ImageModel.FileUploadRequest = { + file: fileBase64, + fileName: path.basename(imagePath), + contentType: contentType + } + if (type == "video") { + body.type = "video" + } + + // 开始上传 + let res = await axios.post(define.lms_url + `/lms/FileUpload/FileUpload/${global.machineId}`, body) + let resData = res.data; + if (resData.code !== 1) { + throw new Error(resData.message || t('未知错误')) + } + + let url = resData.data.url; + if (url == null || isEmpty(url)) { + throw new Error(t('上传后返回的链接为空')) + } + + // 上传成功,返回结果 + return successMessage( + resData.data, + t('上传成功'), + 'SystemIpc_UploadImageToLaiTool' + ) + } catch (error: any) { + console.error('上传图片错误:', error) + return errorMessage( + t("上传失败,{error}", { + error: error.message + }), + 'SystemIpc_UploadImageToLaiTool' + ) + } + } } diff --git a/src/main/service/video/index.ts b/src/main/service/video/index.ts index 51af892..6df44e8 100644 --- a/src/main/service/video/index.ts +++ b/src/main/service/video/index.ts @@ -12,4 +12,9 @@ export class VideoHandle { MJImageToVideo(task: TaskModal.Task) { return this.mjVideoService.MJImageToVideo(task) } + + /** MJ视频扩展生成视频处理方法 将指定的视频通过Midjourney API进行扩展生成新视频 */ + MJVideoExtendToVideo(task: TaskModal.Task) { + return this.mjVideoService.MJVideoExtendToVideo(task) + } } diff --git a/src/main/service/video/mjVideo.ts b/src/main/service/video/mjVideo.ts index d5de5d3..224c926 100644 --- a/src/main/service/video/mjVideo.ts +++ b/src/main/service/video/mjVideo.ts @@ -22,6 +22,7 @@ import path from 'path' import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file' import { DownloadFile } from '@/define/Tools/common' import { getProjectPath } from '../option/optionCommonService' +import { define } from '@/define/define' export class MJVideoService extends MJApiService { constructor() { @@ -95,31 +96,25 @@ export class MJVideoService extends MJApiService { throw new Error(t("不支持的图片链接,仅支持网络图片链接!")) } - // 在提示词后面添加 --raw + // 判断有没有提示词,若有提示词并且raw为true,则在提示词后面添加 --raw if (!isEmpty(prompt) && raw) { prompt = prompt + ' --raw' } - prompt = imageUrl + ' ' + prompt - // 添加 批次信息 - prompt = prompt + ' --bs ' + batchSize - - if (loop) { - prompt = prompt + ' --end loop' - } else { - if (!isEmpty(endImageUrl)) { - prompt = prompt + ' --end ' + endImageUrl - } - } - - let body: { - prompt: string - motion?: MJVideoMotion - videoType: MJVideoType - } = { + let body: BookVideo.MJVideoGenerateRequest = { prompt: prompt, motion: motion, - videoType: videoType + videoType: videoType, + image: imageUrl, + batchSize: batchSize + } + + if (loop) { + body.loop = loop + } else { + if (endImageUrl && !isEmpty(endImageUrl)) { + body.endImage = endImageUrl + } } // 开始请求 @@ -178,6 +173,131 @@ export class MJVideoService extends MJApiService { } } + //#region MJVideoExtendToVideo + + async MJVideoExtendToVideo(task: TaskModal.Task) { + try { + await this.InitMJBasic() + await this.InitMJSetting(ImageGenerateMode.MJ_API) + + // 检查是否支持视频功能 + if (this.videoUrl == null || isEmpty(this.videoUrl)) { + throw new Error(t('当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!')) + } + + // 检查 token + if (this.token == null || isEmpty(this.token)) { + throw new Error( + t("对应Midjourney模式的Token不能为空,请前往 {settingPath} 中配置", { settingPath: t("设置 -> MJ设置") }) + ) + } + + // 开始处理小说数据 + let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById( + task.bookTaskDetailId as string, true + ) + + // 获取视频配置信息 + let videoMessage = bookTaskDetail.videoMessage + if (videoMessage == null || videoMessage == undefined) { + throw new Error(t("小说批次任务的分镜数据的转视频配置为空,请检查")) + } + + // 获取 MJ Video 的options + let mjVideoOptionsString = bookTaskDetail.videoMessage?.mjVideoOptions as string + if (!ValidateJson(mjVideoOptionsString)) { + throw new Error(t("当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查")) + } + let mjVideoOptions: BookTaskDetail.MjVideoOptions = JSON.parse(mjVideoOptionsString) + + let prompt = videoMessage.prompt?.trim() + let motion: MJVideoMotion = + mjVideoOptions.motion === MJVideoMotion.High ? MJVideoMotion.High : MJVideoMotion.Low + + let videoType = mjVideoOptions.videoType ?? MJVideoType.HD + + let batchSize = mjVideoOptions.batchSize ?? MJVideoBatchSize.FOUR + + // 暂不支持尾帧 + // let extendEndImageUrl = mjVideoOptions.extendEndImageUrl?.trim() || '' + + let raw = mjVideoOptions.raw ?? false + + if (mjVideoOptions.taskId == null || isEmpty(mjVideoOptions.taskId) || mjVideoOptions.index == null) { + throw new Error(t("请先选择父任务ID和视频索引!")) + } + + + // 开始构建请求体 + // 判断有没有提示词,若有提示词并且raw为true,则在提示词后面添加 --raw + if (!isEmpty(prompt) && raw) { + prompt = prompt + ' --raw' + } + + let body: BookVideo.MJVideoGenerateRequest = { + motion: motion, + videoType: videoType, + batchSize: batchSize, + action: 'extend', + taskId: mjVideoOptions.taskId, + index: mjVideoOptions.index + } + if (!isEmpty(prompt)) { + body.prompt = prompt + } + + // 开始请求 + let res = await axios.post(this.videoUrl, body, { + headers: { + Authorization: this.token + } + }) + console.log('MJVideoExtendToVideo response', res.data) + + let resData = res.data + let id = resData.result + + // 修改Task, 将数据写入 + this.taskListService.UpdateBackTaskData(task.id as string, { + taskId: id as string, + taskMessage: JSON.stringify(resData) + }) + + // 修改videoMessage + videoMessage.taskId = id + videoMessage.status = VideoStatus.WAIT + videoMessage.messageData = JSON.stringify(resData) + videoMessage.msg = '' + delete videoMessage.imageUrl // 不要修改原本的图片地址 + + // 添加任务成功 返回前端任务事件 + SendReturnMessage( + { + code: 1, + id: task.bookTaskDetailId as string, + message: t('已成功提交Midjourney图转视频任务,任务ID:{taskId}', { taskId: id }), + type: ResponseMessageType.MJ_VIDEO, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + + // 开始循环查询任务状态 + await this.FetchMJVideoResult(bookTaskDetail, task, id) + return successMessage( + t('Midjourney图转视频任务执行完成。'), + 'MJVideoService_MJImageToVideo' + ) + + } catch (error) { + throw new Error( + t('Midjourney图转视频任务执行失败,失败信息如下:{error}', { error: (error as Error).message }) + ) + } + } + + //#endregion + //#endregion //#region FetchMJVideoResult @@ -217,7 +337,7 @@ export class MJVideoService extends MJApiService { task: TaskModal.Task, taskId: string, useTransfer: boolean = false - ) { + ): Promise { console.log(useTransfer) while (true) { let fetchUrl = this.fetchTaskUrl.replace('${id}', taskId) @@ -310,8 +430,9 @@ export class MJVideoService extends MJApiService { taskMessage: JSON.stringify(resData) }) + let mjId = resData.properties?.messageHash ?? resData.id // 下载 视频 - await this.DownloadMJVideo(videoMessage.videoUrls || [], task, bookTaskDetail) + await this.DownloadMJVideo(videoMessage.videoUrls || [], task, bookTaskDetail, mjId) SendReturnMessage( { @@ -365,7 +486,8 @@ export class MJVideoService extends MJApiService { async DownloadMJVideo( videoUrls: string[], task: TaskModal.Task, - bookTaskDetail: Book.SelectBookTaskDetail + bookTaskDetail: Book.SelectBookTaskDetail, + preffix: string ) { // 处理完成 开始下载指定的图片 @@ -388,15 +510,36 @@ export class MJVideoService extends MJApiService { 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')) { + // 转存一下视频文件 + // 获取当前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(videoUrl, videoPath) + await DownloadFile(remoteUrl, videoPath) // 处理返回数据信息 // 开始修改信息 // 将信息添加到里面 let a = { localPath: path.relative(project_path, videoPath), - remotePath: videoUrl, + remotePath: remoteUrl, taskId: bookTaskDetail.videoMessage?.taskId, index: i, type: MappingTaskTypeToVideoModel(task.type as string) diff --git a/src/preload/subPreload/bookProload/bookImagePreload.ts b/src/preload/subPreload/bookProload/bookImagePreload.ts index 53613ea..2c6ffec 100644 --- a/src/preload/subPreload/bookProload/bookImagePreload.ts +++ b/src/preload/subPreload/bookProload/bookImagePreload.ts @@ -58,6 +58,14 @@ export const bookImagePreload = { DEFINE_STRING.BOOK.DOWNLOAD_IMAGE_URL_AND_SPLIT, bookTaskDetailId, imageUrl - ) + ), + + /** 同步主图文件到批次任务 */ + SyncMainImageForBookTask: async (bookTaskId: string) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.SYNC_MAIN_IMAGE_FOR_BOOK_TASK, bookTaskId), + + /** 同步子图文件到批次任务 */ + SyncSubImageForBookTask: async (bookTaskId: string) => + await ipcRenderer.invoke(DEFINE_STRING.BOOK.SYNC_SUB_IMAGE_FOR_BOOK_TASK, bookTaskId) // #endregion } diff --git a/src/preload/subPreload/system.ts b/src/preload/subPreload/system.ts index 321fece..68246d3 100644 --- a/src/preload/subPreload/system.ts +++ b/src/preload/subPreload/system.ts @@ -46,6 +46,10 @@ const system = { ReadTextFile: (filePath: string) => ipcRenderer.invoke(DEFINE_STRING.SYSTEM.READ_TEXT_FILE, filePath), + /** 上传图片到LaiTool云端 */ + UploadImageToLaiTool: (imagePath: string, type: "video" | "image") => + ipcRenderer.invoke(DEFINE_STRING.SYSTEM.UPLOAD_IMAGE_TO_LAITOOL, imagePath, type), + //#endregion //#region 系统相关 diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 418b119..059b7cc 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -50,6 +50,7 @@ declare module 'vue' { InputDialogContent: typeof import('./src/components/common/InputDialogContent.vue')['default'] JianyingGenerateInformation: typeof import('./src/components/Original/BookTaskDetail/JianyingGenerateInformation.vue')['default'] JianyingKeyFrameSetting: typeof import('./src/components/Setting/JianyingKeyFrameSetting.vue')['default'] + LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default'] LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default'] ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default'] MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default'] @@ -121,7 +122,6 @@ declare module 'vue' { OpenInBrowserRound: typeof import('./src/components/common/Icon/OpenInBrowserRound.vue')['default'] OriginalAddBook: typeof import('./src/components/Original/MainHome/OriginalAddBook.vue')['default'] OriginalAddBookTask: typeof import('./src/components/Original/MainHome/OriginalAddBookTask.vue')['default'] - OriginalBookTaskCard: typeof import('./src/components/Original/MainHome/OriginalBookTaskCard.vue')['default'] OriginalEmptyState: typeof import('./src/components/Original/MainHome/OriginalEmptyState.vue')['default'] OriginalMobileHeader: typeof import('./src/components/Original/MainHome/OriginalMobileHeader.vue')['default'] OriginalModifyBookTask: typeof import('./src/components/Original/MainHome/OriginalModifyBookTask.vue')['default'] @@ -149,6 +149,7 @@ declare module 'vue' { TopMenuButtons: typeof import('./src/components/Original/BookTaskDetail/TopMenuButtons.vue')['default'] UploadRound: typeof import('./src/components/common/Icon/UploadRound.vue')['default'] UserAnalysis: typeof import('./src/components/Original/Analysis/UserAnalysis.vue')['default'] + VideoDisplay: typeof import('./src/components/common/VideoDisplay.vue')['default'] WechatGroup: typeof import('./src/components/SoftHome/WechatGroup.vue')['default'] WordGroup: typeof import('./src/components/Original/Copywriter/WordGroup.vue')['default'] } diff --git a/src/renderer/src/common/toolData.ts b/src/renderer/src/common/toolData.ts index 4fdb410..1f43138 100644 --- a/src/renderer/src/common/toolData.ts +++ b/src/renderer/src/common/toolData.ts @@ -2,7 +2,7 @@ import { t } from '@/i18n' import { CloudUploadOutline } from '@vicons/ionicons5' // 工具分类 -export const categories = [ +export const getCategories = () => [ { key: 'media', label: t('媒体工具'), color: '#2080f0' } // { key: 'document', label: '文档处理', color: '#18a058' }, // { key: 'development', label: '开发工具', color: '#f0a020' }, @@ -14,7 +14,7 @@ export const categories = [ ] // 工具数据 -export const toolsData = [ +export const getToolsData = () => [ // 媒体工具 { id: 'image-converter', @@ -251,3 +251,7 @@ export const toolsData = [ // } // } ] + +// 兼容性:保持原有的导出名称,但使用函数 +export const categories = getCategories() +export const toolsData = getToolsData() \ No newline at end of file diff --git a/src/renderer/src/components/CopyWriting/CopyWritingCategoryMenu.vue b/src/renderer/src/components/CopyWriting/CopyWritingCategoryMenu.vue index 28b6269..590abad 100644 --- a/src/renderer/src/components/CopyWriting/CopyWritingCategoryMenu.vue +++ b/src/renderer/src/components/CopyWriting/CopyWritingCategoryMenu.vue @@ -54,7 +54,7 @@
{{ subCategory.name }}
- {{ subCategory.remark }} + {{ subCategory.description }}
diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue index f4de193..9cc2aa8 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue @@ -132,6 +132,74 @@ + + +
+ + + + +
+ {{ t('图片预览') }} +
+
+
+ diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue index 24c84a0..5664d13 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue @@ -403,7 +403,7 @@ async function copyTaskId() { try { const taskId = videoMessage.value.taskId if (!taskId) { - message.warning(t('task_id_is_empty')) + message.warning(t('任务ID不能为空,请检查!')) return } diff --git a/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue b/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue index 8b2a22d..5dc105a 100644 --- a/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue +++ b/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue @@ -8,19 +8,20 @@ :loading="loading" :single-line="false" :row-key="rowKey" - :scroll-x="1600" + :scroll-x="bookStore.selectBookTask.openVideoGenerate ? 1700 : 1600" :max-height="tableHeight" /> - - diff --git a/src/renderer/src/components/Original/MainHome/OriginalTaskCard.vue b/src/renderer/src/components/Original/MainHome/OriginalTaskCard.vue index cf46e7c..1684fbc 100644 --- a/src/renderer/src/components/Original/MainHome/OriginalTaskCard.vue +++ b/src/renderer/src/components/Original/MainHome/OriginalTaskCard.vue @@ -2,7 +2,7 @@ -
@@ -277,7 +280,9 @@ function handleButtonClick(event, action, ...args) { // 计算进度 const progress = computed(() => { // 这里可以根据实际业务逻辑计算进度 - return Math.floor(props.bookTask?.imageVideoProgress?.imageRate ?? 0) // 示例:随机进度 + return props.type == 'mediaToVideo' + ? Math.floor(props.bookTask?.imageVideoProgress?.videoRate ?? 0) + : Math.floor(props.bookTask?.imageVideoProgress?.imageRate ?? 0) }) // 处理任务操作 @@ -306,9 +311,23 @@ async function handleTaskAction(key) { break } } +async function handleDBOpenTask() { + switch (props.type) { + case 'origin': + handleOpenTask() + break + case 'mediaToVideo': + handleOpenVideoInfo() + break + default: + message.error(t('未知的任务类型')) + break + } +} // 处理打开任务 async function handleOpenTask() { + console.log('打开任务', props.bookTask, props.type) // 如果刚刚点击了操作按钮,则不执行双击事件 if (isActionClicked.value) { return diff --git a/src/renderer/src/components/common/LanguageSwitcher.vue b/src/renderer/src/components/common/LanguageSwitcher.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/renderer/src/components/common/TextEllipsis.vue b/src/renderer/src/components/common/TextEllipsis.vue index 575100e..990607f 100644 --- a/src/renderer/src/components/common/TextEllipsis.vue +++ b/src/renderer/src/components/common/TextEllipsis.vue @@ -10,20 +10,20 @@ > {{ displayText }} - + {{ displayText }} - +