方案概览
ComfyUI结合多样的自定义节点,能够批量生成图像,一键加载大量工作流,可以轻松实现人像生成、背景替换、风格迁移和图像动画化等功能。
当您想要独立部署和使用ComfyUI,您可能面临显卡成本高、购买难、并发处理能力有限以及使用门槛高等问题。函数计算推出了ComfyUI API Serverless版解决方案,帮助您通过函数计算快速部署基于ComfyUI模型的AI服务。使用本方案可以充分利用ComfyUI和Serverless技术优势,为广大开发者AI绘画创业及变现提供思路。
为了便于用户直观体验ComfyUI Serverless API解决方案的效果,函数计算基于此方案搭建一个【少年白马专属】破次元壁合照 AI 绘画平台应用,供用户快速体验生成AI绘画。
本方案的技术架构包括以下基础设施和云服务。
函数计算:用于提供ComfyUI模型的应用服务,提供GPU和CPU算力。
部署应用
登录函数计算3.0控制台,在左侧导航栏,单击应用。
当左上角显示函数计算 FC 3.0时,表示当前控制台为3.0控制台。
重要ComfyUI模型的应用只在函数计算 FC 3.0支持,如果您登录的是函数计算 2.0的控制台,请点击右上角的体验函数计算 3.0进行切换。
在应用页面,点击创建应用,选择通过模板创建应用,在人工智能页签找到【少年白马专属】破次元壁合照 AI 绘画平台,光标移至该卡片,然后单击立即创建。
在创建应用页面,设置以下配置项,然后单击创建应用。
重点配置项说明如下,如果您没有特殊要求,其余配置项保持默认值即可。
配置项名称
说明
示例值
角色名
创建应用所需的权限。首次创建应用的用户,需要单击前往授权配置角色权限。
AliyunFCServerlessDevsRole
地域
地域选择可以选择距离自己较近的区域,目前支持华东1(杭州)、华东2(上海)、日本(东京)。
由于当前模板涉及GitHub以及HuggingFace等网站的访问,国内部分地域可能无法直接使用。
日本(东京)
命名空间
设置命名空间名称。如果您第一次创建ComfyUI应用,使用默认值即可;如果非第一次创建ComfyUI应用,建议手动设置命名空间名称,便于和其他应用区分。
ComfyUI-api
在弹出的对话框,仔细阅读应用创建提醒信息,勾选涉及的计费项和我已经了解上面的内容,并同意上述描述,然后单击同意并继续部署。
通过API接口调用ComfyUI解决方案
常规的ComfyUI出图的流程调用/prompt
接口。在并发请求数比较大的情况下,我们往往期望可以利用Serverless的弹性优势,动态创建多个函数实例处理出图任务。但由于ComfyUI本身是“有状态”的,难以确保出图的请求和获取状态的请求固定打到同一个实例上,为了让ComfyUI更加适配Serverless模式,请参考fc-comfyui/src/images/agent的代码,在ComfyUI镜像里内置Agent程序,负责转换ComfyUI请求并且拉起ComfyUI,以HTTP同步请求举例如下图。
以上提供的Agent代码作为Serverless方式调用的实践参考。
功能未经过严格测试,请根据实际的业务需要二次开发或调整相关的代码,并构建ComfyUI镜像。
目前提供的Agent能力介绍
在应用详情页,找到ComfyUI函数,点击进入。
开启Agent能力,需要增加USE_AGENT:true环境变量。更多详见配置环境变量。
说明【少年白马专属】破次元壁合照 AI 绘画平台应用作为示例,已经默认配置了USE_AGENT:true环境变量,如果您的代码进行二次开发,并使用ComfyUI镜像里内置Agent程序,可在环境变量进行配置。
当通过Agent的API调用时,建议您调整实例并发数为:1~5,确保并发请求尽量使用单独的实例,提高出图效率。
出图请求(HTTP 同步)
请求路径:ComfyUI函数请求地址/api/run
。
Body:json格式的prompt数据。
返回值:最后一次的进度,包含图片信息。
以ComfyUI函数的公网请求地址为例,通过Postman工具调用接口演示。
获取ComfyUI函数请求地址。在应用详情页,找到ComfyUI函数,点击进入。
在函数详情页,选择配置页签,点击触发器。选择公网访问地址进行复制。
在Postman工具界面,填写复制公网地址,并拼接
/api/run
。选择请求方式为POST,填写请求参数,请求参数可参考Body.json,然后单击Send。返回200表示调用成功。说明如果您在测试调用的过程中,遇到"Code": "ResourceThrottled","Message": "Reserve resource exceeded limit"的错误信息,可能是当前地域的GPU显卡资源不足,建议您更换地域进行测试。
在store/progress.go中,上面返回值中参数含义如下。
// key 为 node id 的 map 对象 type TProgress map[string]TProgressNode type TProgressNode struct { Max int `json:"max"` // 进度的最大值 Value int `json:"value"` // 当前进度 Start int64 `json:"start"` // 开始时间 LastUpdated int64 `json:"last_updated"` // 最后一次更新时间 Images []TProgressNodeImage `json:"images"` // 当前节点输出的图片信息(路径) Results []string `json:"results,omitempty"` // 当前节点输出的图片 base64 }
出图请求(HTTP 异步)
请求路径:ComfyUI函数请求地址/api/run
。
Header:调用/api/run
接口,并且添加HTTP Header,X-Fc-Invocation-Type
:Async
,借助函数计算将请求转换为异步形式。
Body:json格式的prompt数据。
返回值:最后一次的进度,包含图片信息。
以ComfyUI函数的公网请求地址为例,通过Postman工具调用接口演示。
获取ComfyUI函数请求地址。在应用详情页,找到ComfyUI函数,点击进入。
在函数详情页,选择配置页签,点击触发器。选择公网访问地址进行复制。
在Postman工具界面,填写复制的公网地址,并在地址后拼接
/api/run
。选择请求方式为POST
,当需求为异步请求时,在Header添加X-Fc-Invocation-Type
:Async
和task-id
:值为唯一标识,其中X-Fc-Invocation-Type
告知函数计算为异步形式调用,task-id
为任意唯一值,记录当前任务唯一id,方便后续获取状态。填写请求参数,请求参数可参考Body.json,然后单击Send。返回202表示成功。
出图请求(WebSocket)
请求路径:wss://Host/api/run/ws
。
通过应用开始创作出图,通过浏览器检查功能分析发起的WebSocket出图请求。
以ComfyUI函数的公网地址为例,将公网地址的HTTPS替换为WSS并在后面追加
api/run/ws
。例如:公网地址为https://example.fcapp.run.run。则出图请求地址为:wss://example.fcapp.run/api/run/ws。在应用详情页,点击访问域名。
在ComfyUI界面依次完成STEP 1- 上传您的照片、STEP 2 - 选择角色和STEP 3 - 上传背景图的步骤,点击开始创作,通过浏览器检查工具查看
api/run/ws
请求。如下图,客户端请求服务器,发送一次JSON格式的prompt信息,其中①为请求参数,服务器返回客户端中间状态,②为返回结果。返回值中参数如下。
// key 为 node id 的 map 对象 type TProgress map[string]TProgressNode type TProgressNode struct { Max int `json:"max"` // 进度的最大值 Value int `json:"value"` // 当前进度 Start int64 `json:"start"` // 开始时间 LastUpdated int64 `json:"last_updated"` // 最后一次更新时间 Images []TProgressNodeImage `json:"images"` // 当前节点输出的图片信息(路径) Results []string `json:"results,omitempty"` // 当前节点输出的图片 base64 }
获取状态
路径:/api/run/ws?id=
请求参数:id的值等于task-id
的值。task-id的值可见异步请求task-id。
如果不关心出图进度 ,起另一个线程获取进度:使用 /api/run
+ /api/status
。
curl http://xxxxx/api/status?id=abcdefg -v
{"":{"max":0,"value":0,"start":0,"last_updated":1722234889,"images":null},"10":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"11":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"12":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"13":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"15":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"21":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"22":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"23":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"24":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"25":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"26":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"28":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"3":{"max":17,"value":17,"start":1722234848,"last_updated":1722234889,"images":null},"31":{"max":1,"value":0,"start":1722234848,"last_updated":1722234848,"images":null},"32":{"max":1,"value":0,"start":1722234848,"last_updated":1722234848,"images":null},"33":{"max":1,"value":0,"start":1722234844,"last_updated":1722234844,"images":null},"34":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"4":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"43":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"45":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"46":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"47":{"max":1,"value":0,"start":1722234848,"last_updated":1722234848,"images":null},"48":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"49":{"max":1,"value":1,"start":1722234846,"last_updated":1722234848,"images":null},"5":{"max":0,"value":0,"start":0,"last_updated":0,"images":null},"6":{"max":0,"value":0,"start":0,"last_updated":0,"imag* Connection #0 to host photo-b-comfyui-ibiwqxodsh.cn-hangzhou.fcapp.run left intact
es":null},"8":{"max":1,"value":0,"start":1722234889,"last_updated":1722234889,"images":null},"9":{"max":1,"value":0,"start":1722234889,"last_updated":1722234889,"images":[{"filename":"ComfyUI_00004_.png","subfolder":"","type":"output"}]}}
返回结果参数如下所示:
// key 为 node id 的 map 对象
type TProgress map[string]TProgressNode
type TProgressNode struct {
Max int `json:"max"` // 进度的最大值
Value int `json:"value"` // 当前进度
Start int64 `json:"start"` // 开始时间
LastUpdated int64 `json:"last_updated"` // 最后一次更新时间
Images []TProgressNodeImage `json:"images"` // 当前节点输出的图片信息(路径)
Results []string `json:"results,omitempty"` // 当前节点输出的图片 base64
}
状态存储
在src/images/agent/pkg/store/fs.go的代码中,我们实现了基于文件系统NAS的状态存储,您只需要挂载NAS系统,确保文件可被正常持久化,便可以在多个实例之间共享状态文件,确保可以正确拿到状态信息。
更好的做法是,将状态信息写入到OTS、MySQL等数据库中,您可以仿照fs.go实现Stroe接口针对其他数据库的实现即可。
// Store KV 数据存储
type Store interface {
// Save 存储 value 到 key
Save(key string, value string) error
// Load 从 key 加载 value
Load(key string) (string, error)
}
Output节点
目前,agent仅针对SaveImage节点做了特殊处理,提取其中的图片信息。对于特殊的业务需要,您可能需要更加定制化的工作流处理,可参考src/images/agent/pkg/server /run.go,如
增加更多对于Output的解析。
不解析图片节点,而是借助其他接口获取图片文件。
case "execution_error", "executed":
// 节点执行结束
log.Debugf("%s node %s finished", logPrefix, nodeid)
// 节点已完成时,修改下 Max 和 Value 至少为 1
if currentNodeProgress.Max == 0 && currentNodeProgress.Value == 0 {
currentNodeProgress.Max = 1
currentNodeProgress.Value = 1
}
if promptNode.ClassType == "SaveImage" && msg.Data.Output.Images != nil && len(msg.Data.Output.Images) > 0 {
// 如果是图片节点,则记录一下图片数据
if currentNodeProgress.Images == nil {
currentNodeProgress.Images = make([]store.TProgressNodeImage, 0, len(msg.Data.Output.Images))
}
for _, img := range msg.Data.Output.Images {
currentNodeProgress.Images = append(currentNodeProgress.Images, store.TProgressNodeImage{
Filename: img.Filename,
SubFolder: img.SubFolder,
Type: img.Type,
})
}
}
前端功能集成
与Agent对应,我们也给出了一份前端页面,可参考devsapp/fc-comfyui-couple-photo代码,我们针对ComfyUI的prompt做了一些特殊的约定,以适应自定义需要。如果您也希望创建专属的ComfyUI自定义页面提供给客户,可以参考相关的前端代码。
以【少年白马专属】破次元壁合照 AI 绘画平台应用为例,提供了预定义的prompt文件。
[
{
"title": "破次元壁合照",
"prompt": {},
"params": [
{
"type": "group",
"title": "STEP 1 - 上传您的照片",
"children": [
{
"type": "image",
"id": "10",
"key": "image",
"title": "参考图",
"description": "请上传您的照片,帮助模型理解您的样貌。请尽量选择背景简单、主体突出的半身照,不要佩戴墨镜、帽子等可能影响您特征的衣物。"
},
{
"type": "string",
"id": "24",
"key": "text",
"title": "参考形象描述",
"description": "为了确保模型更好地理解您的特点,您可以使用提示词来加强模型对您的印象(请使用因为描述)。"
}
]
},
{
"type": "image",
"id": "11",
"key": "image",
"title": "STEP 2 - 选择角色",
"description": "请选择您希望合照的角色。",
"options": [
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/百里东君.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/司空长风.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/玥瑶.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/叶鼎之.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/易文君.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/南宫春水.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/萧若风.png"
]
},
{
"type": "image",
"id": "12",
"key": "image",
"title": "STEP 3 - 上传背景图",
"description": "请上传您期望的合影地点的图片,这将作为背景图片的参考。"
}
]
},
{
"title": "背景替换",
"prompt": {},
"params": [
{
"type": "image",
"id": "10",
"key": "image",
"title": "STEP 1 - 选择角色",
"description": "请选择您希望合照的角色。",
"options": [
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/百里东君.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/司空长风.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/玥瑶.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/叶鼎之.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/易文君.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/南宫春水.png",
"https://serverless-tool-images.oss-cn-hangzhou.aliyuncs.com/aigc/json/couple/萧若风.png"
]
},
{
"type": "image",
"id": "12",
"key": "image",
"title": "STEP 2 - 上传背景图",
"description": "请上传您期望的合影地点的图片,这将作为背景图片的参考。"
}
]
}
]
在src/web/src/utils/api.ts代码中通过参数字段,约定了如何渲染页面并允许用户填写自己的参数。
export type ComfyUIPromptEditPanel = {
type: 'image' | 'select' | 'number' | 'string' | 'group'; // 数据类型
id?: string; // 对应 prompt 中的 node id
key: string; // 要修改的参数
title: string; // 标题
description?: string; // 描述
options?: string[] | string; // 可选项
min?: number; // 最小值
max?: number; // 最大值
step?: number; // 调整步数
hidden?: boolean; // 是否隐藏
children?: ComfyUIPromptEditPanel[]; // group 类型的子节点
};
一些其他约定如果seed字段为 -1,则会被替换为随机数。
// 处理下 seed 字段
let prompt_with_seed = JSON.parse(JSON.stringify(prompt));
for (const nodeid of Object.keys(prompt_with_seed)) {
if (prompt_with_seed[nodeid]?.inputs?.seed === -1) {
prompt_with_seed[nodeid].inputs.seed = random();
}
}
清理资源
登录函数计算控制台,在左侧导航栏,单击应用。
在应用页面,找到目标应用,单击右侧操作列的删除应用。
在弹出的对话框,勾选我已确定资源删除的风险,依旧要删除上面已选择的资源,然后单击删除应用及所选资源。