ComfyUI API Serverless版解决方案

方案概览

ComfyUI结合多样的自定义节点,能够批量生成图像,一键加载大量工作流,可以轻松实现人像生成、背景替换、风格迁移和图像动画化等功能。

当您想要独立部署和使用ComfyUI,您可能面临显卡成本高、购买难、并发处理能力有限以及使用门槛高等问题。函数计算推出了ComfyUI API Serverless版解决方案,帮助您通过函数计算快速部署基于ComfyUI模型的AI服务。使用本方案可以充分利用ComfyUI和Serverless技术优势,为广大开发者AI绘画创业及变现提供思路。

为了便于用户直观体验ComfyUI Serverless API解决方案的效果,函数计算基于此方案搭建一个【少年白马专属】破次元壁合照 AI 绘画平台应用,供用户快速体验生成AI绘画。

image

本方案的技术架构包括以下基础设施和云服务。

函数计算:用于提供ComfyUI模型的应用服务,提供GPU和CPU算力。

部署应用

  1. 登录函数计算3.0控制台,在左侧导航栏,单击应用

    当左上角显示函数计算 FC 3.0时,表示当前控制台为3.0控制台。

    重要

    ComfyUI模型的应用只在函数计算 FC 3.0支持,如果您登录的是函数计算 2.0的控制台,请点击右上角的体验函数计算 3.0进行切换。

    image

    image

  2. 在应用页面,点击创建应用,选择通过模板创建应用,在人工智能页签找到【少年白马专属】破次元壁合照 AI 绘画平台光标移至该卡片,然后单击立即创建

    image

  3. 在创建应用页面,设置以下配置项,然后单击创建应用

    重点配置项说明如下,如果您没有特殊要求,其余配置项保持默认值即可。

    配置项名称

    说明

    示例值

    角色名

    创建应用所需的权限。首次创建应用的用户,需要单击前往授权配置角色权限。

    image

    image

    AliyunFCServerlessDevsRole

    地域

    地域选择可以选择距离自己较近的区域,目前支持华东1(杭州)、华东2(上海)、日本(东京)。

    由于当前模板涉及GitHub以及HuggingFace等网站的访问,国内部分地域可能无法直接使用。

    日本(东京)

    命名空间

    设置命名空间名称。如果您第一次创建ComfyUI应用,使用默认值即可;如果非第一次创建ComfyUI应用,建议手动设置命名空间名称,便于和其他应用区分。

    ComfyUI-api

  4. 在弹出的对话框,仔细阅读应用创建提醒信息,勾选涉及的计费项和我已经了解上面的内容,并同意上述描述,然后单击同意并继续部署

    image

通过API接口调用ComfyUI解决方案

常规的ComfyUI出图的流程调用/prompt接口。在并发请求数比较大的情况下,我们往往期望可以利用Serverless的弹性优势,动态创建多个函数实例处理出图任务。但由于ComfyUI本身是“有状态”的,难以确保出图的请求和获取状态的请求固定打到同一个实例上,为了让ComfyUI更加适配Serverless模式,请参考fc-comfyui/src/images/agent的代码,在ComfyUI镜像里内置Agent程序,负责转换ComfyUI请求并且拉起ComfyUI,以HTTP同步请求举例如下图。

重要

以上提供的Agent代码作为Serverless方式调用的实践参考。

功能未经过严格测试,请根据实际的业务需要二次开发或调整相关的代码,并构建ComfyUI镜像。

image

目前提供的Agent能力介绍

在应用详情页,找到ComfyUI函数,点击进入。

image

  • 开启Agent能力,需要增加USE_AGENT:true环境变量。更多详见配置环境变量

    说明

    【少年白马专属】破次元壁合照 AI 绘画平台应用作为示例,已经默认配置了USE_AGENT:true环境变量,如果您的代码进行二次开发,并使用ComfyUI镜像里内置Agent程序,可在环境变量进行配置。

    image

  • 当通过Agent的API调用时,建议您调整实例并发数为:1~5,确保并发请求尽量使用单独的实例,提高出图效率。

    image

出图请求(HTTP 同步)

请求路径:ComfyUI函数请求地址/api/run

Body:json格式的prompt数据。

返回值:最后一次的进度,包含图片信息。

以ComfyUI函数的公网请求地址为例,通过Postman工具调用接口演示。

  1. 获取ComfyUI函数请求地址。在应用详情页,找到ComfyUI函数,点击进入。

    image

  2. 在函数详情页,选择配置页签,点击触发器。选择公网访问地址进行复制。

    image

  3. 在Postman工具界面,填写复制公网地址,并拼接/api/run。选择请求方式为POST,填写请求参数,请求参数可参考Body.json,然后单击Send。返回200表示调用成功。

    image

    说明

    如果您在测试调用的过程中,遇到"Code": "ResourceThrottled","Message": "Reserve resource exceeded limit"的错误信息,可能是当前地域的GPU显卡资源不足,建议您更换地域进行测试。

  4. 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-TypeAsync,借助函数计算将请求转换为异步形式。

Body:json格式的prompt数据。

返回值:最后一次的进度,包含图片信息。

以ComfyUI函数的公网请求地址为例,通过Postman工具调用接口演示。

  1. 获取ComfyUI函数请求地址。在应用详情页,找到ComfyUI函数,点击进入。

    image

  2. 在函数详情页,选择配置页签,点击触发器。选择公网访问地址进行复制。

    image

  3. 在Postman工具界面,填写复制的公网地址,并在地址后拼接/api/run。选择请求方式为POST,当需求为异步请求时,在Header添加X-Fc-Invocation-TypeAsynctask-id:值为唯一标识,其中X-Fc-Invocation-Type告知函数计算为异步形式调用task-id为任意唯一值,记录当前任务唯一id,方便后续获取状态。填写请求参数,请求参数可参考Body.json,然后单击Send。返回202表示成功。

    image

    image

出图请求(WebSocket)

请求路径:wss://Host/api/run/ws

通过应用开始创作出图,通过浏览器检查功能分析发起的WebSocket出图请求。

  1. 以ComfyUI函数的公网地址为例,将公网地址的HTTPS替换为WSS并在后面追加api/run/ws。例如:公网地址为https://example.fcapp.run.run。则出图请求地址为:wss://example.fcapp.run/api/run/ws。

    image

  2. 在应用详情页,点击访问域名。

    image

  3. 在ComfyUI界面依次完成STEP 1- 上传您的照片、STEP 2 - 选择角色STEP 3 - 上传背景图的步骤,点击开始创作,通过浏览器检查工具查看api/run/ws请求。如下图,客户端请求服务器,发送一次JSON格式的prompt信息,其中①为请求参数,服务器返回客户端中间状态,②为返回结果。

    image

    image

  4. 返回值中参数如下。

    // 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
    }
    

    image

获取状态

路径:/api/run/ws?id=

请求参数:id的值等于task-id的值。task-id的值可见异步请求task-id

如果不关心出图进度 ,起另一个线程获取进度:使用 /api/run + /api/status

重要

当选择 /api/run + /api/status 方式时,您需要挂载一个NAS实例或改造代码将状态存放至OTS等数据库,否则在多实例时无法获取进度。更多详见配置NAS文件系统访问数据库服务

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();
    }
  }

清理资源

  1. 登录函数计算控制台,在左侧导航栏,单击应用

  2. 在应用页面,找到目标应用,单击右侧操作列的删除应用

  3. 在弹出的对话框,勾选我已确定资源删除的风险,依旧要删除上面已选择的资源,然后单击删除应用及所选资源

    image