imago 0.2.0 → 0.2.1
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 +4 -4
- data/.rubocop.yml +14 -1
- data/lib/imago/gemini_response_parser.rb +24 -0
- data/lib/imago/image_input.rb +15 -10
- data/lib/imago/multipart_builder.rb +43 -0
- data/lib/imago/providers/gemini.rb +49 -51
- data/lib/imago/providers/openai.rb +50 -85
- data/lib/imago/providers/xai.rb +18 -18
- data/lib/imago/version.rb +1 -1
- data/lib/imago.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 417e5a3f6b7a3d41cee0ce17ec1cc225862fcc72c2448fc1c9b1da989509afd3
|
|
4
|
+
data.tar.gz: feca98a16c48e79f386b88626d7fa5934be4637910ae250a91bf036f79a2865b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb62ccf9dcb695a401f08674d68ba37e7b3f2e8cdb7ae5820735d987aa41839f95a2fd5152533016fd8a6b704fd43df26ebf9e18d8b4f64181ed4bfbea3a4a1e
|
|
7
|
+
data.tar.gz: da94d8fbd02b78cce3ab04c567fa90881752670973925254c77ed724a645c9974dbb635b810e70ebe7182e5f5b658ae83f29590adaa867844760c5501cb70027
|
data/.rubocop.yml
CHANGED
|
@@ -45,8 +45,21 @@ Layout/LineLength:
|
|
|
45
45
|
Max: 120
|
|
46
46
|
|
|
47
47
|
Metrics/ClassLength:
|
|
48
|
+
Max: 100
|
|
49
|
+
|
|
50
|
+
Metrics/MethodLength:
|
|
51
|
+
Max: 25
|
|
52
|
+
|
|
53
|
+
Metrics/AbcSize:
|
|
54
|
+
Max: 10
|
|
48
55
|
Exclude:
|
|
49
|
-
- '
|
|
56
|
+
- 'spec/**/*'
|
|
57
|
+
|
|
58
|
+
Metrics/CyclomaticComplexity:
|
|
59
|
+
Max: 10
|
|
60
|
+
|
|
61
|
+
Metrics/PerceivedComplexity:
|
|
62
|
+
Max: 10
|
|
50
63
|
|
|
51
64
|
RSpec/MessageSpies:
|
|
52
65
|
EnforcedStyle: receive
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Imago
|
|
4
|
+
class GeminiResponseParser
|
|
5
|
+
def parse(body)
|
|
6
|
+
candidates = body['candidates'] || []
|
|
7
|
+
images = candidates.flat_map { |candidate| extract_images(candidate) }
|
|
8
|
+
{ images: images }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def extract_images(candidate)
|
|
14
|
+
parts = candidate.dig('content', 'parts') || []
|
|
15
|
+
parts.filter_map { |part| parse_image_part(part) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse_image_part(part)
|
|
19
|
+
return unless part['inlineData']
|
|
20
|
+
|
|
21
|
+
{ base64: part['inlineData']['data'], mime_type: part['inlineData']['mimeType'] }.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/imago/image_input.rb
CHANGED
|
@@ -31,16 +31,21 @@ module Imago
|
|
|
31
31
|
def self.from_hash(hash)
|
|
32
32
|
hash = hash.transform_keys(&:to_sym)
|
|
33
33
|
|
|
34
|
-
if hash[:base64]
|
|
35
|
-
|
|
34
|
+
return from_base64_hash(hash) if hash[:base64]
|
|
35
|
+
return from_url_hash(hash) if hash[:url]
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
raise ArgumentError, 'Image hash must contain either :url or :base64 key'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_base64_hash(hash)
|
|
41
|
+
raise ArgumentError, 'mime_type is required for base64 images' unless hash[:mime_type]
|
|
42
|
+
|
|
43
|
+
new(base64: hash[:base64], mime_type: hash[:mime_type])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.from_url_hash(hash)
|
|
47
|
+
mime_type = hash[:mime_type] || detect_mime_type(hash[:url])
|
|
48
|
+
new(url: hash[:url], mime_type: mime_type)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def self.detect_mime_type(url)
|
|
@@ -50,7 +55,7 @@ module Imago
|
|
|
50
55
|
nil
|
|
51
56
|
end
|
|
52
57
|
|
|
53
|
-
private_class_method :from_url_string, :from_hash, :detect_mime_type
|
|
58
|
+
private_class_method :from_url_string, :from_hash, :from_base64_hash, :from_url_hash, :detect_mime_type
|
|
54
59
|
|
|
55
60
|
def initialize(url: nil, base64: nil, mime_type: nil)
|
|
56
61
|
@url = url
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Imago
|
|
6
|
+
class MultipartBuilder
|
|
7
|
+
def initialize(model)
|
|
8
|
+
@model = model
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build_body(prompt, images, opts)
|
|
12
|
+
body = base_body(prompt)
|
|
13
|
+
add_images(body, images)
|
|
14
|
+
body.merge(opts.except(:images))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_image_part(image)
|
|
18
|
+
image.url? ? image.url : build_file_part(image)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def base_body(prompt)
|
|
24
|
+
{ model: @model, prompt: prompt }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_images(body, images)
|
|
28
|
+
images.each_with_index do |image, index|
|
|
29
|
+
body["image[#{index}]"] = build_image_part(image)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_file_part(image)
|
|
34
|
+
io = StringIO.new(Base64.decode64(image.base64))
|
|
35
|
+
extension = extract_extension(image.mime_type)
|
|
36
|
+
Faraday::Multipart::FilePart.new(io, image.mime_type, "image.#{extension}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_extension(mime_type)
|
|
40
|
+
mime_type&.split('/')&.last || 'png'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -16,15 +16,8 @@ module Imago
|
|
|
16
16
|
|
|
17
17
|
def generate(prompt, opts = {})
|
|
18
18
|
validate_image_count!(opts[:images], max: MAX_IMAGES)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
response = conn.post(endpoint) do |req|
|
|
23
|
-
req.params['key'] = api_key
|
|
24
|
-
req.body = build_request_body(prompt, opts)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
parse_generate_response(handle_response(response))
|
|
19
|
+
response = execute_generate_request(prompt, opts)
|
|
20
|
+
response_parser.parse(handle_response(response))
|
|
28
21
|
end
|
|
29
22
|
|
|
30
23
|
def models
|
|
@@ -43,6 +36,25 @@ module Imago
|
|
|
43
36
|
|
|
44
37
|
private
|
|
45
38
|
|
|
39
|
+
def response_parser
|
|
40
|
+
@response_parser ||= Imago::GeminiResponseParser.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def execute_generate_request(prompt, opts)
|
|
44
|
+
conn = connection(BASE_URL)
|
|
45
|
+
body = build_request_body(prompt, opts)
|
|
46
|
+
conn.post(generate_endpoint) { |req| configure_request(req, body) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def generate_endpoint
|
|
50
|
+
"models/#{model}:generateContent"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def configure_request(req, body)
|
|
54
|
+
req.params['key'] = api_key
|
|
55
|
+
req.body = body
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
def build_request_body(prompt, opts)
|
|
47
59
|
body = { contents: [{ parts: build_parts(prompt, opts) }] }
|
|
48
60
|
body[:generationConfig] = build_generation_config(opts) if generation_config_present?(opts)
|
|
@@ -51,71 +63,57 @@ module Imago
|
|
|
51
63
|
|
|
52
64
|
def build_parts(prompt, opts)
|
|
53
65
|
parts = [{ text: build_prompt(prompt, opts) }]
|
|
54
|
-
|
|
55
|
-
images.each { |img| parts << build_image_part(img) }
|
|
66
|
+
normalize_images(opts[:images]).each { |img| parts << build_image_part(img) }
|
|
56
67
|
parts
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
def build_image_part(image)
|
|
60
|
-
|
|
61
|
-
{ fileData: { fileUri: image.url, mimeType: image.mime_type }.compact }
|
|
62
|
-
else
|
|
63
|
-
{ inlineData: { data: image.base64, mimeType: image.mime_type } }
|
|
64
|
-
end
|
|
71
|
+
image.url? ? build_file_data(image) : build_inline_data(image)
|
|
65
72
|
end
|
|
66
73
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"#{prompt}. Avoid: #{opts[:negative_prompt]}"
|
|
74
|
+
def build_file_data(image)
|
|
75
|
+
{ fileData: { fileUri: image.url, mimeType: image.mime_type }.compact }
|
|
71
76
|
end
|
|
72
77
|
|
|
73
|
-
def
|
|
74
|
-
|
|
78
|
+
def build_inline_data(image)
|
|
79
|
+
{ inlineData: { data: image.base64, mimeType: image.mime_type } }
|
|
75
80
|
end
|
|
76
81
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
config[:candidateCount] = opts[:sample_count] || opts[:n] if opts[:n] || opts[:sample_count]
|
|
80
|
-
config[:seed] = opts[:seed] if opts[:seed]
|
|
81
|
-
config[:aspectRatio] = opts[:aspect_ratio] if opts[:aspect_ratio]
|
|
82
|
-
config
|
|
82
|
+
def build_prompt(prompt, opts)
|
|
83
|
+
opts[:negative_prompt] ? "#{prompt}. Avoid: #{opts[:negative_prompt]}" : prompt
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
images = candidates.flat_map { |candidate| extract_images_from_candidate(candidate) }
|
|
88
|
-
{ images: images }
|
|
86
|
+
def generation_config_present?(opts)
|
|
87
|
+
opts[:n] || opts[:sample_count] || opts[:aspect_ratio] || opts[:seed]
|
|
89
88
|
end
|
|
90
89
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
parts.filter_map do |part|
|
|
94
|
-
next unless part['inlineData']
|
|
95
|
-
|
|
96
|
-
{ base64: part['inlineData']['data'], mime_type: part['inlineData']['mimeType'] }.compact
|
|
97
|
-
end
|
|
90
|
+
def build_generation_config(opts)
|
|
91
|
+
{ candidateCount: opts[:sample_count] || opts[:n], seed: opts[:seed], aspectRatio: opts[:aspect_ratio] }.compact
|
|
98
92
|
end
|
|
99
93
|
|
|
100
94
|
def fetch_models
|
|
101
|
-
|
|
102
|
-
response
|
|
103
|
-
req.params['key'] = api_key
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
body = handle_response(response)
|
|
107
|
-
filter_image_models(body['models'] || [])
|
|
95
|
+
response = connection(BASE_URL).get('models') { |req| req.params['key'] = api_key }
|
|
96
|
+
filter_image_models(handle_response(response)['models'] || [])
|
|
108
97
|
rescue ApiError
|
|
109
98
|
KNOWN_IMAGE_MODELS
|
|
110
99
|
end
|
|
111
100
|
|
|
112
101
|
def filter_image_models(models)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
102
|
+
names = extract_image_model_names(models)
|
|
103
|
+
names.empty? ? KNOWN_IMAGE_MODELS : names
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_image_model_names(models)
|
|
107
|
+
content_models = models.select { |m| supports_generate_content?(m) }
|
|
108
|
+
content_models.map { |m| m['name'].sub('models/', '') }.select { |n| image_model?(n) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def supports_generate_content?(model)
|
|
112
|
+
model['supportedGenerationMethods']&.include?('generateContent')
|
|
113
|
+
end
|
|
117
114
|
|
|
118
|
-
|
|
115
|
+
def image_model?(name)
|
|
116
|
+
name.include?('imagen') || name.include?('image')
|
|
119
117
|
end
|
|
120
118
|
end
|
|
121
119
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'faraday/multipart'
|
|
4
|
-
require 'base64'
|
|
5
4
|
|
|
6
5
|
module Imago
|
|
7
6
|
module Providers
|
|
@@ -9,28 +8,12 @@ module Imago
|
|
|
9
8
|
BASE_URL = 'https://api.openai.com/v1'
|
|
10
9
|
MAX_IMAGES = 16
|
|
11
10
|
|
|
12
|
-
KNOWN_IMAGE_MODELS = %w[
|
|
13
|
-
|
|
14
|
-
dall-e-2
|
|
15
|
-
gpt-image-1
|
|
16
|
-
gpt-image-1.5
|
|
17
|
-
gpt-image-1-mini
|
|
18
|
-
].freeze
|
|
19
|
-
|
|
20
|
-
MODELS_SUPPORTING_IMAGE_INPUT = %w[
|
|
21
|
-
dall-e-2
|
|
22
|
-
gpt-image-1
|
|
23
|
-
gpt-image-1.5
|
|
24
|
-
gpt-image-1-mini
|
|
25
|
-
].freeze
|
|
11
|
+
KNOWN_IMAGE_MODELS = %w[dall-e-3 dall-e-2 gpt-image-1 gpt-image-1.5 gpt-image-1-mini].freeze
|
|
12
|
+
MODELS_SUPPORTING_IMAGE_INPUT = %w[dall-e-2 gpt-image-1 gpt-image-1.5 gpt-image-1-mini].freeze
|
|
26
13
|
|
|
27
14
|
def generate(prompt, opts = {})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
generate_with_images(prompt, opts)
|
|
31
|
-
else
|
|
32
|
-
generate_text_only(prompt, opts)
|
|
33
|
-
end
|
|
15
|
+
has_images = opts[:images] && !opts[:images].empty?
|
|
16
|
+
has_images ? generate_with_images(prompt, opts) : generate_text_only(prompt, opts)
|
|
34
17
|
end
|
|
35
18
|
|
|
36
19
|
def models
|
|
@@ -50,82 +33,72 @@ module Imago
|
|
|
50
33
|
private
|
|
51
34
|
|
|
52
35
|
def generate_text_only(prompt, opts)
|
|
53
|
-
|
|
54
|
-
response
|
|
55
|
-
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
56
|
-
req.body = build_request_body(prompt, opts)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
parse_generate_response(handle_response(response))
|
|
36
|
+
response = post_with_auth('images/generations', build_request_body(prompt, opts))
|
|
37
|
+
parse_response(response)
|
|
60
38
|
end
|
|
61
39
|
|
|
62
40
|
def generate_with_images(prompt, opts)
|
|
63
41
|
validate_model_supports_images!
|
|
64
42
|
validate_image_count!(opts[:images], max: MAX_IMAGES)
|
|
43
|
+
response = post_multipart_edit(prompt, opts)
|
|
44
|
+
parse_response(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def post_with_auth(endpoint, body)
|
|
48
|
+
connection(BASE_URL).post(endpoint) do |req|
|
|
49
|
+
req.headers['Authorization'] = auth_header
|
|
50
|
+
req.body = body
|
|
51
|
+
end
|
|
52
|
+
end
|
|
65
53
|
|
|
54
|
+
def post_multipart_edit(prompt, opts)
|
|
55
|
+
body = build_multipart_body(prompt, opts)
|
|
56
|
+
multipart_connection.post('images/edits') { |req| configure_auth_request(req, body) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_multipart_body(prompt, opts)
|
|
66
60
|
images = normalize_images(opts[:images])
|
|
67
|
-
|
|
61
|
+
multipart_builder.build_body(prompt, images, opts)
|
|
62
|
+
end
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
def configure_auth_request(req, body)
|
|
65
|
+
req.headers['Authorization'] = auth_header
|
|
66
|
+
req.body = body
|
|
67
|
+
end
|
|
73
68
|
|
|
74
|
-
|
|
69
|
+
def parse_response(response)
|
|
70
|
+
body = handle_response(response)
|
|
71
|
+
images = body['data']&.map { |img| parse_image(img) }
|
|
72
|
+
{ images: images || [], created: body['created'] }
|
|
75
73
|
end
|
|
76
74
|
|
|
77
|
-
def
|
|
78
|
-
|
|
75
|
+
def auth_header
|
|
76
|
+
"Bearer #{api_key}"
|
|
77
|
+
end
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"Model '#{model}' does not support image inputs. Supported models: #{supported}",
|
|
83
|
-
status_code: 400
|
|
84
|
-
)
|
|
79
|
+
def multipart_builder
|
|
80
|
+
@multipart_builder ||= Imago::MultipartBuilder.new(model)
|
|
85
81
|
end
|
|
86
82
|
|
|
87
|
-
def multipart_connection
|
|
88
|
-
Faraday.new(url:
|
|
83
|
+
def multipart_connection
|
|
84
|
+
@multipart_connection ||= Faraday.new(url: BASE_URL) do |conn|
|
|
89
85
|
conn.request :multipart
|
|
90
86
|
conn.response :json
|
|
91
87
|
conn.adapter Faraday.default_adapter
|
|
92
88
|
end
|
|
93
89
|
end
|
|
94
90
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
model: model,
|
|
98
|
-
prompt: prompt
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
images.each_with_index do |image, index|
|
|
102
|
-
body["image[#{index}]"] = build_image_part(image)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
clean_opts = opts.except(:images)
|
|
106
|
-
body.merge(clean_opts)
|
|
107
|
-
end
|
|
91
|
+
def validate_model_supports_images!
|
|
92
|
+
return if MODELS_SUPPORTING_IMAGE_INPUT.include?(model)
|
|
108
93
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
io = StringIO.new(Base64.decode64(image.base64))
|
|
114
|
-
extension = image.mime_type&.split('/')&.last || 'png'
|
|
115
|
-
Faraday::Multipart::FilePart.new(io, image.mime_type, "image.#{extension}")
|
|
116
|
-
end
|
|
94
|
+
raise InvalidRequestError.new(
|
|
95
|
+
"Model '#{model}' does not support image inputs. Supported: #{MODELS_SUPPORTING_IMAGE_INPUT.join(', ')}",
|
|
96
|
+
status_code: 400
|
|
97
|
+
)
|
|
117
98
|
end
|
|
118
99
|
|
|
119
100
|
def build_request_body(prompt, opts)
|
|
120
|
-
{
|
|
121
|
-
model: model,
|
|
122
|
-
prompt: prompt
|
|
123
|
-
}.merge(opts)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def parse_generate_response(body)
|
|
127
|
-
images = body['data']&.map { |img| parse_image(img) }
|
|
128
|
-
{ images: images || [], created: body['created'] }
|
|
101
|
+
{ model: model, prompt: prompt }.merge(opts)
|
|
129
102
|
end
|
|
130
103
|
|
|
131
104
|
def parse_image(img)
|
|
@@ -133,23 +106,15 @@ module Imago
|
|
|
133
106
|
end
|
|
134
107
|
|
|
135
108
|
def fetch_models
|
|
136
|
-
|
|
137
|
-
response
|
|
138
|
-
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
body = handle_response(response)
|
|
142
|
-
filter_image_models(body['data'] || [])
|
|
109
|
+
response = connection(BASE_URL).get('models') { |req| req.headers['Authorization'] = auth_header }
|
|
110
|
+
filter_image_models(handle_response(response)['data'] || [])
|
|
143
111
|
rescue ApiError
|
|
144
112
|
KNOWN_IMAGE_MODELS
|
|
145
113
|
end
|
|
146
114
|
|
|
147
115
|
def filter_image_models(models)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.select { |id| id.include?('dall-e') || id.include?('image') }
|
|
151
|
-
|
|
152
|
-
image_model_ids.empty? ? KNOWN_IMAGE_MODELS : image_model_ids
|
|
116
|
+
ids = models.map { |m| m['id'] }.select { |id| id.include?('dall-e') || id.include?('image') }
|
|
117
|
+
ids.empty? ? KNOWN_IMAGE_MODELS : ids
|
|
153
118
|
end
|
|
154
119
|
end
|
|
155
120
|
end
|
data/lib/imago/providers/xai.rb
CHANGED
|
@@ -12,13 +12,7 @@ module Imago
|
|
|
12
12
|
|
|
13
13
|
def generate(prompt, opts = {})
|
|
14
14
|
raise_if_images_provided(opts)
|
|
15
|
-
|
|
16
|
-
conn = connection(BASE_URL)
|
|
17
|
-
response = conn.post('images/generations') do |req|
|
|
18
|
-
req.headers['Authorization'] = "Bearer #{api_key}"
|
|
19
|
-
req.body = build_request_body(prompt, opts)
|
|
20
|
-
end
|
|
21
|
-
|
|
15
|
+
response = execute_generate_request(prompt, opts)
|
|
22
16
|
parse_generate_response(handle_response(response))
|
|
23
17
|
end
|
|
24
18
|
|
|
@@ -38,6 +32,21 @@ module Imago
|
|
|
38
32
|
|
|
39
33
|
private
|
|
40
34
|
|
|
35
|
+
def execute_generate_request(prompt, opts)
|
|
36
|
+
conn = connection(BASE_URL)
|
|
37
|
+
body = build_request_body(prompt, opts)
|
|
38
|
+
conn.post('images/generations') { |req| configure_request(req, body) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configure_request(req, body)
|
|
42
|
+
req.headers['Authorization'] = auth_header
|
|
43
|
+
req.body = body
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def auth_header
|
|
47
|
+
"Bearer #{api_key}"
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def build_request_body(prompt, opts)
|
|
42
51
|
{
|
|
43
52
|
model: model,
|
|
@@ -48,17 +57,8 @@ module Imago
|
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def parse_generate_response(body)
|
|
51
|
-
images = body['data']&.map
|
|
52
|
-
|
|
53
|
-
url: img['url'],
|
|
54
|
-
base64: img['b64_json']
|
|
55
|
-
}.compact
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
images: images || [],
|
|
60
|
-
created: body['created']
|
|
61
|
-
}
|
|
60
|
+
images = body['data']&.map { |img| { url: img['url'], base64: img['b64_json'] }.compact }
|
|
61
|
+
{ images: images || [], created: body['created'] }
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def raise_if_images_provided(opts)
|
data/lib/imago/version.rb
CHANGED
data/lib/imago.rb
CHANGED
|
@@ -6,6 +6,8 @@ require 'json'
|
|
|
6
6
|
require_relative 'imago/version'
|
|
7
7
|
require_relative 'imago/errors'
|
|
8
8
|
require_relative 'imago/image_input'
|
|
9
|
+
require_relative 'imago/multipart_builder'
|
|
10
|
+
require_relative 'imago/gemini_response_parser'
|
|
9
11
|
require_relative 'imago/providers/base'
|
|
10
12
|
require_relative 'imago/providers/openai'
|
|
11
13
|
require_relative 'imago/providers/gemini'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: imago
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- NEETzsche
|
|
@@ -67,7 +67,9 @@ files:
|
|
|
67
67
|
- lib/imago.rb
|
|
68
68
|
- lib/imago/client.rb
|
|
69
69
|
- lib/imago/errors.rb
|
|
70
|
+
- lib/imago/gemini_response_parser.rb
|
|
70
71
|
- lib/imago/image_input.rb
|
|
72
|
+
- lib/imago/multipart_builder.rb
|
|
71
73
|
- lib/imago/providers/base.rb
|
|
72
74
|
- lib/imago/providers/gemini.rb
|
|
73
75
|
- lib/imago/providers/openai.rb
|