package comfyui import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" "strings" "time" "github.com/gorilla/websocket" ) type Client struct { BaseURL string HTTPClient *http.Client } func New(baseURL string) *Client { return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), HTTPClient: &http.Client{Timeout: 120 * time.Second}, } } type PromptResponse struct { PromptID string `json:"prompt_id"` Number int `json:"number"` } func (c *Client) SubmitPrompt(ctx context.Context, prompt map[string]any, clientID string) (*PromptResponse, error) { body, _ := json.Marshal(map[string]any{ "prompt": prompt, "client_id": clientID, }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/prompt", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return nil, fmt.Errorf("comfyui prompt %d: %s", resp.StatusCode, string(data)) } var out PromptResponse if err := json.Unmarshal(data, &out); err != nil { return nil, err } return &out, nil } func (c *Client) Interrupt(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/interrupt", nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { data, _ := io.ReadAll(resp.Body) return fmt.Errorf("comfyui interrupt %d: %s", resp.StatusCode, string(data)) } return nil } type HistoryEntry struct { Outputs map[string]HistoryNodeOutput `json:"outputs"` Status map[string]any `json:"status"` } type HistoryNodeOutput struct { Images []OutputImage `json:"images"` } type OutputImage struct { Filename string `json:"filename"` Subfolder string `json:"subfolder"` Type string `json:"type"` } func (c *Client) GetHistory(ctx context.Context, promptID string) (map[string]HistoryEntry, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/history/"+promptID, nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return nil, fmt.Errorf("comfyui history %d: %s", resp.StatusCode, string(data)) } var out map[string]HistoryEntry if err := json.Unmarshal(data, &out); err != nil { return nil, err } return out, nil } func (c *Client) DownloadView(ctx context.Context, filename, subfolder, imageType string) ([]byte, string, error) { q := url.Values{} q.Set("filename", filename) if subfolder != "" { q.Set("subfolder", subfolder) } if imageType == "" { imageType = "output" } q.Set("type", imageType) u := c.BaseURL + "/view?" + q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, "", err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, "", err } if resp.StatusCode >= 400 { return nil, "", fmt.Errorf("comfyui view %d", resp.StatusCode) } mime := resp.Header.Get("Content-Type") if mime == "" { mime = "image/png" } return data, mime, nil } func (c *Client) UploadImage(ctx context.Context, data []byte, filename string) (string, error) { var buf bytes.Buffer w := multipart.NewWriter(&buf) part, err := w.CreateFormFile("image", filename) if err != nil { return "", err } if _, err := part.Write(data); err != nil { return "", err } _ = w.WriteField("overwrite", "true") _ = w.Close() req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/upload/image", &buf) if err != nil { return "", err } req.Header.Set("Content-Type", w.FormDataContentType()) resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return "", fmt.Errorf("comfyui upload %d: %s", resp.StatusCode, string(body)) } var out struct { Name string `json:"name"` } if err := json.Unmarshal(body, &out); err != nil { return "", err } return out.Name, nil } func (c *Client) HealthCheck(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/system_stats", nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("status %d", resp.StatusCode) } return nil } type ProgressEvent struct { Type string Data map[string]any Node string Value float64 Max float64 PromptID string ErrorText string } func (c *Client) wsURL(clientID string) string { u, _ := url.Parse(c.BaseURL) scheme := "ws" if u.Scheme == "https" { scheme = "wss" } return fmt.Sprintf("%s://%s/ws?clientId=%s", scheme, u.Host, url.QueryEscape(clientID)) } func (c *Client) Watch(ctx context.Context, clientID string, onEvent func(ProgressEvent)) error { conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.wsURL(clientID), nil) if err != nil { return err } defer conn.Close() done := make(chan struct{}) go func() { defer close(done) for { _, msg, err := conn.ReadMessage() if err != nil { return } var envelope struct { Type string `json:"type"` Data json.RawMessage `json:"data"` } if err := json.Unmarshal(msg, &envelope); err != nil { continue } ev := ProgressEvent{Type: envelope.Type} var data map[string]any _ = json.Unmarshal(envelope.Data, &data) ev.Data = data if envelope.Type == "progress" { if n, ok := data["node"].(string); ok { ev.Node = n } if v, ok := data["value"].(float64); ok { ev.Value = v } if m, ok := data["max"].(float64); ok { ev.Max = m } } if envelope.Type == "executing" { if n, ok := data["node"].(string); ok { ev.Node = n } if pid, ok := data["prompt_id"].(string); ok { ev.PromptID = pid } } if envelope.Type == "execution_error" { if msgs, ok := data["exception_message"].(string); ok { ev.ErrorText = msgs } } onEvent(ev) } }() select { case <-ctx.Done(): _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) <-done return ctx.Err() case <-done: return nil } } func CollectOutputImages(history map[string]HistoryEntry, promptID string) []OutputImage { entry, ok := history[promptID] if !ok { return nil } var images []OutputImage for _, out := range entry.Outputs { images = append(images, out.Images...) } return images }