工作流java使用教程:新手完成第一张作品的方法

技术教程8小时前发布 jdkjadmin
0 00
工作流java使用教程:新手完成第一张作品的方法

工作流java使用教程:新手完成第一张作品的方法

本文所说的“工作流 Java”,是指使用 Java 程序调用工作流服务,完成一张图片的生成与下载。示例选择 ComfyUI:它把模型加载、提示词编码、采样和图片保存组织成节点工作流,并提供 HTTP API。Java 负责提交工作流、等待结果、取回图片;实际推理由 ComfyUI 及其配置的模型完成。

如果你想在 Java 中直接编写图形节点编辑器,或寻找一个名为“工作流 Java”的特定产品,本文并不覆盖那类实现。这里的目标很具体:在本机启动 ComfyUI,使用 Java 提交一个最小图片工作流,得到第一张作品。

一、适用场景与边界

这种组合适合已有 Java 服务需要调用图像生成能力的情况,例如内部创作工具、批量素材任务或原型验证。它也便于把生成流程拆成可复用的节点图,再由 Java 传入提示词、尺寸等参数。

它不等于把模型嵌入 Java 程序。ComfyUI 仍需单独运行,模型文件、显存、Python 环境和节点依赖也由它管理。若只是个人尝试,先用 ComfyUI 自带界面跑通工作流通常更容易定位问题;若要承载多人或高并发请求,还需额外设计队列、限流、超时、鉴权和资源隔离,不能把下面的单机示例直接当作生产服务。

示例采用 ComfyUI 常见的本地 HTTP API:向 /prompt 提交 API 格式工作流,随后查询 /history/{prompt_id},再从 /view 下载结果。不同版本、扩展节点和部署方式可能带来差异;如接口响应结构不符,应以当前服务版本及其 API 文档为准。

二、准备运行环境

  1. 安装并启动 ComfyUI,确认它能在界面中生成图片。默认本机地址通常是 http://127.0.0.1:8188;如果启动参数改过监听地址或端口,后文代码也要相应修改。
  2. 在 ComfyUI 可用的模型目录中放入兼容的 checkpoint,并记下文件名。模型许可、适用范围和生成效果取决于具体模型,不由 Java 或工作流自动保证。
  3. 在界面载入模型,先用一条简单提示词生成一次。如果界面本身都无法完成生成,先检查模型、节点和显存,不要急着排查 Java。
  4. 准备 JDK 11 或更高版本。下面使用 Java 11 的 HttpClient,并用 Jackson 处理 JSON。

在 ComfyUI 界面中创建工作流并使用“保存(API 格式)”或相应的 API 工作流导出功能,可以获得提交给 /prompt 的节点图。普通界面工作流 JSON 与 API 格式不一定相同:API 格式通常以节点 ID 为键,每个节点包含 class_type 和 inputs。不要把任意界面导出文件原样当成 API 工作流提交。

三、先理解最小工作流

本文的工作流包含六个节点:加载 checkpoint、编码正向提示词、编码负向提示词、创建空 latent、采样、解码并保存图片。节点间通过 ["节点ID", 输出序号] 引用上游结果。下面的 JSON 只是结构示意,模型文件名需要替换成你本机实际可用的名称;若模型或 ComfyUI 版本不接受某项输入,请先在界面生成 API 格式工作流,再按本机导出的结构调整。

