Skip to content

Instantly share code, notes, and snippets.

@kishida
Last active October 9, 2025 20:33
Show Gist options
  • Select an option

  • Save kishida/0d4bd9d3a937a1383e7c2295fea88ef3 to your computer and use it in GitHub Desktop.

Select an option

Save kishida/0d4bd9d3a937a1383e7c2295fea88ef3 to your computer and use it in GitHub Desktop.

Revisions

  1. kishida revised this gist Oct 9, 2025. 1 changed file with 154 additions and 0 deletions.
    154 changes: 154 additions & 0 deletions qwen_image_gguf.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,154 @@
    {
    "3": {
    "inputs": {
    "seed": 123,
    "steps": 8,
    "cfg": 2.5,
    "sampler_name": "euler",
    "scheduler": "simple",
    "denoise": 1,
    "model": [
    "66",
    0
    ],
    "positive": [
    "6",
    0
    ],
    "negative": [
    "7",
    0
    ],
    "latent_image": [
    "58",
    0
    ]
    },
    "class_type": "KSampler",
    "_meta": {
    "title": "Kサンプラー"
    }
    },
    "6": {
    "inputs": {
    "text": "cat",
    "clip": [
    "38",
    0
    ]
    },
    "class_type": "CLIPTextEncode",
    "_meta": {
    "title": "CLIP Text Encode (Positive Prompt)"
    }
    },
    "7": {
    "inputs": {
    "text": "",
    "clip": [
    "38",
    0
    ]
    },
    "class_type": "CLIPTextEncode",
    "_meta": {
    "title": "CLIP Text Encode (Negative Prompt)"
    }
    },
    "8": {
    "inputs": {
    "samples": [
    "3",
    0
    ],
    "vae": [
    "39",
    0
    ]
    },
    "class_type": "VAEDecode",
    "_meta": {
    "title": "VAEデコード"
    }
    },
    "38": {
    "inputs": {
    "clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
    "type": "qwen_image",
    "device": "default"
    },
    "class_type": "CLIPLoader",
    "_meta": {
    "title": "CLIPを読み込む"
    }
    },
    "39": {
    "inputs": {
    "vae_name": "qwen_image_vae.safetensors"
    },
    "class_type": "VAELoader",
    "_meta": {
    "title": "VAEを読み込む"
    }
    },
    "58": {
    "inputs": {
    "width": 512,
    "height": 512,
    "batch_size": 1
    },
    "class_type": "EmptySD3LatentImage",
    "_meta": {
    "title": "空のSD3潜在画像"
    }
    },
    "60": {
    "inputs": {
    "filename_prefix": "ComfyUI",
    "images": [
    "8",
    0
    ]
    },
    "class_type": "SaveImage",
    "_meta": {
    "title": "画像を保存"
    }
    },
    "66": {
    "inputs": {
    "shift": 3.1000000000000005,
    "model": [
    "73",
    0
    ]
    },
    "class_type": "ModelSamplingAuraFlow",
    "_meta": {
    "title": "モデルサンプリングオーラフロー"
    }
    },
    "73": {
    "inputs": {
    "lora_name": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
    "strength_model": 1,
    "model": [
    "76",
    0
    ]
    },
    "class_type": "LoraLoaderModelOnly",
    "_meta": {
    "title": "LoRAローダーモデルのみ"
    }
    },
    "76": {
    "inputs": {
    "unet_name": "Qwen_Image-Q3_K_M.gguf"
    },
    "class_type": "UnetLoaderGGUF",
    "_meta": {
    "title": "Unet Loader (GGUF)"
    }
    }
    }
  2. kishida created this gist Oct 9, 2025.
    303 changes: 303 additions & 0 deletions ComfyUIClient.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,303 @@
    package naoki.vcc;

    import java.io.*;
    import java.net.URI;
    import java.net.URLEncoder;
    import java.net.http.*;
    import java.net.http.WebSocket;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.util.*;
    import java.util.concurrent.*;

    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.node.*;
    import java.nio.file.Path;

    public class ComfyUIClient implements AutoCloseable {

    private static final String SERVER_ADDRESS = "localhost:8188";
    private static final String CLIENT_ID = "my_client_id";
    private static final String WORKFLOW = "qwen_image_gguf.json";

    private final ObjectNode promptData;
    private WebSocket ws;
    private final HttpClient client;
    private static final ObjectMapper mapper = new ObjectMapper();
    private ComfyHandler comfyHandler;
    private final Map<String, GenerationHandler> handlers = new HashMap<>();
    private final Random rand = new Random();

    public interface ComfyHandler {
    void onOpen();
    void onSid(String sid);
    void onQueueStatus(int remaining);
    void onClose(int statusCode, String reason);
    void onError(Throwable error);
    }

    public interface GenerationHandler {
    void onStart();
    void onFinish(byte[] imageData);
    void onProgress(int value, int max);
    void onError(IOException ex);
    }

    public ComfyUIClient() throws IOException{
    this(null);
    }

    public ComfyUIClient(ComfyHandler handler) throws IOException{
    this.comfyHandler = handler;
    client = HttpClient.newHttpClient();
    // プロンプトJSON読み込み
    promptData = loadPromptJson(WORKFLOW);

    // WebSocket接続開始
    startWebSocket();

    }
    public void startGenerate(String positivePrompt, String negativePrompt, GenerationHandler handler) throws UncheckedIOException{
    startGenerate(positivePrompt, negativePrompt, rand.nextInt(9_999_999), handler);
    }
    public void startGenerate(String positivePrompt, String negativePrompt, int seed, GenerationHandler handler) throws UncheckedIOException{
    try {
    var data = createPromptData(promptData, positivePrompt, negativePrompt, seed);
    var id = comfyUIPrompt(data);
    synchronized (handlers) {
    handlers.put(id, handler);
    }
    } catch (IOException ex) {
    throw new UncheckedIOException(ex);
    }
    }

    @Override
    public void close() {
    if (ws != null) ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye");
    }

    /** WebSocket接続 */
    private void startWebSocket() {
    CountDownLatch wsConnected = new CountDownLatch(1);

    client.newWebSocketBuilder()
    .buildAsync(URI.create("ws://" + SERVER_ADDRESS + "/ws?clientId=" + CLIENT_ID),
    new WSListener())
    .thenAccept(socket -> {
    ws = socket;
    wsConnected.countDown();
    });

    try {
    wsConnected.await();
    } catch (InterruptedException e) {
    if (comfyHandler != null) comfyHandler.onError(e);
    }
    }

    /** prompt.json 読み込み */
    private ObjectNode loadPromptJson(String path) throws IOException {
    return (ObjectNode) mapper.readTree(Files.readString(Path.of(path), StandardCharsets.UTF_8));
    }

    /** プロンプト更新 */
    private ObjectNode createPromptData(ObjectNode n, String positive, String negative, int seed) {
    ObjectNode prompt = n.deepCopy();
    Iterator<String> fields = prompt.fieldNames();
    while (fields.hasNext()) {
    String key = fields.next();
    JsonNode node = prompt.get(key);
    if (!(node instanceof ObjectNode obj)) continue;

    String classType = obj.path("class_type").asText();
    ObjectNode inputs = (ObjectNode) obj.path("inputs");

    if ("KSampler".equals(classType) && inputs != null) {
    if (inputs.has("seed")) {
    inputs.put("seed", seed);
    System.out.println("🛠️ " + key + " の seed を " + seed + " に設定");
    }

    JsonNode posRef = inputs.get("positive");
    if (posRef != null && posRef.isArray() && posRef.size() > 0) {
    String refId = posRef.get(0).asText();
    JsonNode refNode = prompt.get(refId);
    if (refNode instanceof ObjectNode refObj) {
    ObjectNode refInputs = (ObjectNode) refObj.path("inputs");
    refInputs.put("text", positive);
    System.out.println("📝 Positiveプロンプト: " + positive);
    }
    }
    JsonNode negRef = inputs.get("negative");
    if (negRef != null && negRef.isArray() && negRef.size() > 0) {
    String refId = negRef.get(0).asText();
    JsonNode refNode = prompt.get(refId);
    if (refNode instanceof ObjectNode refObj) {
    ObjectNode refInputs = (ObjectNode) refObj.path("inputs");
    refInputs.put("text", negative);
    System.out.println("📝 Negativeプロンプト: " + negative);
    }
    }

    }
    }
    return prompt;
    }


    /** ComfyUI: プロンプト送信 */
    private String comfyUIPrompt(ObjectNode prompt) throws IOException{
    try {
    ObjectNode payload = mapper.createObjectNode();
    payload.put("client_id", CLIENT_ID);
    payload.set("prompt", prompt);

    HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://" + SERVER_ADDRESS + "/prompt"))
    .POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
    .header("Content-Type", "application/json")
    .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("📥 サーバー応答: " + response.body());

    JsonNode body = mapper.readTree(response.body());
    String id = body.path("prompt_id").asText(null);
    System.out.println(" ⇨ prompt_id: " + id);
    return id;
    } catch ( InterruptedException e) {
    System.out.println("❌ workflow送信エラー: " + e.getMessage());
    return null;
    }
    }

    /** ComfyUI: 画像取得 */
    private byte[] comfyUIView(String filename, String type, String subfolder) throws IOException {
    String url = String.format("http://%s/view?filename=%s&type=%s&subfolder=%s",
    SERVER_ADDRESS,
    URLEncoder.encode(filename, StandardCharsets.UTF_8),
    URLEncoder.encode(type, StandardCharsets.UTF_8),
    URLEncoder.encode(subfolder, StandardCharsets.UTF_8));

    System.out.println("✅ 画像取得: " + url);

    HttpRequest req = HttpRequest.newBuilder().uri(URI.create(url)).build();
    try {
    HttpResponse<byte[]> res = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
    return res.body();
    } catch (InterruptedException ex) {
    throw new RuntimeException(ex);
    }
    }

    /** WebSocket リスナー */
    private class WSListener implements WebSocket.Listener {
    private final StringBuilder buffer = new StringBuilder();

    @Override
    public void onOpen(WebSocket webSocket) {
    if (comfyHandler != null) comfyHandler.onOpen();
    WebSocket.Listener.super.onOpen(webSocket);
    }

    @Override
    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
    buffer.append(data);
    if (last) {
    handleMessage(buffer.toString());
    buffer.setLength(0);
    }
    return WebSocket.Listener.super.onText(webSocket, data, last);
    }

    private void handleMessage(String msgStr) {
    try {
    JsonNode msg = mapper.readTree(msgStr);
    String type = msg.path("type").asText();

    switch (type) {
    case "status" -> {
    String sid = msg.path("data").path("sid").asText(null);
    if (sid != null && comfyHandler != null){
    comfyHandler.onSid(sid);
    }
    var remaining = msg.findPath("queue_remaining").asInt(-1);
    if (remaining >= 0 && comfyHandler != null) {
    comfyHandler.onQueueStatus(remaining);
    }
    }

    case "progress" -> {
    JsonNode data = msg.path("data");
    int value = data.path("value").asInt(0);
    int max = data.path("max").asInt(1);
    var id = data.path("prompt_id").asText();

    GenerationHandler h;
    synchronized (handlers) {
    h = handlers.get(id);
    }
    if (h != null) h.onProgress(value, max);
    }

    case "executed" -> {
    System.out.println("📥 executed: " + msg);
    JsonNode data = msg.path("data");
    var id = data.path("prompt_id").asText();
    JsonNode images = data.path("output").path("images");
    if (images.isArray() && images.size() > 0) {
    ObjectNode img = (ObjectNode) images.get(0);

    var filename = img.path("filename").asText();
    var filetype = img.path("type").asText();
    var subfolder = img.path("subfolder").asText();
    GenerationHandler h;
    synchronized (handlers) {
    h = handlers.get(id);
    }
    if (h != null) {
    try {
    byte[] imageData = comfyUIView(filename, filetype, subfolder);
    h.onFinish(imageData);
    } catch (IOException ex) {
    h.onError(ex);
    }
    }
    }
    }

    case "execution_start" -> {
    var id = msg.findPath("prompt_id").asText();
    GenerationHandler h;
    synchronized (handlers) {
    h = handlers.get(id);
    }
    if (h != null) h.onStart();
    }
    case "execution_success" -> {
    var id = msg.findPath("prompt_id").asText();
    synchronized (handlers) {
    handlers.remove(id);
    }
    }
    default -> {} // 無視
    }
    } catch (Exception e) {
    if (comfyHandler != null) comfyHandler.onError(e);
    }
    }

    @Override
    public void onError(WebSocket webSocket, Throwable error) {
    if (comfyHandler != null) comfyHandler.onError(error);
    }

    @Override
    public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
    if (comfyHandler != null) comfyHandler.onClose(statusCode, reason);

    return WebSocket.Listener.super.onClose(webSocket, statusCode, reason);
    }
    }
    }