Skip to content

Commit 63b05aa

Browse files
committed
Return DeferredImage
1 parent dede3d5 commit 63b05aa

9 files changed

+498
-38
lines changed

lib/ruby_llm/deferred_image.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
# Represents an image that may not yet be fully generated.
5+
class DeferredImage
6+
include Imageable
7+
8+
attr_reader :provider_instance, :url
9+
10+
def initialize(url:, provider_instance:)
11+
@url = url
12+
@provider_instance = provider_instance
13+
end
14+
15+
def deferred?
16+
true
17+
end
18+
19+
def to_blob
20+
provider_instance.fetch_image_blob(url)
21+
end
22+
end
23+
end

lib/ruby_llm/image.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
module RubyLLM
44
# Represents a generated image from an AI model.
55
class Image
6+
include Imageable
7+
68
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id
79

810
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil)
@@ -26,11 +28,6 @@ def to_blob
2628
end
2729
end
2830

29-
def save(path)
30-
File.binwrite(File.expand_path(path), to_blob)
31-
path
32-
end
33-
3431
def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
3532
model: nil,
3633
provider: nil,

lib/ruby_llm/imageable.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
# Mixin for classes that can be used to generate images
5+
module Imageable
6+
def base64? = false
7+
def deferred? = false
8+
def to_blob = raise NotImplementedError, 'to_blob is not implemented'
9+
10+
def save(path)
11+
if (blob = to_blob)
12+
File.binwrite(File.expand_path(path), blob)
13+
path
14+
end
15+
end
16+
end
17+
end

lib/ruby_llm/providers/replicate/images.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ def render_image_payload(prompt, model:, **params)
3030
end
3131

3232
def parse_image_response(response, **)
33-
response.body
33+
DeferredImage.new(url: response.body.dig('urls', 'get'), provider_instance: self)
34+
end
35+
36+
def fetch_image_blob(url)
37+
prediction = @connection.get(url).body
38+
return unless prediction['status'] == 'succeeded'
39+
40+
image_url = Array(prediction['output']).first
41+
@connection.get(image_url).body
3442
end
3543

3644
private

spec/fixtures/vcr_cassettes/image_basic_functionality_google_imagen-4-ultra_an_official_replicate_model_can_paint_images.yml

Lines changed: 142 additions & 7 deletions
Large diffs are not rendered by default.

spec/fixtures/vcr_cassettes/image_basic_functionality_google_nano-banana_an_official_replicate_model_can_paint_images_with_custom_parameters.yml

Lines changed: 143 additions & 7 deletions
Large diffs are not rendered by default.

spec/fixtures/vcr_cassettes/image_basic_functionality_prunaai_hidream-l1-fast_an_unofficial_replicate_model_can_paint_images.yml

Lines changed: 140 additions & 7 deletions
Large diffs are not rendered by default.

spec/ruby_llm/image_generation_spec.rb

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def save_and_verify_image(image)
2222
end
2323
end
2424

25+
def wait_for_deferred_image
26+
sleep(15) if VCR.current_cassette.recording?
27+
end
28+
2529
RSpec.describe RubyLLM::Image do
2630
include_context 'with configured RubyLLM'
2731

@@ -60,27 +64,34 @@ def save_and_verify_image(image)
6064
end
6165

6266
it 'google/imagen-4-ultra, an official replicate model, can paint images' do
63-
response = RubyLLM.paint('a siamese cat', model: 'google/imagen-4-ultra')
67+
image = RubyLLM.paint('a siamese cat', model: 'google/imagen-4-ultra', output_format: 'png')
6468

65-
expect(response).to be_a(Hash)
66-
expect(response.dig('urls', 'get')).to be_present
69+
expect(image.deferred?).to be(true)
70+
71+
wait_for_deferred_image
72+
save_and_verify_image image
6773
end
6874

6975
it 'google/nano-banana, an official replicate model, can paint images with custom parameters' do
7076
ref_img1 = 'https://replicate.delivery/pbxt/NbYIclp4A5HWLsJ8lF5KgiYSNaLBBT1jUcYcHYQmN1uy5OnN/tmpcqc07f_q.png'
7177
ref_img2 = 'https://replicate.delivery/pbxt/NbYId45yH8s04sptdtPcGqFIhV7zS5GTcdS3TtNliyTAoYPO/Screenshot%202025-08-26%20at%205.30.12%E2%80%AFPM.png'
72-
response = RubyLLM.paint('a siamese cat', model: 'google/nano-banana',
73-
image_input: [ref_img1, ref_img2])
78+
image = RubyLLM.paint('a siamese cat', model: 'google/nano-banana',
79+
image_input: [ref_img1, ref_img2],
80+
output_format: 'png')
7481

75-
expect(response).to be_a(Hash)
76-
expect(response.dig('urls', 'get')).to be_present
82+
expect(image.deferred?).to be(true)
83+
84+
wait_for_deferred_image
85+
save_and_verify_image image
7786
end
7887

7988
it 'prunaai/hidream-l1-fast, an unofficial replicate model, can paint images' do
80-
response = RubyLLM.paint('a siamese cat', model: 'prunaai/hidream-l1-fast')
89+
image = RubyLLM.paint('a siamese cat', model: 'prunaai/hidream-l1-fast', output_format: 'png')
8190

82-
expect(response).to be_a(Hash)
83-
expect(response.dig('urls', 'get')).to be_present
91+
expect(image.deferred?).to be(true)
92+
93+
wait_for_deferred_image
94+
save_and_verify_image image
8495
end
8596

8697
it 'validates model existence' do

spec/support/rspec_configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
config.around do |example|
1515
cassette_name = example.full_description.parameterize(separator: '_').delete_prefix('rubyllm_')
16-
VCR.use_cassette(cassette_name, record: :new_episodes) do
16+
VCR.use_cassette(cassette_name) do
1717
example.run
1818
end
1919
end

0 commit comments

Comments
 (0)