{
  "1": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": {
      "ckpt_name": "your-model.safetensors"
    }
  },
  "2": {
    "class_type": "CLIPTextEncode",
    "inputs": {
      "text": "a small red fox in a quiet forest, illustration",
      "clip": ["1", 1]
    }
  },
  "3": {
    "class_type": "CLIPTextEncode",
    "inputs": {
      "text": "blurry, low quality",
      "clip": ["1", 1]
    }
  },
  "4": {
    "class_type": "EmptyLatentImage",
    "inputs": {
      "width": 512,
      "height": 512,
      "batch_size": 1
    }
  },
  "5": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 123456789,
      "steps": 20,
      "cfg": 7.0,
      "sampler_name": "euler",
      "scheduler": "normal",
      "denoise": 1.0,
      "model": ["1", 0],
      "positive": ["2", 0],
      "negative": ["3", 0],
      "latent_image": ["4", 0]
    }
  },
  "6": {
    "class_type": "VAEDecode",
    "inputs": {
      "samples": ["5", 0],
      "vae": ["1", 2]
    }
  },
  "7": {
    "class_type": "SaveImage",
    "inputs": {
      "filename_prefix": "java_first_art",
      "images": ["6", 0]
    }
  }
}

节点编号是工作流内部标识,不是固定值。采样器名称、调度器名称、尺寸、步数和提示词效果都可能受模型、节点版本及硬件影响。示例中的 512×512、20 步只是便于入门的示例设置,不是所有模型的推荐值或硬性上限。高分辨率和更大的批量通常会增加显存及运行时间;具体可用范围应根据模型说明和本机资源逐步测试。

四、创建 Java 项目

下面给出一个 Maven 项目。Jackson 只负责 JSON 编解码;发请求使用 JDK 自带的 HTTP 客户端。创建 pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>comfyui-java-demo</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>11</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.2</version>
    </dependency>
  </dependencies>
</project>

版本号是一个明确的依赖示例;在实际项目中,应依据团队依赖策略选择并维护 Jackson 版本。将下面代码保存为 src/main/java/example/ComfyUiFirstImage.java。把 MODEL 改成 ComfyUI 能识别的 checkpoint 文件名。

五、可运行示例:提交、轮询并下载图片

package example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;

public class ComfyUiFirstImage {
    private static final String BASE = "http://127.0.0.1:8188";
    private static final String MODEL = "your-model.safetensors";

    private static final ObjectMapper JSON = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    public static void main(String[] args) throws Exception {
        ObjectNode workflow = buildWorkflow();
        ObjectNode requestBody = JSON.createObjectNode();
        requestBody.put("client_id", UUID.randomUUID().toString());
        requestBody.set("prompt", workflow);

        HttpResponse<String> submitted = send(
                HttpRequest.newBuilder(URI.create(BASE + "/prompt"))
                        .timeout(Duration.ofSeconds(30))
                        .header("Content-Type", "application/json")
                        .POST(HttpRequest.BodyPublishers.ofString(
                                JSON.writeValueAsString(requestBody)))
                        .build());

        JsonNode submitJson = JSON.readTree(submitted.body());
        if (submitJson.has("error")) {
            throw new IllegalStateException("工作流提交失败:" + submitJson);
        }
        JsonNode promptIdNode = submitJson.get("prompt_id");
        if (promptIdNode == null) {
            throw new IllegalStateException("响应中没有 prompt_id:" + submitted.body());
        }

        String promptId = promptIdNode.asText();
        System.out.println("已提交任务:" + promptId);

        JsonNode history = waitForHistory(promptId);
        JsonNode outputs = history.path(promptId).path("outputs");
        JsonNode image = findFirstImage(outputs);
        if (image == null) {
            throw new IllegalStateException(
                    "任务完成,但历史记录中没有找到图片输出:" + outputs);
        }

        String filename = image.path("filename").asText();
        String subfolder = image.path("subfolder").asText("");
        String type = image.path("type").asText("output");

        String query = "filename=" + enc(filename)
                + "&subfolder=" + enc(subfolder)
                + "&type=" + enc(type);
        HttpResponse<byte[]> downloaded = HTTP.send(
                HttpRequest.newBuilder(URI.create(BASE + "/view?" + query))
                        .timeout(Duration.ofSeconds(60))
                        .GET()
                        .build(),
                HttpResponse.BodyHandlers.ofByteArray());
        checkStatus(downloaded.statusCode(), new String(
                downloaded.body(), StandardCharsets.UTF_8));

        Path output = Path.of("first-art.png");
        Files.write(output, downloaded.body());
        System.out.println("图片已保存到:" + output.toAbsolutePath());
    }

