diff --git a/package-lock.json b/package-lock.json index c6a2ea1..266e374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "laitool", - "version": "3.2.4", + "version": "3.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laitool", - "version": "3.2.2", + "version": "3.3.1", "hasInstallScript": true, "dependencies": { "@alicloud/alimt20181012": "^1.2.0", diff --git a/package.json b/package.json index 7963d34..d423d22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "laitool", - "version": "3.2.5", + "version": "3.3.1", "description": "An AI tool for image processing, video processing, and other functions.", "main": "./out/main/index.js", "author": "laitool.cn", diff --git a/resources/image/c_s/bcd05697-e4eb-48fe-a164-a8fabafe53c9.png b/resources/image/c_s/bcd05697-e4eb-48fe-a164-a8fabafe53c9.png new file mode 100644 index 0000000..a468910 Binary files /dev/null and b/resources/image/c_s/bcd05697-e4eb-48fe-a164-a8fabafe53c9.png differ diff --git a/resources/image/c_s/f5f23903-a91b-44ed-9c13-8e089be9c546.png b/resources/image/c_s/f5f23903-a91b-44ed-9c13-8e089be9c546.png new file mode 100644 index 0000000..a468910 Binary files /dev/null and b/resources/image/c_s/f5f23903-a91b-44ed-9c13-8e089be9c546.png differ diff --git a/resources/scripts/db/book.realm.lock b/resources/scripts/db/book.realm.lock index dec7719..b3cbffa 100644 Binary files a/resources/scripts/db/book.realm.lock and b/resources/scripts/db/book.realm.lock differ diff --git a/resources/scripts/db/software.realm b/resources/scripts/db/software.realm index 205f13d..bf216ad 100644 Binary files a/resources/scripts/db/software.realm and b/resources/scripts/db/software.realm differ diff --git a/resources/scripts/db/software.realm.lock b/resources/scripts/db/software.realm.lock index 131a685..1e4547d 100644 Binary files a/resources/scripts/db/software.realm.lock and b/resources/scripts/db/software.realm.lock differ diff --git a/resources/scripts/db/tts.realm.lock b/resources/scripts/db/tts.realm.lock index ae6f085..83a496a 100644 Binary files a/resources/scripts/db/tts.realm.lock and b/resources/scripts/db/tts.realm.lock differ diff --git a/src/define/define_string/bookDefineString.ts b/src/define/define_string/bookDefineString.ts index 17b6dd9..0972dd5 100644 --- a/src/define/define_string/bookDefineString.ts +++ b/src/define/define_string/bookDefineString.ts @@ -80,6 +80,11 @@ const BOOK = { */ SD_IMAGE_GENERATE_RETURN: 'SD_IMAGE_GENERATE_RETURN', + /** + * ComfyUI生图返回信息 + */ + ComfyUI_IMAGE_GENERATE_RETURN: 'ComfyUI_IMAGE_GENERATE_RETURN', + /** * D3 出图返回信息 */ @@ -119,6 +124,12 @@ const BOOK = { /** 保存缓存区的屠图片到小说主图或者是选图区 */ SAVE_CACHE_IMAGE_TO_DATA: "SAVE_CACHE_IMAGE_TO_DATA", + /** 删除缓存区中的图片 */ + DELETE_CACHE_IMAGE: "DELETE_CACHE_IMAGE", + + /** 移动指定图片链接到主图 */ + MOVE_IMAGE_TO_MAIN_IMAGE: "MOVE_IMAGE_TO_MAIN_IMAGE", + //#endregion COMPUTE_STORYBOARD: 'COMPUTE_STORYBOARD', diff --git a/src/define/enum/bookEnum.ts b/src/define/enum/bookEnum.ts index b6978ad..3c1091c 100644 --- a/src/define/enum/bookEnum.ts +++ b/src/define/enum/bookEnum.ts @@ -17,6 +17,8 @@ export enum BookImageCategory { MJ = 'mj', // SD SD = 'sd', + // ComfyUI + ComfyUI = "comfyui", // D3 D3 = 'd3', // FLUX API @@ -75,6 +77,8 @@ export enum BookBackTaskType { MJ_IMAGE = 'mj_image', // SD 生成图片 SD_IMAGE = 'sd_image', + // ComfyUI 生成图片 + ComfyUI_IMAGE = 'comfyui_image', // flux forge 生成图片 FLUX_FORGE_IMAGE = 'flux_forge_image', // flux api 生成图片 diff --git a/src/define/enum/mjEnum.ts b/src/define/enum/mjEnum.ts index 3cf17da..1351593 100644 --- a/src/define/enum/mjEnum.ts +++ b/src/define/enum/mjEnum.ts @@ -14,6 +14,8 @@ export enum MJImageType { // 本地 SD LOCAL_SD = 'local_sd', + // ComfyUI + ComfyUI = 'comfyui', // flux-api FLUX_API = 'flux-api', diff --git a/src/define/enum/option.ts b/src/define/enum/option.ts index 9b7dc05..92bea7c 100644 --- a/src/define/enum/option.ts +++ b/src/define/enum/option.ts @@ -56,5 +56,20 @@ export enum OptionKeyName { FLUX_APIModelList = 'FLUX_APIModelList', + //#endregion + + //#region SD/ComfyUI + + /** + * ComfyUI 基础设置 + */ + ComfyUI_SimpleSetting = "ComfyUI_SimpleSetting", + + + /** + * ComfyUI 工作流设置 + */ + ComfyUI_WorkFlowSetting = "ComfyUI_WorkFlowSetting" + //#endregion } \ No newline at end of file diff --git a/src/define/enum/softwareEnum.ts b/src/define/enum/softwareEnum.ts index ba0a32e..e8497d5 100644 --- a/src/define/enum/softwareEnum.ts +++ b/src/define/enum/softwareEnum.ts @@ -64,6 +64,7 @@ export enum ResponseMessageType { REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE',// 反推提示词翻译 GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译 MJ_IMAGE = 'MJ_IMAGE',// MJ 生成图片 + ComfyUI_IMAGE = 'ComfyUI_IMAGE',// ComfyUI 生成图片 HD_IMAGE = 'HD_IMAGE',// HD 生成图片 RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频 LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频 diff --git a/src/define/setting/imageSetting.js b/src/define/setting/imageSetting.js index dbfe520..b948b87 100644 --- a/src/define/setting/imageSetting.js +++ b/src/define/setting/imageSetting.js @@ -42,13 +42,17 @@ export const ImageSetting = { return { code: 1, data: [ + { + label: 'MJ', + value: 'mj' + }, { label: 'SD', value: 'sd' }, { - label: 'MJ', - value: 'mj' + label: 'ComfyUI', + value: 'comfyui' }, { label: 'D3', diff --git a/src/main/IPCEvent/bookIpc.ts b/src/main/IPCEvent/bookIpc.ts index 8184db1..8950e6c 100644 --- a/src/main/IPCEvent/bookIpc.ts +++ b/src/main/IPCEvent/bookIpc.ts @@ -299,6 +299,12 @@ export function BookIpc() { /** 保存缓存区的屠图片到小说主图或者是选图区 */ ipcMain.handle(DEFINE_STRING.BOOK.SAVE_CACHE_IMAGE_TO_DATA, async (event, bookTaskDetailId: string, imageFile: string | string[], option: string) => await bookImage.SaveCacheImageToData(bookTaskDetailId, imageFile, option)) + /** 删除缓存区的图片 */ + ipcMain.handle(DEFINE_STRING.BOOK.DELETE_CACHE_IMAGE, async (event, bookTaskId: string, imageFile: string) => await bookImage.DeleteCacheImage(bookTaskId, imageFile)) + + /** 移动指定的图片链接到主图 */ + ipcMain.handle(DEFINE_STRING.BOOK.MOVE_IMAGE_TO_MAIN_IMAGE, async (event, bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) => await bookImage.MoveImageToMainImage(bookTaskId, bookTaskDetailId, sourceImagePath)) + //#endregion diff --git a/src/main/Service/Book/bookImage.ts b/src/main/Service/Book/bookImage.ts index 10ac105..62e4fe0 100644 --- a/src/main/Service/Book/bookImage.ts +++ b/src/main/Service/Book/bookImage.ts @@ -324,6 +324,9 @@ export class BookImage { } else if (element.imageCategory == BookImageCategory.SD) { taskType = BookBackTaskType.SD_IMAGE; responseMessageName = DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN; + } else if (element.imageCategory == BookImageCategory.ComfyUI) { + taskType = BookBackTaskType.ComfyUI_IMAGE; + responseMessageName = DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN; } else if (element.imageCategory == BookImageCategory.D3) { taskType = BookBackTaskType.D3_IMAGE; responseMessageName = DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN; @@ -792,4 +795,108 @@ export class BookImage { //#endregion + //#region 删除缓存区中的图片 + + /** + * 删除缓存区中的图片 + * @param bookTaskId 指定的小说任务ID + * @param imageFile 要删除的图片文件地址 + * @returns + */ + public async DeleteCacheImage(bookTaskId: string, imageFile: string) { + try { + imageFile = imageFile.split("?t=")[0]; + imageFile = imageFile.split("?time=")[0]; + if (imageFile.startsWith("file:/")) { + imageFile = imageFile.replace("file:///", ""); + imageFile = imageFile.replace("file://", ""); + imageFile = imageFile.replace("file:/", ""); + } + if (!await CheckFileOrDirExist(imageFile)) { + throw new Error(`图片文件 ${imageFile} 不存在,请检查`) + } + let deleteBaseName = path.basename(imageFile).toLocaleLowerCase(); + + // 开始查找数据删除 + let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId); + // 修改缓存区数据 + let cacheImageList = bookTask.cacheImageList ?? []; + + for (let i = 0; i < cacheImageList.length; i++) { + const element = cacheImageList[i]; + let baseName = path.basename(element).toLocaleLowerCase(); + if (deleteBaseName == baseName) { + cacheImageList.splice(i, 1); + break; + } + } + // 删除本地的图片 + await fs.promises.unlink(imageFile); + // 修改 + await this.bookServiceBasic.UpdetedBookTaskData(bookTaskId, { + cacheImageList: cacheImageList.map((item) => path.relative(define.project_path, item)) + }) + return successMessage(null, '删除选中缓存区中的图片成功', 'BookImage_DeleteCacheImage') + + } catch (error) { + return errorMessage('删除选中缓存区中的图片失败,错误信息如下:' + error.message, 'BookImage_DeleteCacheImage') + } + } + + //#endregion + + //#region 将指定的图片放到主图中 + + /** + * 将指定的图片链接复制到主图文件夹中 + * @param bookTaskDetailId + * @param sourceImagePath + * @returns + */ + public async MoveImageToMainImage(bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) { + try { + let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId); + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskDetailId); + + // 获取小说项目的地址 + let imageFolder = bookTask.imageFolder; + if (imageFolder == null) { + throw new Error('没有找到对应的小说项目的图片地址,请检查') + } + + let currentImagePath = path.join(imageFolder, `${bookTaskDetail.name}.png`); + // 判断地址对应的文件是不是存在 + if (await CheckFileOrDirExist(currentImagePath)) { + await fs.promises.unlink(currentImagePath); + } + + sourceImagePath = sourceImagePath.split("?t=")[0]; + sourceImagePath = sourceImagePath.split("?time=")[0]; + if (sourceImagePath.startsWith("file:/")) { + sourceImagePath = sourceImagePath.replace("file:///", ""); + sourceImagePath = sourceImagePath.replace("file://", ""); + sourceImagePath = sourceImagePath.replace("file:/", ""); + } + sourceImagePath = path.resolve(sourceImagePath); + if (!await CheckFileOrDirExist(sourceImagePath)) { + throw new Error(`图片文件 ${sourceImagePath} 不存在,请检查`) + } + + // 复制文件,判断父文件夹是不是存在 + await CopyFileOrFolder(sourceImagePath, currentImagePath, true); + // 修改数据库数据 + await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetailId, { + outImagePath: path.relative(define.project_path, currentImagePath) + }) + // 返回数据 + return successMessage(currentImagePath + `?t=${Date.now()}`, '将指定的图片放到主图中成功', 'BookImage_MoveImageToMainImage') + + } catch (error) { + return errorMessage('将指定的图片放到主图中失败,错误信息如下:' + error.message, 'BookImage_MoveImageToMainImage') + } + + } + + //#endregion + } diff --git a/src/main/Service/MJ/mjApi.ts b/src/main/Service/MJ/mjApi.ts index 84ec4cc..49e8050 100644 --- a/src/main/Service/MJ/mjApi.ts +++ b/src/main/Service/MJ/mjApi.ts @@ -286,12 +286,16 @@ class MJApi { /** * 提交MJ API/代理模式 出图任务 - * @param taskId + * @param taskId */ async SubmitMJImagineAPI(taskId: string, prompt: string): Promise { - let _bookBackTaskListService = await BookBackTaskListService.getInstance() + // 这边校验是不是在提示词包含不正确的链接 + if (prompt.includes("feishu.cn")) { + throw new Error("提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接") + } + // 提交API的出图任务 let data = { botType: this.bootType, @@ -322,7 +326,6 @@ class MJApi { let resData: any = undefined; if (useTransfer) { - let url = define.lms + "/lms/Forward/SimpleTransfer" let transferConfig = { method: 'post', diff --git a/src/main/Service/SD/comfyui.ts b/src/main/Service/SD/comfyui.ts new file mode 100644 index 0000000..e43878b --- /dev/null +++ b/src/main/Service/SD/comfyui.ts @@ -0,0 +1,526 @@ +import { BookBackTaskListService } from "@/define/db/service/Book/bookBackTaskListService"; +import { OptionRealmService } from "@/define/db/service/SoftWare/optionRealmService"; +import { OptionKeyName } from "@/define/enum/option"; +import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder } from "@/define/Tools/file"; +import { ValidateJson } from "@/define/Tools/validate"; +import { TaskModal } from "@/model/task"; +import { BookServiceBasic } from "@/main/Service/ServiceBasic/bookServiceBasic"; +import fs from "fs"; +import axios from "axios"; +import { isEmpty } from "lodash"; +import { BookBackTaskStatus, BookTaskStatus, BookType, MJAction } from "@/define/enum/bookEnum"; +import { GeneralResponse } from "@/model/generalResponse"; +import { DEFINE_STRING } from "@/define/define_string"; +import { ResponseMessageType } from "@/define/enum/softwareEnum"; +import { MJImageType, MJRespoonseType } from "@/define/enum/mjEnum"; +import { MJ } from "@/model/mj"; +import { SendMessageToRenderer } from "../globalService"; +import { Book } from "@/model/book/book"; +import path from 'path' +import { Base64ToFile } from "@/define/Tools/image"; +import { define } from "@/define/define" + + +export class ComfyUIOpt { + bookServiceBasic: BookServiceBasic + + constructor() { + this.bookServiceBasic = new BookServiceBasic() + } + + + //#region 获取ComfyUI的设置 + /** + * 获取ComfyUI的设置 + * @returns + */ + private async GetComfyUISetting(): Promise { + let result = {} as ComfyUIModel.ComfyUISettingCollection; + let optionRealmService = await OptionRealmService.getInstance() + let comfyuiSimpleSettingOption = optionRealmService.GetOptionByKey(OptionKeyName.ComfyUI_SimpleSetting) + if (comfyuiSimpleSettingOption == null) { + throw new Error("未找到ComfyUI的设置,请检查是否正确设置!!"); + } + if (!ValidateJson(comfyuiSimpleSettingOption.value)) { + throw new Error("ComfyUI的设置不是有效的JSON格式,请检查是否正确设置!!"); + } + result["comfyuiSimpleSetting"] = JSON.parse(comfyuiSimpleSettingOption.value); + + let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey(OptionKeyName.ComfyUI_WorkFlowSetting) + if (comfyuiWorkFlowSettingOption == null) { + throw new Error("未找到ComfyUI的工作流设置,请检查是否正确设置!!"); + } + if (!ValidateJson(comfyuiWorkFlowSettingOption.value)) { + throw new Error("ComfyUI的工作流设置不是有效的JSON格式,请检查是否正确设置!!"); + } + let comfyuiWorkFlowList = JSON.parse(comfyuiWorkFlowSettingOption.value); + result["comfyuiWorkFlowSetting"] = comfyuiWorkFlowList; + + if (comfyuiWorkFlowList.length <= 0) { + throw new Error("ComfyUI的工作流设置为空,请检查是否正确设置!!"); + } + + // 获取选中的工作流 + let selectedWorkflow = comfyuiWorkFlowList.find(item => item.id == result.comfyuiSimpleSetting.selectedWorkflow); + if (selectedWorkflow == null) { + throw new Error("未找到选中的工作流,请检查是否正确设置!!"); + } + + // 判断工作流对应的文件是不是存在 + if (!await CheckFileOrDirExist(selectedWorkflow.workflowPath)) { + throw new Error("本地未找到选中的工作流文件地址,请检查是否正确设置!!"); + } + result["comfyuiSelectedWorkflow"] = selectedWorkflow; + + return result; + } + + //#endregion + + + //#region 组合ComfyUI的请求体 + /** + * 组合ComfyUI的请求体 + * @param prompt 正向提示词 + * @param negativePrompt 反向提示词 + * @param workflowPath 工作流地址 + */ + private async GetComfyUIAPIBody(prompt: string, negativePrompt: string, workflowPath: string): Promise { + let jsonContentString = await fs.promises.readFile(workflowPath, 'utf-8'); + if (!ValidateJson(jsonContentString)) { + throw new Error("工作流文件内容不是有效的JSON格式,请检查是否正确设置!!"); + } + + let jsonContent = JSON.parse(jsonContentString); + // 判断是否是对象 + if (jsonContent !== null && typeof jsonContent === 'object' && !Array.isArray(jsonContent)) { + // 遍历对象属性 + for (const key in jsonContent) { + let element = jsonContent[key]; + if (element && element.class_type === 'CLIPTextEncode') { + if (element._meta?.title === '正向提示词') { + jsonContent[key].inputs.text = prompt; + } + if (element._meta?.title === '反向提示词') { + jsonContent[key].inputs.text = negativePrompt; + } + } + if (element && element.class_type === "KSampler") { + const crypto = require('crypto'); + const buffer = crypto.randomBytes(8); + let seed = BigInt('0x' + buffer.toString('hex')); + + jsonContent[key].inputs.seed = seed.toString(); + } else if (element && element.class_type === "KSamplerAdvanced") { + const crypto = require('crypto'); + const buffer = crypto.randomBytes(8); + let seed = BigInt('0x' + buffer.toString('hex')); + + jsonContent[key].inputs.noise_seed = seed.toString(); + } + } + } else { + throw new Error("工作流文件内容不是有效的JSON对象格式,请检查是否正确设置!!"); + } + let result = JSON.stringify({ + prompt: jsonContent + }); + return result; + } + + //#endregion + + //#region 提交ComfyUI生成图片任务 + + private async SubmitComfyUIImagine(body: string, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection): Promise { + + let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1"); + if (url.endsWith('/')) { + url = url + "api/prompt" + } else { + url = url + "/api/prompt" + } + var config = { + method: 'post', + url: url, + headers: { + 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)', + 'Content-Type': 'application/json' + }, + data: body + }; + + let res = await axios(config); + let resData = res.data; + // 判断是不是失败 + if (resData.error) { + let errorNode = ''; + if (resData.node_errors) { + for (const key in resData.node_errors) { + errorNode += key + ', '; + } + } + let msg = "错误信息:" + resData.error.message + "错误节点:" + errorNode; + throw new Error(msg); + } + // 没有错误 判断是不是成功 + if (resData.prompt_id && !isEmpty(resData.prompt_id)) { + // 成功 + return resData; + } else { + throw new Error("未知错误,未获取到请求ID,请检查是否正确设置!!"); + } + } + + //#endregion + + //#region 获取comfyui出图任务 + + /** + * 获取ComfyUI出图任务 + * @param promptId + * @param comfyUISettingCollection + */ + private async GetComfyUIImageTask(promptId: string, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection): Promise { + if (isEmpty(promptId)) { + throw new Error("未获取到请求ID,请检查是否正确设置!!"); + } + if (isEmpty(comfyUISettingCollection.comfyuiSimpleSetting.requestUrl)) { + throw new Error("未获取到ComfyUI的请求地址,请检查是否正确设置!!"); + } + + let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1"); + if (url.endsWith('/')) { + url = url + "api/history" + } else { + url = url + "/api/history" + } + + var config = { + method: 'get', + url: `${url}/${promptId}`, + headers: { + 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)' + } + }; + + let res = await axios.request(config); + let resData = res.data; + // 判断状态是失败还是成功 + let data = resData[promptId]; + if (data == null) { + // 还在执行中 或者是任务不存在 + return { + progress: 0, + status: "in_progress", + message: "任务正在执行中" + } + } + let completed = data.status?.completed; + let outputs = data.outputs; + if (completed && outputs) { + let imageNames = []; + for (const key in outputs) { + let outputNode = outputs[key]; + if (outputNode && outputNode?.images && outputNode?.images.length > 0) { + for (let i = 0; i < outputNode?.images.length; i++) { + const element = outputNode?.images[i]; + imageNames.push(element.filename); + } + } + } + return { + progress: 100, + status: "success", + imageNames: imageNames + } + } else { + return { + progress: 0, + status: "error", + message: "生图失败,详细失败信息看启动器控制台" + } + } + + } + + + //#endregion + + //#region 请求下载图片 + + /** + * 请求下载对应的图片 + * @param url + * @param path + */ + private async DownloadFileUrl(imageNames: string[], comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection, book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail): Promise<{ + outImagePath: string, + subImagePath: string[] + }> { + + let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1"); + if (url.endsWith('/')) { + url = url + "api/view" + } else { + url = url + "/api/view" + } + let outImagePath = ""; + let subImagePath = []; + for (let i = 0; i < imageNames.length; i++) { + const imageName = imageNames[i]; + + var config = { + method: 'get', + url: `${url}?filename=${imageName}&nocache=${Date.now()}`, + headers: { + 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)' + }, + responseType: 'arraybuffer' as 'arraybuffer' // 明确指定类型 + }; + + let res = await axios.request(config); + + // 检查响应状态和类型 + console.log(`图片下载状态: ${res.status}, 内容类型: ${res.headers['content-type']}`); + + // 确保得到的是图片数据 + if (!res.headers['content-type']?.includes('image/')) { + console.error(`响应不是图片: ${res.headers['content-type']}`); + continue; + } + + let resData = res.data; + console.log(resData); + + let SdOriginalImage = path.join(book.bookFolderPath, 'data/SdOriginalImage'); + await CheckFolderExistsOrCreate(SdOriginalImage); + let outputFolder = bookTask.imageFolder; + await CheckFolderExistsOrCreate(outputFolder); + let inputFolder = path.join(book.bookFolderPath, 'tmp/input') + await CheckFolderExistsOrCreate(inputFolder); + + + // 包含info信息的图片地址 + let infoImgPath = path.join(SdOriginalImage, `info_${bookTaskDetail.name}_${new Date().getTime()}_${i}.png`) + // 不包含info信息的图片地址 + let imgPath = path.join(SdOriginalImage, `${bookTaskDetail.name}_${new Date().getTime()}_${i}.png`) + + // 直接将二进制数据写入文件 + await fs.promises.writeFile(infoImgPath, Buffer.from(resData)); + + // 这边去图片信息 + // await DeleteFileExifData(path.join(define.package_path, 'exittool/exiftool.exe'), infoImgPath, imgPath); + + if (i == 0) { + // 复制到对应的文件夹里面 + let outPath = path.join(outputFolder, `${bookTaskDetail.name}.png`) + await CopyFileOrFolder(infoImgPath, outPath) + outImagePath = outPath + } + subImagePath.push(infoImgPath) + } + console.log(outImagePath); + console.log(subImagePath); + + // 将获取的数据返回 + return { + outImagePath: outImagePath, + subImagePath: subImagePath + } + + } + + + //#endregion + + //#region 获取出图任务 + + async FetchImageTask(task: TaskModal.Task, promptId: string, book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection) { + while (true) { + try { + let resData = await this.GetComfyUIImageTask(promptId, comfyUISettingCollection); + + // 判断他的状态是不是成功 + if (resData.status == 'error') { + // 生图失败 + await this.bookServiceBasic.UpdateBookTaskDetail(task.bookTaskDetailId, { + status: BookTaskStatus.IMAGE_FAIL, + }); + let errorMsg = `MJ生成图片失败,失败信息如下:${resData.message}` + await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, { + mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl, + progress: 100, + category: MJImageType.ComfyUI, + imageClick: "", + imageShow: "", + messageId: promptId, + action: MJAction.IMAGINE, + status: 'error', + message: errorMsg + }) + await this.bookServiceBasic.UpdateTaskStatus({ + id: task.id, + status: BookBackTaskStatus.FAIL, + errorMessage: errorMsg + }); + global.newWindow[0].win.webContents.send(task.messageName, { + code: 0, + message: errorMsg, + data: { + status: 'error', + message: errorMsg, + id: task.bookTaskDetailId + } + }) + return; + } else if (resData.status == 'in_progress') { + // 生图中 + await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, { + mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl, + progress: 0, + category: MJImageType.ComfyUI, + imageClick: "", + imageShow: "", + messageId: promptId, + action: MJAction.IMAGINE, + status: 'running', + message: "任务正在执行中" + }) + + global.newWindow[0].win.webContents.send(task.messageName, { + code: 1, + message: "running", + data: { + status: 'running', + message: "任务正在执行中", + id: task.bookTaskDetailId + } + }) + } else { + + let res = await this.DownloadFileUrl(resData.imageNames, comfyUISettingCollection, book, bookTask, bookTaskDetail); + console.log(res); + + // 修改数据库数据 + // 修改数据库 + await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, { + outImagePath: path.relative(define.project_path, res.outImagePath), + subImagePath: res.subImagePath.map((item) => path.relative(define.project_path, item)) + }) + await this.bookServiceBasic.UpdateTaskStatus({ + id: task.id, + status: BookBackTaskStatus.DONE + }); + + await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, { + mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl, + progress: 100, + category: MJImageType.ComfyUI, + imageClick: "", + imageShow: "", + messageId: promptId, + action: MJAction.IMAGINE, + status: 'success', + message: "ComfyUI 生成图片成功" + }) + + global.newWindow[0].win.webContents.send(task.messageName, { + code: 1, + message: "ComfyUI 生成图片成功", + data: { + status: 'success', + message: 'ComfyUI 生成图片成功', + id: task.bookTaskDetailId, + outImagePath: res.outImagePath + "?t=" + new Date().getTime(), + subImagePath: res.subImagePath.map((item) => item + "?t=" + new Date().getTime()) + } + }) + break; + } + await new Promise(resolve => setTimeout(resolve, 3000)); + } catch (error) { + throw error; + } + } + } + + //#endregion + + //#region ComfyUI 生成图片 + /** + * ComfyUI 生成图片 + * @param task + */ + async ComfyUIImageGenerate(task: TaskModal.Task) { + try { + let comfyUISettingCollection = await this.GetComfyUISetting(); + + let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(task.bookTaskDetailId); + let book = await this.bookServiceBasic.GetBookDataById(bookTaskDetail.bookId) + let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskDetail.bookTaskId) + + let prompt = bookTaskDetail.prompt; + let negativePrompt = comfyUISettingCollection.comfyuiSimpleSetting.negativePrompt; + + // 开始组合请求体 + let body = await this.GetComfyUIAPIBody(prompt, negativePrompt, comfyUISettingCollection.comfyuiSelectedWorkflow.workflowPath); + + // 开始发送请求 + let resData = await this.SubmitComfyUIImagine(body, comfyUISettingCollection); + + // 修改任务状态 + await this.bookServiceBasic.UpdateBookTaskDetail(task.bookTaskDetailId, { + status: BookTaskStatus.IMAGE + }) + await this.bookServiceBasic.UpdateTaskStatus({ + id: task.id, + status: BookBackTaskStatus.RUNNING + }) + global.newWindow[0].win.webContents.send(task.messageName, { + code: 0, + message: "任务已提交", + data: { + status: 'submited', + message: '任务已提交', + id: task.bookTaskDetailId + } + }) + + await this.FetchImageTask(task, resData.prompt_id, book, bookTask, bookTaskDetail, comfyUISettingCollection); + + } catch (error) { + + let errorMsg = "ComfyUI 生图失败,失败信息如下:" + error.toString() + await this.bookServiceBasic.UpdateTaskStatus({ + id: task.id, + status: BookBackTaskStatus.FAIL, + errorMessage: errorMsg + }) + await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, { + mjApiUrl: "", + progress: 0, + category: MJImageType.ComfyUI, + imageClick: "", + imageShow: "", + messageId: "", + action: MJAction.IMAGINE, + status: "error", + message: errorMsg, + }) + + global.newWindow[0].win.webContents.send(task.messageName, { + code: 0, + message: errorMsg, + data: { + status: 'error', + message: errorMsg, + id: task.bookTaskDetailId + } + }) + throw error + } + } + + //#endregion +} \ No newline at end of file diff --git a/src/main/Service/SD/sd.ts b/src/main/Service/SD/sd.ts index 79c1ba4..8289ef3 100644 --- a/src/main/Service/SD/sd.ts +++ b/src/main/Service/SD/sd.ts @@ -257,6 +257,10 @@ export class SDOpt { } else { url = url + "/sdapi/v1/txt2img" } + + // 替换url中的localhost为127.0.0.1 + url = url.replace('localhost', '127.0.0.1'); + if (!isEmpty(sdSetting.webui.prompt)) { prompt = sdSetting.webui.prompt + ', ' + prompt } @@ -345,8 +349,8 @@ export class SDOpt { messageId: subImagePath.join(','), action: MJAction.IMAGINE, status: "success", - subImagePath: subImagePath, - outImagePath: outImagePath, + subImagePath: subImagePath + "?t=" + new Date().getTime(), + outImagePath: subImagePath.map((item) => item + "?t=" + new Date().getTime()), message: "SD生成图片成功" } await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, resp) diff --git a/src/main/Service/d3.ts b/src/main/Service/d3.ts index a41c7a0..1e9cd7b 100644 --- a/src/main/Service/d3.ts +++ b/src/main/Service/d3.ts @@ -9,6 +9,7 @@ import { Base64ToFile, GetImageBase64 } from "../../define/Tools/image"; import { BookBackTaskStatus } from "../../define/enum/bookEnum"; import { MJAction, MJImageType } from "../../define/enum/mjEnum"; import axios from "axios"; +import { TaskModal } from "@/model/task"; export class D3Opt { bookServiceBasic: BookServiceBasic gptService: GptService diff --git a/src/main/Service/task/taskManage.ts b/src/main/Service/task/taskManage.ts index 0bf0be2..7d17c93 100644 --- a/src/main/Service/task/taskManage.ts +++ b/src/main/Service/task/taskManage.ts @@ -8,6 +8,7 @@ import { GeneralResponse } from '../../../model/generalResponse' import { DEFINE_STRING } from '../../../define/define_string' import { MJOpt } from '../MJ/mj' import { SDOpt } from '../SD/sd' +import { ComfyUIOpt } from '../SD/comfyui' import { D3Opt } from '../d3' import { FluxOpt } from '../Flux/flux' import { AsyncQueue } from '../../quene' @@ -39,6 +40,8 @@ export class TaskManager { intervalId: any; // 用于存储 setInterval 的 ID mjOpt: MJOpt sdOpt: SDOpt + comfyUIOpt: ComfyUIOpt + d3Opt: D3Opt fluxOpt: FluxOpt @@ -50,6 +53,7 @@ export class TaskManager { this.reverseBook = new ReverseBook(); this.mjOpt = new MJOpt(); this.sdOpt = new SDOpt(); + this.comfyUIOpt = new ComfyUIOpt(); this.d3Opt = new D3Opt() this.softWareServiceBasic = new SoftWareServiceBasic(); this.fluxOpt = new FluxOpt() @@ -254,6 +258,17 @@ export class TaskManager { }, `${batch}_${task.id}`, batch, `${batch}_${task.id}_${new Date().getTime()}`, this.bookServiceBasic.SetMessageNameTaskToFail) } + /** + * 将Comfy UI生图任务添加到内存任务中 + * @param task + */ + async AddComfyUIImage(task: TaskModal.Task) { + let batch = task.messageName; + global.requestQuene.enqueue(async () => { + await this.comfyUIOpt.ComfyUIImageGenerate(task); + }, `${batch}_${task.id}`, batch, `${batch}_${task.id}_${new Date().getTime()}`, this.bookServiceBasic.SetMessageNameTaskToFail) + } + /** * 异步添加D3图像生成任务 * @@ -350,6 +365,9 @@ export class TaskManager { case BookBackTaskType.SD_IMAGE: this.AddSDImage(task); break; + case BookBackTaskType.ComfyUI_IMAGE: + this.AddComfyUIImage(task); + break; case BookBackTaskType.D3_IMAGE: this.AddD3Image(task); break; diff --git a/src/model/comfyui.d.ts b/src/model/comfyui.d.ts new file mode 100644 index 0000000..47ffd4f --- /dev/null +++ b/src/model/comfyui.d.ts @@ -0,0 +1,43 @@ +declare namespace ComfyUIModel { + + /** ComfyUI的基础设置的模型 */ + interface ComfyUI_SimpleSettingModel { + /** 请求地址 */ + requestUrl: string, + /** 选择的工作流 */ + selectedWorkflow: string, + /** 反向提示词 */ + negativePrompt: string, + } + + /** ComfyUI 工作流设置的模型 */ + interface ComfyUI_WorkFlowSettingModel { + /** 设置的ID */ + id: string, + /** 自定义的名字 */ + name: string, + /** 工作流的地址 */ + workflowPath: string, + } + + /** + * ComfyUI的设置集合 + */ + interface ComfyUISettingCollection { + /** + * ComfyUI的基础设置 + */ + comfyuiSimpleSetting: ComfyUI_SimpleSettingModel, + /** + * ComfyUI的工作流集合 + */ + comfyuiWorkFlowSetting: Array, + + /** + * 当前选中的工作流 + */ + comfyuiSelectedWorkflow: ComfyUI_WorkFlowSettingModel + + } + +} \ No newline at end of file diff --git a/src/model/option/option.d.ts b/src/model/option/option.d.ts index d0ba7ef..8cd391d 100644 --- a/src/model/option/option.d.ts +++ b/src/model/option/option.d.ts @@ -109,9 +109,15 @@ declare namespace OptionModel { /** 超时时间,单位 毫秒 */ timeOut: number } - - - //#endregion + //#region SD/MJ + + /** ComfyUI的基础设置的模型 */ + interface ComfyUI_SimpleSettingModel extends ComfyUIModel.ComfyUI_SimpleSettingModel { }; + + /** ComfyUI 工作流设置的模型 */ + interface ComfyUI_WorkFlowSettingModel extends ComfyUIModel.ComfyUI_WorkFlowSettingModel { }; + + //#endregion } diff --git a/src/preload/book/book.ts b/src/preload/book/book.ts index da4af81..de70dc0 100644 --- a/src/preload/book/book.ts +++ b/src/preload/book/book.ts @@ -194,6 +194,11 @@ const book = { /** 保存缓存区的屠图片到小说主图或者是选图区 */ SaveCacheImageToData: async (bookTaskDetailId: string, imageFile: string | string[], option: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.SAVE_CACHE_IMAGE_TO_DATA, bookTaskDetailId, imageFile, option), + /** 删除缓存区中的图片 */ + DeleteCacheImage: async (bookTaskId: string, imageFile: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.DELETE_CACHE_IMAGE, bookTaskId, imageFile), + + /** 移动指定的图片链接到主图 */ + MoveImageToMainImage: async (bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.MOVE_IMAGE_TO_MAIN_IMAGE, bookTaskId, bookTaskDetailId, sourceImagePath), //#endregion //#region 一键反推的单个任务 diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index d5ff828..aea3ab6 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,25 +1,42 @@ diff --git a/src/renderer/src/components/CopyWriting/ManageAISetting.vue b/src/renderer/src/components/CopyWriting/ManageAISetting.vue index a3fbb5c..2ef5538 100644 --- a/src/renderer/src/components/CopyWriting/ManageAISetting.vue +++ b/src/renderer/src/components/CopyWriting/ManageAISetting.vue @@ -1,42 +1,136 @@ + + diff --git a/src/renderer/src/components/Setting/SDSetting/ComfyUISetting.vue b/src/renderer/src/components/Setting/SDSetting/ComfyUISetting.vue new file mode 100644 index 0000000..ccd4424 --- /dev/null +++ b/src/renderer/src/components/Setting/SDSetting/ComfyUISetting.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/renderer/src/components/Components/SDADetailerSetting.vue b/src/renderer/src/components/Setting/SDSetting/SDADetailerSetting.vue similarity index 100% rename from src/renderer/src/components/Components/SDADetailerSetting.vue rename to src/renderer/src/components/Setting/SDSetting/SDADetailerSetting.vue diff --git a/src/renderer/src/components/Setting/SDSetting.vue b/src/renderer/src/components/Setting/SDSetting/SDSetting.vue similarity index 83% rename from src/renderer/src/components/Setting/SDSetting.vue rename to src/renderer/src/components/Setting/SDSetting/SDSetting.vue index a4b64d3..419bf0b 100644 --- a/src/renderer/src/components/Setting/SDSetting.vue +++ b/src/renderer/src/components/Setting/SDSetting/SDSetting.vue @@ -62,24 +62,18 @@ v-model:value="formValue.denoising_strength" placeholder="输入重绘幅度" /> - - - -
- + + + +
- - - * - - + +
+ + + * + + + + + + 修手/脸设置 + +
+
- - + + diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js index f11e49f..1acbfee 100644 --- a/src/renderer/src/main.js +++ b/src/renderer/src/main.js @@ -61,7 +61,7 @@ const routes = [ { path: '/sd_setting', name: 'sd_setting', - component: () => import('./components/Setting/SDSetting.vue') + component: () => import('./components/Setting/SDSetting/SDSetting.vue') }, { path: '/copywriting', diff --git a/src/stores/option.ts b/src/stores/option.ts index 055dd01..fed86b1 100644 --- a/src/stores/option.ts +++ b/src/stores/option.ts @@ -31,6 +31,16 @@ export type OptionStoreModel = { /** MJ 基础设置 */ [OptionKeyName.MJ_GlobalSetting]: MJSettingModel.MJ_GlobalSettingModel; //#endregion + + //#region SD/ComfyUI + + /** ComfyUI的基础设置 */ + [OptionKeyName.ComfyUI_SimpleSetting]: OptionModel.ComfyUI_SimpleSettingModel; + + /** COmfyUI的工作流设置 */ + [OptionKeyName.ComfyUI_WorkFlowSetting]: Array; + + //#endregion } export const useOptionStore = defineStore('option', { @@ -104,7 +114,13 @@ export const useOptionStore = defineStore('option', { mj_remoteSimpleSetting: { useTransfer: false } - } + }, + [OptionKeyName.ComfyUI_SimpleSetting]: { + requestUrl: "", + negativePrompt: "", + selectedWorkflow: null + }, + [OptionKeyName.ComfyUI_WorkFlowSetting]: [] } as unknown as OptionStoreModel), getters: { diff --git a/tsconfig.json b/tsconfig.json index 6a83c81..f687e30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ }, "include": [ "src", - "./package.json" -, "src/renderer/src/components/Book/Components/.vue" ] + "./package.json", + "src/renderer/src/components/Book/Components/.vue" + ] } \ No newline at end of file