comfyui-ruby 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 318858dd755608980d1b251b0d5271458646ee04aeba16167d4590e4293bf175
4
+ data.tar.gz: e23e068a97520ab5d5d7f3de2077071b9c817e5edbdd9a1bd0bdb2d32bf2badd
5
+ SHA512:
6
+ metadata.gz: 25d9e51bc4582af348695f1b762c82a6e7a686b3dbe8ff908355c9c8e874a75526dd851d5853106e39f3cd18110f915345b1056828a3bb6e5823436f40aaf928
7
+ data.tar.gz: 0a32aba52fda57833fe399dd49cec55a81ab05f4e5b166e9455748a770c65d98bc402a293b58d2fdca6be51ef9b08ad38a7e55ac9ca31c17f21af2952c9e36e3
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Comfyui::Ruby
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/comfyui/ruby`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/comfyui-ruby.
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "securerandom"
6
+
7
+ module ComfyUI
8
+ # Ruby client for the ComfyUI HTTP + WebSocket API.
9
+ #
10
+ # @example
11
+ # client = ComfyUI::Client.new("http://localhost:8188")
12
+ # client.system_stats
13
+ # client.models
14
+ # client.generate(prompt: "a cat in space")
15
+ #
16
+ class Client
17
+ attr_reader :url
18
+
19
+ # @param url [String] ComfyUI base URL
20
+ # @param timeout [Integer] HTTP timeout in seconds
21
+ def initialize(url = nil, timeout: 30)
22
+ @url = url || ENV.fetch("COMFYUI_URL", DEFAULT_URL)
23
+ @conn = Faraday.new(url: @url) do |f|
24
+ f.options.timeout = timeout
25
+ f.options.open_timeout = 10
26
+ f.request :json
27
+ f.response :json
28
+ f.response :raise_error
29
+ end
30
+ @raw_conn = Faraday.new(url: @url) do |f|
31
+ f.options.timeout = timeout
32
+ end
33
+ end
34
+
35
+ # ----------------------------------------------------------------
36
+ # Query endpoints
37
+ # ----------------------------------------------------------------
38
+
39
+ # Get system stats (GPU, RAM, etc.)
40
+ # @return [Hash]
41
+ def system_stats
42
+ get("/system_stats")
43
+ end
44
+
45
+ # Get queue status.
46
+ # @return [Hash] with queue_running and queue_pending
47
+ def queue_status
48
+ get("/queue")
49
+ end
50
+
51
+ # Clear the queue.
52
+ # @return [Boolean]
53
+ def clear_queue
54
+ post("/queue", {clear: true})
55
+ true
56
+ end
57
+
58
+ # Get available nodes and their configurations.
59
+ # @return [Hash]
60
+ def object_info
61
+ get("/object_info")
62
+ end
63
+
64
+ # Get available models grouped by type.
65
+ # @return [Hash{String => Array<String>}]
66
+ def models
67
+ info = object_info
68
+ result = {}
69
+
70
+ model_types = {
71
+ "checkpoints" => ["CheckpointLoaderSimple", "ckpt_name"],
72
+ "loras" => ["LoraLoader", "lora_name"],
73
+ "vae" => ["VAELoader", "vae_name"],
74
+ "clip" => ["CLIPLoader", "clip_name"],
75
+ "controlnet" => ["ControlNetLoader", "control_net_name"],
76
+ "upscale_models" => ["UpscaleModelLoader", "model_name"]
77
+ }
78
+
79
+ model_types.each do |type, (node_class, input_name)|
80
+ next unless info[node_class]
81
+
82
+ inputs = info.dig(node_class, "input", "required") || {}
83
+ next unless inputs[input_name]
84
+
85
+ input_def = inputs[input_name]
86
+ if input_def.is_a?(Array) && input_def[0].is_a?(Array)
87
+ result[type] = input_def[0]
88
+ end
89
+ end
90
+
91
+ result
92
+ end
93
+
94
+ # Get generation history.
95
+ # @param prompt_id [String, nil] specific prompt ID (nil = recent history)
96
+ # @param max_items [Integer] maximum items to return
97
+ # @return [Hash]
98
+ def history(prompt_id: nil, max_items: 100)
99
+ if prompt_id
100
+ get("/history/#{prompt_id}")
101
+ else
102
+ get("/history", max_items: max_items)
103
+ end
104
+ end
105
+
106
+ # ----------------------------------------------------------------
107
+ # Workflow execution
108
+ # ----------------------------------------------------------------
109
+
110
+ # Queue a workflow for execution.
111
+ # @param workflow [Hash] ComfyUI API format workflow
112
+ # @param client_id [String] client ID for WebSocket tracking
113
+ # @return [Hash] with prompt_id and number
114
+ def queue_prompt(workflow, client_id: nil)
115
+ client_id ||= SecureRandom.uuid
116
+ payload = {prompt: workflow, client_id: client_id}
117
+ result = post("/prompt", payload)
118
+
119
+ if result["error"]
120
+ raise APIError, "Workflow error: #{result["error"]}"
121
+ end
122
+
123
+ result
124
+ end
125
+
126
+ # Run a workflow and wait for completion via polling.
127
+ # @param workflow [Hash] ComfyUI API format workflow
128
+ # @param timeout [Float] max wait in seconds
129
+ # @param on_progress [Proc, nil] callback(step, total, message)
130
+ # @return [WorkflowResult]
131
+ def run_workflow(workflow, timeout: 600, on_progress: nil)
132
+ client_id = SecureRandom.uuid
133
+ result = queue_prompt(workflow, client_id: client_id)
134
+ prompt_id = result["prompt_id"]
135
+
136
+ poll_for_completion(prompt_id, timeout: timeout, on_progress: on_progress)
137
+ end
138
+
139
+ # Generate an image with a simple text-to-image workflow.
140
+ # @param prompt [String] positive prompt
141
+ # @param negative_prompt [String] negative prompt
142
+ # @param model [String, nil] checkpoint name (nil = first available)
143
+ # @param width [Integer] image width
144
+ # @param height [Integer] image height
145
+ # @param steps [Integer] sampling steps
146
+ # @param cfg [Float] CFG scale
147
+ # @param seed [Integer] seed (-1 = random)
148
+ # @param sampler [String] sampler name
149
+ # @param scheduler [String] scheduler name
150
+ # @param lora_name [String, nil] LoRA filename
151
+ # @param lora_strength [Float] LoRA strength
152
+ # @param batch_size [Integer] images per batch
153
+ # @param vae [String, nil] VAE filename
154
+ # @param timeout [Float] max wait in seconds
155
+ # @param on_progress [Proc, nil] callback(step, total, message)
156
+ # @return [GenerationResult]
157
+ def generate(
158
+ prompt:,
159
+ negative_prompt: "",
160
+ model: nil,
161
+ width: 1024,
162
+ height: 1024,
163
+ steps: 20,
164
+ cfg: 7.0,
165
+ seed: -1,
166
+ sampler: "euler",
167
+ scheduler: "normal",
168
+ lora_name: nil,
169
+ lora_strength: 1.0,
170
+ batch_size: 1,
171
+ vae: nil,
172
+ timeout: 600,
173
+ on_progress: nil
174
+ )
175
+ # Auto-select first checkpoint if none specified
176
+ unless model
177
+ available = models
178
+ model = available["checkpoints"]&.first
179
+ raise Error, "No checkpoints available" unless model
180
+ end
181
+
182
+ workflow = ComfyUI::Workflow.build(
183
+ prompt: prompt,
184
+ negative_prompt: negative_prompt,
185
+ model: model,
186
+ width: width,
187
+ height: height,
188
+ steps: steps,
189
+ cfg: cfg,
190
+ seed: seed,
191
+ sampler: sampler,
192
+ scheduler: scheduler,
193
+ lora_name: lora_name,
194
+ lora_strength: lora_strength,
195
+ batch_size: batch_size,
196
+ vae: vae
197
+ )
198
+
199
+ result = run_workflow(workflow, timeout: timeout, on_progress: on_progress)
200
+
201
+ images = result.outputs.each_with_object([]) do |(_node_id, output), acc|
202
+ next unless output["images"]
203
+
204
+ output["images"].each do |img_info|
205
+ filename = img_info["filename"]
206
+ subfolder = img_info["subfolder"] || ""
207
+ type = img_info["type"] || "output"
208
+ acc << {filename: filename, subfolder: subfolder, type: type} if type == "output"
209
+ end
210
+ end
211
+
212
+ GenerationResult.new(
213
+ prompt_id: result.prompt_id,
214
+ images: images,
215
+ node_errors: result.node_errors,
216
+ success: result.success
217
+ )
218
+ end
219
+
220
+ # Download a generated image.
221
+ # @param filename [String] image filename
222
+ # @param subfolder [String] subfolder
223
+ # @param folder_type [String] output, input, or temp
224
+ # @return [String] image bytes
225
+ def image(filename, subfolder: "", folder_type: "output")
226
+ response = @raw_conn.get("/view", {
227
+ filename: filename,
228
+ subfolder: subfolder,
229
+ type: folder_type
230
+ })
231
+ response.body
232
+ end
233
+
234
+ private
235
+
236
+ def get(path, params = {})
237
+ response = @conn.get(path, params)
238
+ response.body
239
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
240
+ raise ConnectionError, "Failed to connect to ComfyUI at #{@url}: #{e.message}"
241
+ rescue Faraday::ClientError, Faraday::ServerError => e
242
+ raise APIError, "ComfyUI API error: #{e.message}"
243
+ end
244
+
245
+ def post(path, body = {})
246
+ response = @conn.post(path) { |req| req.body = body }
247
+ response.body
248
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
249
+ raise ConnectionError, "Failed to connect to ComfyUI at #{@url}: #{e.message}"
250
+ rescue Faraday::ClientError, Faraday::ServerError => e
251
+ raise APIError, "ComfyUI API error: #{e.message}"
252
+ end
253
+
254
+ def poll_for_completion(prompt_id, timeout: 600, on_progress: nil)
255
+ start = Time.now
256
+ interval = 0.5
257
+
258
+ loop do
259
+ elapsed = Time.now - start
260
+ raise TimeoutError, "Workflow did not complete within #{timeout}s" if elapsed > timeout
261
+
262
+ hist = history(prompt_id: prompt_id)
263
+
264
+ if hist[prompt_id]
265
+ entry = hist[prompt_id]
266
+ outputs = entry["outputs"] || {}
267
+ status = entry.dig("status", "status_str")
268
+
269
+ if status == "error"
270
+ return WorkflowResult.new(
271
+ prompt_id: prompt_id,
272
+ outputs: outputs,
273
+ node_errors: entry.dig("status", "messages") || {},
274
+ success: false
275
+ )
276
+ end
277
+
278
+ return WorkflowResult.new(
279
+ prompt_id: prompt_id,
280
+ outputs: outputs,
281
+ success: true
282
+ )
283
+ end
284
+
285
+ on_progress&.call(0, 0, "Running...")
286
+ sleep interval
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComfyUI
4
+ GenerationResult = Data.define(:prompt_id, :images, :node_errors, :success) do
5
+ def initialize(prompt_id:, images: [], node_errors: {}, success: true)
6
+ super
7
+ end
8
+ end
9
+
10
+ WorkflowResult = Data.define(:prompt_id, :outputs, :node_errors, :success) do
11
+ def initialize(prompt_id:, outputs: {}, node_errors: {}, success: true)
12
+ super
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module ComfyUI
2
+ VERSION = "0.1.0.pre1"
3
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module ComfyUI
7
+ # Default SDXL/Illustrious/Pony compatible workflow template.
8
+ # Uses separate VAE loader for better quality with modern models.
9
+ DEFAULT_WORKFLOW = {
10
+ "3" => {
11
+ "class_type" => "KSampler",
12
+ "inputs" => {
13
+ "seed" => 0,
14
+ "steps" => 20,
15
+ "cfg" => 7.0,
16
+ "sampler_name" => "euler",
17
+ "scheduler" => "normal",
18
+ "denoise" => 1.0,
19
+ "model" => ["4", 0],
20
+ "positive" => ["6", 0],
21
+ "negative" => ["7", 0],
22
+ "latent_image" => ["5", 0]
23
+ }
24
+ },
25
+ "4" => {
26
+ "class_type" => "CheckpointLoaderSimple",
27
+ "inputs" => {"ckpt_name" => ""}
28
+ },
29
+ "5" => {
30
+ "class_type" => "EmptyLatentImage",
31
+ "inputs" => {"width" => 1024, "height" => 1024, "batch_size" => 1}
32
+ },
33
+ "6" => {
34
+ "class_type" => "CLIPTextEncode",
35
+ "inputs" => {"text" => "", "clip" => ["4", 1]}
36
+ },
37
+ "7" => {
38
+ "class_type" => "CLIPTextEncode",
39
+ "inputs" => {"text" => "", "clip" => ["4", 1]}
40
+ },
41
+ "8" => {
42
+ "class_type" => "VAEDecode",
43
+ "inputs" => {"samples" => ["3", 0], "vae" => ["11", 0]}
44
+ },
45
+ "9" => {
46
+ "class_type" => "SaveImage",
47
+ "inputs" => {"filename_prefix" => "comfy", "images" => ["8", 0]}
48
+ },
49
+ "11" => {
50
+ "class_type" => "VAELoader",
51
+ "inputs" => {"vae_name" => "sdxl_vae.safetensors"}
52
+ }
53
+ }.freeze
54
+
55
+ LORA_LOADER_NODE = {
56
+ "class_type" => "LoraLoader",
57
+ "inputs" => {
58
+ "lora_name" => "",
59
+ "strength_model" => 1.0,
60
+ "strength_clip" => 1.0,
61
+ "model" => ["4", 0],
62
+ "clip" => ["4", 1]
63
+ }
64
+ }.freeze
65
+
66
+ DEFAULT_VAE = "sdxl_vae.safetensors"
67
+
68
+ module Workflow
69
+ module_function
70
+
71
+ # Build a text-to-image workflow from parameters.
72
+ #
73
+ # @param prompt [String] positive prompt
74
+ # @param negative_prompt [String] negative prompt
75
+ # @param model [String, nil] checkpoint filename
76
+ # @param width [Integer] image width
77
+ # @param height [Integer] image height
78
+ # @param steps [Integer] sampling steps
79
+ # @param cfg [Float] CFG scale
80
+ # @param seed [Integer] random seed (-1 for random)
81
+ # @param sampler [String] sampler name
82
+ # @param scheduler [String] scheduler name
83
+ # @param lora_name [String, nil] LoRA filename
84
+ # @param lora_strength [Float] LoRA strength
85
+ # @param batch_size [Integer] images per batch
86
+ # @param vae [String, nil] VAE filename (nil = use checkpoint's VAE)
87
+ # @return [Hash] ComfyUI API workflow
88
+ def build(
89
+ prompt:,
90
+ negative_prompt: "",
91
+ model: nil,
92
+ width: 1024,
93
+ height: 1024,
94
+ steps: 20,
95
+ cfg: 7.0,
96
+ seed: -1,
97
+ sampler: "euler",
98
+ scheduler: "normal",
99
+ lora_name: nil,
100
+ lora_strength: 1.0,
101
+ batch_size: 1,
102
+ vae: nil
103
+ )
104
+ wf = JSON.parse(JSON.generate(DEFAULT_WORKFLOW)) # deep copy
105
+
106
+ actual_seed = seed >= 0 ? seed : rand(2**32)
107
+
108
+ # KSampler
109
+ wf["3"]["inputs"].merge!(
110
+ "seed" => actual_seed,
111
+ "steps" => steps,
112
+ "cfg" => cfg,
113
+ "sampler_name" => sampler,
114
+ "scheduler" => scheduler
115
+ )
116
+
117
+ # Checkpoint
118
+ wf["4"]["inputs"]["ckpt_name"] = model if model
119
+
120
+ # Dimensions
121
+ wf["5"]["inputs"].merge!("width" => width, "height" => height, "batch_size" => batch_size)
122
+
123
+ # Prompts
124
+ wf["6"]["inputs"]["text"] = prompt
125
+ wf["7"]["inputs"]["text"] = negative_prompt
126
+
127
+ # VAE
128
+ if vae
129
+ wf["11"]["inputs"]["vae_name"] = vae
130
+ else
131
+ wf.delete("11")
132
+ wf["8"]["inputs"]["vae"] = ["4", 2]
133
+ end
134
+
135
+ # LoRA injection
136
+ if lora_name
137
+ lora = JSON.parse(JSON.generate(LORA_LOADER_NODE))
138
+ lora["inputs"].merge!(
139
+ "lora_name" => lora_name,
140
+ "strength_model" => lora_strength,
141
+ "strength_clip" => lora_strength,
142
+ "model" => ["4", 0],
143
+ "clip" => ["4", 1]
144
+ )
145
+ wf["10"] = lora
146
+
147
+ wf["3"]["inputs"]["model"] = ["10", 0]
148
+ wf["6"]["inputs"]["clip"] = ["10", 1]
149
+ wf["7"]["inputs"]["clip"] = ["10", 1]
150
+ end
151
+
152
+ wf
153
+ end
154
+ end
155
+ end
data/lib/comfyui.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "comfyui/version"
4
+ require_relative "comfyui/client"
5
+ require_relative "comfyui/workflow"
6
+ require_relative "comfyui/result"
7
+
8
+ module ComfyUI
9
+ DEFAULT_URL = "http://127.0.0.1:8188"
10
+
11
+ class Error < StandardError; end
12
+ class ConnectionError < Error; end
13
+ class APIError < Error; end
14
+ class TimeoutError < Error; end
15
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: comfyui-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - aladac
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faye-websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.11'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.11'
41
+ description: A Ruby client for ComfyUI — query models, queue workflows, generate images,
42
+ and track progress via WebSocket.
43
+ email:
44
+ - aladac@saiden.dev
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - lib/comfyui.rb
51
+ - lib/comfyui/client.rb
52
+ - lib/comfyui/result.rb
53
+ - lib/comfyui/version.rb
54
+ - lib/comfyui/workflow.rb
55
+ homepage: https://github.com/aladac/comfyui-ruby
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/aladac/comfyui-ruby
60
+ source_code_uri: https://github.com/aladac/comfyui-ruby
61
+ changelog_uri: https://github.com/aladac/comfyui-ruby/blob/main/CHANGELOG.md
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.2.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.5.22
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Ruby client for the ComfyUI API
81
+ test_files: []