    private static ObjectNode buildWorkflow() {
        ObjectNode graph = JSON.createObjectNode();

        ObjectNode loader = node(graph, "1", "CheckpointLoaderSimple");
        loader.putObject("inputs").put("ckpt_name", MODEL);

        ObjectNode positive = node(graph, "2", "CLIPTextEncode");
        ObjectNode posInputs = positive.putObject("inputs");
        posInputs.put("text",
                "a small red fox in a quiet forest, warm morning light, illustration");
        posInputs.set("clip", link("1", 1));

        ObjectNode negative = node(graph, "3", "CLIPTextEncode");
        ObjectNode negInputs = negative.putObject("inputs");
        negInputs.put("text", "blurry, low quality");
        negInputs.set("clip", link("1", 1));

        ObjectNode latent = node(graph, "4", "EmptyLatentImage");
        ObjectNode latentInputs = latent.putObject("inputs");
        latentInputs.put("width", 512);
        latentInputs.put("height", 512);
        latentInputs.put("batch_size", 1);

        ObjectNode sampler = node(graph, "5", "KSampler");
        ObjectNode sampleInputs = sampler.putObject("inputs");
        sampleInputs.put("seed", 123456789);
        sampleInputs.put("steps", 20);
        sampleInputs.put("cfg", 7.0);
        sampleInputs.put("sampler_name", "euler");
        sampleInputs.put("scheduler", "normal");
        sampleInputs.put("denoise", 1.0);
        sampleInputs.set("model", link("1", 0));
        sampleInputs.set("positive", link("2", 0));
        sampleInputs.set("negative", link("3", 0));
        sampleInputs.set("latent_image", link("4", 0));

        ObjectNode decode = node(graph, "6", "VAEDecode");
        ObjectNode decodeInputs = decode.putObject("inputs");
        decodeInputs.set("samples", link("5", 0));
        decodeInputs.set("vae", link("1", 2));

        ObjectNode save = node(graph, "7", "SaveImage");
        ObjectNode saveInputs = save.putObject("inputs");
        saveInputs.put("filename_prefix", "java_first_art");
        saveInputs.set("images", link("6", 0));

        return graph;
    }

    private static ObjectNode node(ObjectNode graph, String id, String type) {
        ObjectNode value = JSON.createObjectNode();
        value.put("class_type", type);
        graph.set(id, value);
        return value;
    }

    private static com.fasterxml.jackson.databind.node.ArrayNode link(
            String nodeId, int outputIndex) {
        com.fasterxml.jackson.databind.node.ArrayNode value =
                JSON.createArrayNode();
        value.add(nodeId);
        value.add(outputIndex);
        return value;
    }

    private static JsonNode waitForHistory(String promptId) throws Exception {
        long deadline = System.nanoTime() + Duration.ofMinutes(10).toNanos();
        String url = BASE + "/history/" + enc(promptId);

        while (System.nanoTime() < deadline) {
            HttpResponse<String> response = send(
                    HttpRequest.newBuilder(URI.create(url))
                            .timeout(Duration.ofSeconds(30))
                            .GET()
                            .build());

            JsonNode root = JSON.readTree(response.body());
            JsonNode result = root.get(promptId);
            if (result != null) {
                JsonNode status = result.path("status");
                if (status.path("status_str").asText().equals("error")) {
                    throw new IllegalStateException("ComfyUI 执行失败:" + result);
                }
                if (result.has("outputs")) {
                    return root;
                }
            }
            Thread.sleep(1000);
        }
        throw new IllegalStateException("等待任务超时;可检查 ComfyUI 队列和服务日志");
    }

    private static JsonNode findFirstImage(JsonNode outputs) {
        var nodes = outputs.elements();
        while (nodes.hasNext()) {
            JsonNode images = nodes.next().path("images");
            if (images.isArray() && !images.isEmpty()) {
                return images.get(0);
            }
        }
        return null;
    }

