LaiTool_PRO/src/main/service/sd/comfyUIServiceHandle.ts
2025-09-04 16:58:42 +08:00

614 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option'
import { SettingModal } from '@/define/model/setting'
import { TaskModal } from '@/define/model/task'
import { optionSerialization } from '../option/optionSerialization'
import {
CheckFileOrDirExist,
CheckFolderExistsOrCreate,
CopyFileOrFolder
} from '@/define/Tools/file'
import fs from 'fs'
import { ValidateJson } from '@/define/Tools/validate'
import axios from 'axios'
import { isEmpty } from 'lodash'
import { BookBackTaskStatus, BookTaskStatus, OperateBookType } from '@/define/enum/bookEnum'
import { SendReturnMessage } from '@/public/generalTools'
import { Book } from '@/define/model/book/book'
import { MJAction } from '@/define/enum/mjEnum'
import { ImageGenerateMode } from '@/define/data/mjData'
import path from 'path'
import { getProjectPath } from '../option/optionCommonService'
import { SDServiceHandle } from './sdServiceHandle'
export class ComfyUIServiceHandle extends SDServiceHandle {
constructor() {
super()
}
ComfyUIImageGenerate = async (task: TaskModal.Task) => {
try {
await this.InitSDBasic()
let comfyUISettingCollection = await this.GetComfyUISetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string
)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说分镜')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
)
if (bookTask == null) {
throw new Error('未找到对应的小说任务')
}
// 调用方法合并提示词
let mergeRes = await this.MergeSDPrompt(
task.bookTaskDetailId as string,
OperateBookType.BOOKTASKDETAIL
)
if (mergeRes.code == 0) {
throw new Error(mergeRes.message)
}
// 获取提示词
bookTaskDetail.prompt = mergeRes.data[0].prompt
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.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, {
status: BookTaskStatus.IMAGE
})
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.RUNNING
})
SendReturnMessage(
{
code: 1,
message: '任务已提交',
id: task.bookTaskDetailId as string,
data: {
status: 'submited',
message: '任务已提交',
id: task.bookTaskDetailId as string
} as any
},
task.messageName as string
)
await this.FetchImageTask(
task,
resData.prompt_id,
book,
bookTask,
bookTaskDetail,
comfyUISettingCollection
)
} catch (error: any) {
let errorMsg = 'ComfyUI 生图失败,失败信息如下:' + error.toString()
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
errorMessage: errorMsg
})
await this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: '',
progress: 0,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
imageShow: '',
messageId: '',
action: MJAction.IMAGINE,
status: 'error',
message: errorMsg
}
)
SendReturnMessage(
{
code: 0,
message: errorMsg,
id: task.bookTaskDetailId as string,
data: {
status: 'error',
message: errorMsg,
id: task.bookTaskDetailId
}
},
task.messageName as string
)
throw error
}
}
//#region 获取ComfyUI的设置
/**
* 获取ComfyUI的设置
* @returns
*/
private async GetComfyUISetting(): Promise<SettingModal.ComfyUISettingCollection> {
let result = {} as SettingModal.ComfyUISettingCollection
let optionRealmService = await OptionRealmService.getInstance()
let comfyuiSimpleSettingOption = optionRealmService.GetOptionByKey(
OptionKeyName.SD.ComfyUISimpleSetting
)
result['comfyuiSimpleSetting'] = optionSerialization<SettingModal.ComfyUISimpleSettingModel>(
comfyuiSimpleSettingOption,
'设置 -> ComfyUI 设置'
)
let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey(
OptionKeyName.SD.ComfyUIWorkFlowSetting
)
let comfyuiWorkFlowList = optionSerialization<SettingModal.ComfyUIWorkFlowSettingModel[]>(
comfyuiWorkFlowSettingOption,
'设置 -> ComfyUI 设置'
)
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<string> {
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: SettingModal.ComfyUISettingCollection
): Promise<any> {
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 获取出图任务
async FetchImageTask(
task: TaskModal.Task,
promptId: string,
book: Book.SelectBook,
bookTask: Book.SelectBookTask,
bookTaskDetail: Book.SelectBookTaskDetail,
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
) {
while (true) {
try {
let resData = await this.GetComfyUIImageTask(promptId, comfyUISettingCollection)
// 判断他的状态是不是成功
if (resData.status == 'error') {
// 生图失败
await this.bookTaskDetailService.ModifyBookTaskDetailById(
task.bookTaskDetailId as string,
{
status: BookTaskStatus.IMAGE_FAIL
}
)
let errorMsg = `MJ生成图片失败失败信息如下${resData.message}`
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 100,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
imageShow: '',
messageId: promptId,
action: MJAction.IMAGINE,
status: 'error',
message: errorMsg
}
)
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
errorMessage: errorMsg
})
SendReturnMessage(
{
code: 0,
message: errorMsg,
id: task.bookTaskDetailId as string,
data: {
status: 'error',
message: errorMsg,
id: task.bookTaskDetailId
}
},
task.messageName as string
)
return
} else if (resData.status == 'in_progress') {
// 生图中
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 0,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
imageShow: '',
messageId: promptId,
action: MJAction.IMAGINE,
status: 'running',
message: '任务正在执行中'
}
)
SendReturnMessage(
{
code: 1,
message: 'running',
id: task.bookTaskDetailId as string,
data: {
status: 'running',
message: '任务正在执行中',
id: task.bookTaskDetailId
}
},
task.messageName as string
)
} else {
let res = await this.DownloadFileUrl(
resData.imageNames,
comfyUISettingCollection,
book,
bookTask,
bookTaskDetail
)
let projectPath = await getProjectPath()
// 修改数据库数据
// 修改数据库
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetail.id as string, {
outImagePath: path.relative(projectPath, res.outImagePath),
subImagePath: res.subImagePath.map((item) => path.relative(projectPath, item))
})
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.DONE
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 100,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
imageShow: '',
messageId: promptId,
action: MJAction.IMAGINE,
status: 'success',
message: 'ComfyUI 生成图片成功'
}
)
SendReturnMessage(
{
code: 1,
message: 'ComfyUI 生成图片成功',
id: task.bookTaskDetailId as string,
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())
}
},
task.messageName as string
)
break
}
await new Promise((resolve) => setTimeout(resolve, 3000))
} catch (error) {
throw error
}
}
}
//#endregion
//#region 获取comfyui出图任务
/**
* 获取ComfyUI出图任务
* @param promptId
* @param comfyUISettingCollection
*/
private async GetComfyUIImageTask(
promptId: string,
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
): Promise<any> {
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: string[] = []
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 as string)
}
}
}
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: SettingModal.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: string[] = []
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 subImageFolderPath = path.join(
bookTask.imageFolder as string,
`subImage/${bookTaskDetail.name}`
)
await CheckFolderExistsOrCreate(subImageFolderPath)
let outputFolder = bookTask.imageFolder as string
await CheckFolderExistsOrCreate(outputFolder)
let inputFolder = path.join(book.bookFolderPath as string, 'tmp/input')
await CheckFolderExistsOrCreate(inputFolder)
// 包含info信息的图片地址
let infoImgPath = path.join(subImageFolderPath, `${new Date().getTime()}_${i}.png`)
// 直接将二进制数据写入文件
await fs.promises.writeFile(infoImgPath, Buffer.from(resData))
if (i == 0) {
// 复制到对应的文件夹里面
let outPath = path.join(outputFolder, `${bookTaskDetail.name}.png`)
await CopyFileOrFolder(infoImgPath, outPath)
outImagePath = outPath
}
subImagePath.push(infoImgPath as string)
}
console.log(outImagePath)
console.log(subImagePath)
// 将获取的数据返回
return {
outImagePath: outImagePath,
subImagePath: subImagePath
}
}
//#endregion
}