    private static HttpResponse<String> send(HttpRequest request)
            throws Exception {
        HttpResponse<String> response = HTTP.send(
                request, HttpResponse.BodyHandlers.ofString());
        checkStatus(response.statusCode(), response.body());
        return response;
    }

    private static void checkStatus(int code, String body) {
        if (code < 200 || code >= 300) {
            throw new IllegalStateException(
                    "HTTP 请求失败,状态码 " + code + ",响应:" + body);
        }
    }

    private static String enc(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}

运行前检查:服务地址与端口正确;MODEL 的文件名和服务端模型清单一致;工作流所需节点可用。使用 Maven 编译运行时,可执行:

mvn -q compile
mvn -q dependency:build-classpath -Dmdep.outputFile=classpath.txt
java -cp "target/classes:$(cat classpath.txt)" example.ComfyUiFirstImage

上面的 classpath 命令适用于 macOS/Linux shell。Windows 命令行的路径分隔符和命令替换写法不同;也可以在 IDE 中导入 Maven 项目后运行主类。成功后,图片会保存到运行程序的当前目录,文件名为 first-art.png。

六、代码做了什么

  • 提交任务:程序把工作流放进请求体的 prompt 字段,并生成 client_id。正常响应中会返回 prompt_id;若是 HTTP 错误或响应缺少任务编号,程序直接报错。
  • 轮询状态:示例每秒查询一次历史记录,最多等待十分钟。生成耗时受硬件、模型、队列长度和工作流影响,十分钟只是客户端等待上限,不代表服务端保证在该时间内完成。正式服务应把等待时间做成配置,并考虑取消任务和退避重试。
  • 提取输出:程序遍历历史记录中的输出节点,找到第一张图片,再用返回的文件名、子目录和类型请求 /view。输出节点不一定只有一个;这里仅取第一张,方便完成入门流程。
  • 保存文件:下载内容按字节写入磁盘。示例文件名固定,重复运行会覆盖当前目录中的同名文件;需要保留多次结果时,应使用任务 ID 或时间戳生成文件名。

历史接口在不同版本或执行状态下,响应字段可能有差异。示例使用常见的任务 ID、outputs 和图片元数据结构。如果任务确已完成但代码提示没有图片,不要直接断定模型失败:先查看服务端历史记录、输出目录和版本对应的 API 响应,再调整解析逻辑。

七、参数怎么改才不容易踩坑

提示词与负面提示词

正向提示词描述希望得到的内容,负向提示词表达希望模型避免的特征,但它们不是严格约束,也不能保证每次生成都精确遵守。不同模型对提示词的训练和解释方式不同。先从单一主体、明确场景和简洁风格开始,逐步调整;一次改动多个因素,很难判断效果变化来自哪里。

宽度、高度与批量

EmptyLatentImage 的宽高以像素表示,批量大小表示一次生成的 latent 数量。数值需要符合所用模型和工作流的要求,资源占用也会随尺寸、批量和模型变化。某些模型或节点对尺寸有额外偏好或限制,不能仅凭示例中的 512 推断适用于所有模型。第一次运行建议使用较小尺寸和批量 1,确认链路后再提高。

seed、steps、cfg 与采样器

seed 是随机种子。固定种子有助于在条件相近时进行对比,但不保证跨模型、软件版本、硬件或不同工作流得到完全相同的像素结果。steps 是采样步数;步数增加会影响耗时和效果,但并非越高越好。cfg 控制提示词引导相关行为,具体表现因模型而异。sampler_name 与 scheduler 必须是当前环境支持的值,不能假设所有安装都具有相同选项。

denoise 在这个从空 latent 开始的示例中设为 1.0。图生图工作流会有不同的 latent 输入和降噪设置,不能只把这里的数值改掉就认为工作流已经变成图生图。

八、常见错误与排查顺序

  1. 连接被拒绝:先在浏览器或命令行访问本机服务地址,确认 ComfyUI 正在运行、端口未改变。Java 与 ComfyUI 若在不同容器或主机中,127.0.0.1 指向的不是同一台机器;应使用正确的服务地址和容器网络配置。
  2. HTTP 400 或工作流校验失败:检查提交的是 API 格式;节点 ID 引用、输出序号、输入字段和节点类型都要与当前节点定义匹配。最稳妥的起点是让界面生成一次 API 工作流,再只修改提示词、模型名等已知字段。
  3. 模型找不到:确认模型已放到服务可扫描的位置,文件名拼写与 ckpt_name 完全一致,并在 ComfyUI 中刷新或重启模型列表。路径不是任意本机路径字符串,通常应填写节点下拉列表显示的模型名称。
  4. 找不到 sampler 或节点:检查名称是否来自当前安装版本的选项列表。自定义节点缺失、节点升级或工作流依赖不全,都会导致节点不可用;不要只修改 Java 代码来掩盖服务端缺少依赖的问题。
  5. 显存不足或生成失败:先降低宽高和批量,关闭其他占用显存的任务,再查看 ComfyUI 控制台日志。不要把客户端超时误认为服务端已停止计算;请求超时后应核对队列和历史状态,避免重复提交造成额外负载。
  6. 任务有结果但下载失败:核对历史输出中的 filename、subfolder 和 type,并确保 Java 请求的是同一个 ComfyUI 实例。不要自行拼接未经编码的文件名到 URL;示例使用 UTF-8 URL 编码处理查询参数。
  7. 运行时找不到 Jackson:确认 Maven 依赖已下载,并在 IDE 中以 Maven 项目导入。直接使用 java -cp target/classes 不会自动包含第三方依赖。

九、安全与上线边界

本例假设 ComfyUI 只在可信本机环境使用。开发者不应把没有访问控制的生成服务直接暴露到公网,也不应将开放接口交给不可信用户任意提交工作流。未授权的请求可能消耗 GPU、磁盘和网络资源;具体风险取决于服务部署、接口配置及可用节点。若必须远程访问,应在受控网络或经过身份认证的服务后使用,并配置访问控制、限流、请求体大小限制和资源监测。不要把管理接口的可访问性误当成安全认证。

生产环境中还应限制输出文件的保存期限和可下载范围,避免把用户输入直接拼成文件路径或命令;Java 应对服务端响应进行校验,记录必要的任务 ID 和错误摘要,避免日志泄露敏感提示词或凭据。若通过反向代理访问,需确认代理转发规则和超时设置适合较长任务。具体的鉴权与部署配置随 ComfyUI 版本、代理和部署架构而异,本文示例没有提供,也不能声称默认接口具备身份认证。

使用模型生成内容时,还要遵守所用模型及素材的许可条款、平台规则和适用法律。模型输出并不自动获得可商用、可公开发布或不侵权的保证;涉及人物、商标、受保护作品或敏感主题时,应单独评估风险。工作流和 Java 客户端只是调用与处理工具,不会替代内容审核、授权确认或安全策略。

十、完成第一张作品之后

先确认这条最小链路完整:ComfyUI 能单独生成;Java 能成功提交并取得任务 ID;历史记录能找到图片;Java 能下载并在本地打开文件。之后再把提示词、尺寸、seed 和模型名改为命令行参数或配置项,按业务需要增加任务队列、并发控制和错误处理。每次只改变一项,保存工作流与运行记录,能让结果复现和问题排查都更容易。

用 Java 调用工作流的核心并不神秘:提交符合服务端要求的图,等待异步任务完成,再按服务返回的元数据取回产物。真正需要谨慎的是模型与节点的兼容性、资源消耗,以及接口暴露后的安全边界。把这些边界厘清,第一张作品就不只是“跑出来”,也能成为后续开发的可靠起点。

© 版权声明

相关文章