reve_ai 0.1.0
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 +7 -0
- data/README.md +204 -0
- data/Rakefile +15 -0
- data/lib/reve_ai/client.rb +105 -0
- data/lib/reve_ai/configuration.rb +100 -0
- data/lib/reve_ai/errors.rb +194 -0
- data/lib/reve_ai/http/client.rb +208 -0
- data/lib/reve_ai/resources/base.rb +109 -0
- data/lib/reve_ai/resources/images.rb +184 -0
- data/lib/reve_ai/response.rb +136 -0
- data/lib/reve_ai/version.rb +6 -0
- data/lib/reve_ai.rb +90 -0
- metadata +87 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module ReveAI
|
|
8
|
+
# HTTP layer for API communication.
|
|
9
|
+
module HTTP
|
|
10
|
+
# Low-level HTTP client using Faraday.
|
|
11
|
+
#
|
|
12
|
+
# Handles connection management, request/response processing, error handling,
|
|
13
|
+
# and automatic retries for transient failures.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
class Client
|
|
17
|
+
# @return [Hash{Integer => Class}] Mapping of HTTP status codes to error classes
|
|
18
|
+
ERROR_CODE_MAP = {
|
|
19
|
+
400 => BadRequestError,
|
|
20
|
+
401 => UnauthorizedError,
|
|
21
|
+
402 => InsufficientCreditsError,
|
|
22
|
+
403 => ForbiddenError,
|
|
23
|
+
404 => NotFoundError,
|
|
24
|
+
422 => UnprocessableEntityError,
|
|
25
|
+
429 => RateLimitError
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# @return [Array<Integer>] HTTP status codes that trigger automatic retry
|
|
29
|
+
RETRY_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
30
|
+
|
|
31
|
+
# @return [Configuration] Configuration instance for this client
|
|
32
|
+
attr_reader :configuration
|
|
33
|
+
|
|
34
|
+
# Creates a new HTTP client.
|
|
35
|
+
#
|
|
36
|
+
# @param configuration [Configuration] Configuration instance
|
|
37
|
+
# @api private
|
|
38
|
+
def initialize(configuration)
|
|
39
|
+
@configuration = configuration
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Makes a POST request to the API.
|
|
43
|
+
#
|
|
44
|
+
# @param path [String] API endpoint path (e.g., "/v1/image/create")
|
|
45
|
+
# @param body [Hash] Request body to send as JSON
|
|
46
|
+
#
|
|
47
|
+
# @return [Response] Parsed API response
|
|
48
|
+
#
|
|
49
|
+
# @raise [TimeoutError] if request times out
|
|
50
|
+
# @raise [ConnectionError] if connection fails
|
|
51
|
+
# @raise [NetworkError] for other network errors
|
|
52
|
+
# @raise [BadRequestError] on 400 responses
|
|
53
|
+
# @raise [UnauthorizedError] on 401 responses
|
|
54
|
+
# @raise [InsufficientCreditsError] on 402 responses
|
|
55
|
+
# @raise [ForbiddenError] on 403 responses
|
|
56
|
+
# @raise [NotFoundError] on 404 responses
|
|
57
|
+
# @raise [UnprocessableEntityError] on 422 responses
|
|
58
|
+
# @raise [RateLimitError] on 429 responses
|
|
59
|
+
# @raise [ServerError] on 5xx responses
|
|
60
|
+
#
|
|
61
|
+
# @api private
|
|
62
|
+
def post(path, body = {})
|
|
63
|
+
normalized_path = path.sub(%r{^/}, "")
|
|
64
|
+
response = connection.post(normalized_path) { |req| req.body = JSON.generate(body) }
|
|
65
|
+
handle_response(response)
|
|
66
|
+
rescue Faraday::TimeoutError => e
|
|
67
|
+
raise TimeoutError, "Request timed out: #{e.message}"
|
|
68
|
+
rescue Faraday::ConnectionFailed => e
|
|
69
|
+
handle_connection_failed(e)
|
|
70
|
+
rescue Faraday::Error => e
|
|
71
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Handles connection failed errors.
|
|
77
|
+
#
|
|
78
|
+
# Distinguishes between timeout errors (which may appear as connection failures)
|
|
79
|
+
# and actual connection failures.
|
|
80
|
+
#
|
|
81
|
+
# @param error [Faraday::ConnectionFailed] The connection error
|
|
82
|
+
# @raise [TimeoutError] if error indicates timeout
|
|
83
|
+
# @raise [ConnectionError] otherwise
|
|
84
|
+
# @api private
|
|
85
|
+
def handle_connection_failed(error)
|
|
86
|
+
raise TimeoutError, "Request timed out: #{error.message}" if error.message.include?("execution expired")
|
|
87
|
+
|
|
88
|
+
raise ConnectionError, "Connection failed: #{error.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the Faraday connection, creating it if needed.
|
|
92
|
+
#
|
|
93
|
+
# @return [Faraday::Connection] Configured Faraday connection
|
|
94
|
+
# @api private
|
|
95
|
+
def connection
|
|
96
|
+
@connection ||= build_connection
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Builds a new Faraday connection with all middleware.
|
|
100
|
+
#
|
|
101
|
+
# @return [Faraday::Connection] New connection instance
|
|
102
|
+
# @api private
|
|
103
|
+
def build_connection
|
|
104
|
+
Faraday.new(url: configuration.base_url) do |conn|
|
|
105
|
+
configure_retry(conn)
|
|
106
|
+
configure_headers(conn)
|
|
107
|
+
configure_timeouts(conn)
|
|
108
|
+
configure_logging(conn)
|
|
109
|
+
conn.adapter Faraday.default_adapter
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Configures retry middleware.
|
|
114
|
+
#
|
|
115
|
+
# @param conn [Faraday::Connection] Connection to configure
|
|
116
|
+
# @api private
|
|
117
|
+
def configure_retry(conn)
|
|
118
|
+
conn.request :retry, max: configuration.max_retries, interval: 0.5,
|
|
119
|
+
backoff_factor: 2, retry_statuses: RETRY_STATUSES, methods: [:post]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Configures request headers.
|
|
123
|
+
#
|
|
124
|
+
# @param conn [Faraday::Connection] Connection to configure
|
|
125
|
+
# @api private
|
|
126
|
+
def configure_headers(conn)
|
|
127
|
+
conn.headers["Authorization"] = "Bearer #{configuration.api_key}"
|
|
128
|
+
conn.headers["Content-Type"] = "application/json"
|
|
129
|
+
conn.headers["Accept"] = "application/json"
|
|
130
|
+
conn.headers["User-Agent"] = user_agent
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Configures connection and read timeouts.
|
|
134
|
+
#
|
|
135
|
+
# @param conn [Faraday::Connection] Connection to configure
|
|
136
|
+
# @api private
|
|
137
|
+
def configure_timeouts(conn)
|
|
138
|
+
conn.options.timeout = configuration.timeout
|
|
139
|
+
conn.options.open_timeout = configuration.open_timeout
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Configures response logging if debug mode is enabled.
|
|
143
|
+
#
|
|
144
|
+
# @param conn [Faraday::Connection] Connection to configure
|
|
145
|
+
# @api private
|
|
146
|
+
def configure_logging(conn)
|
|
147
|
+
conn.response :logger, configuration.logger if configuration.debug && configuration.logger
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the User-Agent header value.
|
|
151
|
+
#
|
|
152
|
+
# @return [String] User-Agent string
|
|
153
|
+
# @api private
|
|
154
|
+
def user_agent
|
|
155
|
+
"reve-ai-ruby/#{ReveAI::VERSION} Ruby/#{RUBY_VERSION}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Processes the HTTP response.
|
|
159
|
+
#
|
|
160
|
+
# @param response [Faraday::Response] Raw Faraday response
|
|
161
|
+
# @return [Response] Wrapped response on success
|
|
162
|
+
# @raise [APIError] on error responses
|
|
163
|
+
# @api private
|
|
164
|
+
def handle_response(response)
|
|
165
|
+
body = parse_body(response.body)
|
|
166
|
+
return build_success_response(response, body) if response.status.between?(200, 299)
|
|
167
|
+
|
|
168
|
+
raise_api_error(response.status, body, response.headers.to_h)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Builds a successful response object.
|
|
172
|
+
#
|
|
173
|
+
# @param response [Faraday::Response] Raw response
|
|
174
|
+
# @param body [Hash] Parsed body
|
|
175
|
+
# @return [Response] Response wrapper
|
|
176
|
+
# @api private
|
|
177
|
+
def build_success_response(response, body)
|
|
178
|
+
Response.new(status: response.status, headers: response.headers.to_h, body: body)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Parses the response body as JSON.
|
|
182
|
+
#
|
|
183
|
+
# @param body [String, nil] Raw response body
|
|
184
|
+
# @return [Hash] Parsed body, or empty hash if nil/empty
|
|
185
|
+
# @api private
|
|
186
|
+
def parse_body(body)
|
|
187
|
+
return {} if body.nil? || body.empty?
|
|
188
|
+
|
|
189
|
+
JSON.parse(body, symbolize_names: true)
|
|
190
|
+
rescue JSON::ParserError
|
|
191
|
+
{ raw: body }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Raises the appropriate API error for a status code.
|
|
195
|
+
#
|
|
196
|
+
# @param status [Integer] HTTP status code
|
|
197
|
+
# @param body [Hash] Parsed response body
|
|
198
|
+
# @param headers [Hash] Response headers
|
|
199
|
+
# @raise [APIError] Appropriate error subclass
|
|
200
|
+
# @api private
|
|
201
|
+
def raise_api_error(status, body, headers)
|
|
202
|
+
error_class = ERROR_CODE_MAP[status] || (status >= 500 ? ServerError : APIError)
|
|
203
|
+
message = body[:message] || body[:error] || "Unknown error"
|
|
204
|
+
raise error_class.new(message, status: status, body: body, headers: headers)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReveAI
|
|
4
|
+
# API resource classes.
|
|
5
|
+
#
|
|
6
|
+
# Each resource class provides methods for a related set of API operations.
|
|
7
|
+
module Resources
|
|
8
|
+
# Base class for API resources with common validation methods.
|
|
9
|
+
#
|
|
10
|
+
# Provides HTTP client access and input validation for all resource classes.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass and implement endpoint-specific methods
|
|
13
|
+
# @api private
|
|
14
|
+
class Base
|
|
15
|
+
# @return [Client] The client instance for this resource
|
|
16
|
+
attr_reader :client
|
|
17
|
+
|
|
18
|
+
# Creates a new resource instance.
|
|
19
|
+
#
|
|
20
|
+
# @param client [Client] The API client
|
|
21
|
+
# @api private
|
|
22
|
+
def initialize(client)
|
|
23
|
+
@client = client
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
# Returns the HTTP client for making requests.
|
|
29
|
+
#
|
|
30
|
+
# @return [HTTP::Client] HTTP client instance
|
|
31
|
+
# @api private
|
|
32
|
+
def http_client
|
|
33
|
+
client.http_client
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the client configuration.
|
|
37
|
+
#
|
|
38
|
+
# @return [Configuration] Configuration instance
|
|
39
|
+
# @api private
|
|
40
|
+
def configuration
|
|
41
|
+
client.configuration
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Makes a POST request to the API.
|
|
45
|
+
#
|
|
46
|
+
# @param path [String] API endpoint path
|
|
47
|
+
# @param body [Hash] Request body
|
|
48
|
+
# @return [Response] API response
|
|
49
|
+
# @api private
|
|
50
|
+
def post(path, body = {})
|
|
51
|
+
http_client.post(path, body)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Validates a text prompt.
|
|
55
|
+
#
|
|
56
|
+
# @param prompt [String] The prompt to validate
|
|
57
|
+
# @param field_name [String] Name for error messages (default: "Prompt")
|
|
58
|
+
# @raise [ValidationError] if prompt is nil, empty, or exceeds max length
|
|
59
|
+
# @api private
|
|
60
|
+
def validate_prompt!(prompt, field_name: "Prompt")
|
|
61
|
+
raise ValidationError, "#{field_name} is required" if prompt.nil? || prompt.empty?
|
|
62
|
+
|
|
63
|
+
max_length = Configuration::MAX_PROMPT_LENGTH
|
|
64
|
+
return unless prompt.length > max_length
|
|
65
|
+
|
|
66
|
+
raise ValidationError, "#{field_name} exceeds maximum length of #{max_length} characters"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validates an aspect ratio value.
|
|
70
|
+
#
|
|
71
|
+
# @param aspect_ratio [String, nil] The aspect ratio to validate
|
|
72
|
+
# @raise [ValidationError] if aspect ratio is invalid
|
|
73
|
+
# @api private
|
|
74
|
+
def validate_aspect_ratio!(aspect_ratio)
|
|
75
|
+
return if aspect_ratio.nil?
|
|
76
|
+
|
|
77
|
+
valid_ratios = Configuration::VALID_ASPECT_RATIOS
|
|
78
|
+
return if valid_ratios.include?(aspect_ratio)
|
|
79
|
+
|
|
80
|
+
raise ValidationError, "Invalid aspect_ratio '#{aspect_ratio}'. Must be one of: #{valid_ratios.join(", ")}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Validates a single reference image.
|
|
84
|
+
#
|
|
85
|
+
# @param image [String] Base64 encoded image data
|
|
86
|
+
# @raise [ValidationError] if image is nil or empty
|
|
87
|
+
# @api private
|
|
88
|
+
def validate_reference_image!(image)
|
|
89
|
+
raise ValidationError, "Reference image is required" if image.nil? || image.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validates an array of reference images.
|
|
93
|
+
#
|
|
94
|
+
# @param images [Array<String>] Array of base64 encoded images
|
|
95
|
+
# @raise [ValidationError] if images is nil, empty, exceeds max, or contains empty elements
|
|
96
|
+
# @api private
|
|
97
|
+
def validate_reference_images!(images)
|
|
98
|
+
raise ValidationError, "Reference images are required" if images.nil? || images.empty?
|
|
99
|
+
|
|
100
|
+
max_images = Configuration::MAX_REFERENCE_IMAGES
|
|
101
|
+
raise ValidationError, "Maximum #{max_images} reference images allowed" if images.length > max_images
|
|
102
|
+
|
|
103
|
+
images.each_with_index do |image, index|
|
|
104
|
+
raise ValidationError, "Reference image at index #{index} is empty" if image.nil? || image.empty?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReveAI
|
|
4
|
+
module Resources
|
|
5
|
+
# Image generation, editing, and remixing operations.
|
|
6
|
+
#
|
|
7
|
+
# Provides methods for creating images from text prompts, editing existing
|
|
8
|
+
# images, and remixing multiple reference images into new compositions.
|
|
9
|
+
#
|
|
10
|
+
# @example Generate an image from text
|
|
11
|
+
# client = ReveAI::Client.new(api_key: "your-key")
|
|
12
|
+
# result = client.images.create(
|
|
13
|
+
# prompt: "A sunset over mountains with a lake in the foreground",
|
|
14
|
+
# aspect_ratio: "16:9"
|
|
15
|
+
# )
|
|
16
|
+
# puts result.base64 # Base64 encoded PNG
|
|
17
|
+
#
|
|
18
|
+
# @example Edit an existing image
|
|
19
|
+
# result = client.images.edit(
|
|
20
|
+
# edit_instruction: "Make the sky more dramatic with storm clouds",
|
|
21
|
+
# reference_image: base64_encoded_original
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @example Remix multiple images
|
|
25
|
+
# result = client.images.remix(
|
|
26
|
+
# prompt: "Combine the style of <img>1</img> with the subject of <img>2</img>",
|
|
27
|
+
# reference_images: [style_image_base64, subject_image_base64]
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @note All images are returned as base64 encoded PNG data.
|
|
31
|
+
# @see https://api.reve.com/console/docs Reve API Documentation
|
|
32
|
+
class Images < Base
|
|
33
|
+
# @return [String] API endpoint for image creation
|
|
34
|
+
CREATE_ENDPOINT = "/v1/image/create"
|
|
35
|
+
|
|
36
|
+
# @return [String] API endpoint for image editing
|
|
37
|
+
EDIT_ENDPOINT = "/v1/image/edit"
|
|
38
|
+
|
|
39
|
+
# @return [String] API endpoint for image remixing
|
|
40
|
+
REMIX_ENDPOINT = "/v1/image/remix"
|
|
41
|
+
|
|
42
|
+
# Generates an image from a text prompt.
|
|
43
|
+
#
|
|
44
|
+
# @param prompt [String] Text description of the desired image (max 2560 chars)
|
|
45
|
+
# @param aspect_ratio [String, nil] Output aspect ratio (defaults to API default)
|
|
46
|
+
# @param version [String, nil] Model version to use (defaults to "latest")
|
|
47
|
+
#
|
|
48
|
+
# @option aspect_ratio [String] "16:9" Widescreen landscape
|
|
49
|
+
# @option aspect_ratio [String] "9:16" Portrait (phone)
|
|
50
|
+
# @option aspect_ratio [String] "3:2" Classic landscape
|
|
51
|
+
# @option aspect_ratio [String] "2:3" Classic portrait
|
|
52
|
+
# @option aspect_ratio [String] "4:3" Standard landscape
|
|
53
|
+
# @option aspect_ratio [String] "3:4" Standard portrait
|
|
54
|
+
# @option aspect_ratio [String] "1:1" Square
|
|
55
|
+
#
|
|
56
|
+
# @return [ImageResponse] Response containing base64 encoded image
|
|
57
|
+
#
|
|
58
|
+
# @raise [ValidationError] if prompt is empty or exceeds max length
|
|
59
|
+
# @raise [ValidationError] if aspect_ratio is invalid
|
|
60
|
+
# @raise [BadRequestError] if API rejects the request
|
|
61
|
+
# @raise [UnauthorizedError] if API key is invalid
|
|
62
|
+
# @raise [InsufficientCreditsError] if account has no credits
|
|
63
|
+
# @raise [RateLimitError] if rate limit is exceeded
|
|
64
|
+
#
|
|
65
|
+
# @example Basic usage
|
|
66
|
+
# result = client.images.create(prompt: "A cat wearing a top hat")
|
|
67
|
+
#
|
|
68
|
+
# @example With aspect ratio
|
|
69
|
+
# result = client.images.create(
|
|
70
|
+
# prompt: "A panoramic mountain landscape",
|
|
71
|
+
# aspect_ratio: "16:9"
|
|
72
|
+
# )
|
|
73
|
+
#
|
|
74
|
+
# @example Save to file
|
|
75
|
+
# result = client.images.create(prompt: "A sunset")
|
|
76
|
+
# File.binwrite("image.png", Base64.decode64(result.base64))
|
|
77
|
+
#
|
|
78
|
+
# @see https://api.reve.com/console/docs#/Image/create_v1_image_create_post
|
|
79
|
+
def create(prompt:, aspect_ratio: nil, version: nil)
|
|
80
|
+
validate_prompt!(prompt)
|
|
81
|
+
validate_aspect_ratio!(aspect_ratio)
|
|
82
|
+
|
|
83
|
+
body = { prompt: prompt }
|
|
84
|
+
body[:aspect_ratio] = aspect_ratio if aspect_ratio
|
|
85
|
+
body[:version] = version if version
|
|
86
|
+
|
|
87
|
+
response = post(CREATE_ENDPOINT, body)
|
|
88
|
+
ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Edits an existing image using text instructions.
|
|
92
|
+
#
|
|
93
|
+
# @param edit_instruction [String] Text description of how to edit the image (max 2560 chars)
|
|
94
|
+
# @param reference_image [String] Base64 encoded image to edit
|
|
95
|
+
# @param aspect_ratio [String, nil] Output aspect ratio (defaults to reference image ratio)
|
|
96
|
+
# @param version [String, nil] Model version to use (defaults to "latest")
|
|
97
|
+
#
|
|
98
|
+
# @return [ImageResponse] Response containing base64 encoded edited image
|
|
99
|
+
#
|
|
100
|
+
# @raise [ValidationError] if edit_instruction is empty or exceeds max length
|
|
101
|
+
# @raise [ValidationError] if reference_image is empty
|
|
102
|
+
# @raise [ValidationError] if aspect_ratio is invalid
|
|
103
|
+
# @raise [UnprocessableEntityError] if reference_image is not valid base64
|
|
104
|
+
# @raise [BadRequestError] if API rejects the request
|
|
105
|
+
# @raise [UnauthorizedError] if API key is invalid
|
|
106
|
+
#
|
|
107
|
+
# @example Change colors
|
|
108
|
+
# result = client.images.edit(
|
|
109
|
+
# edit_instruction: "Change the car color from red to blue",
|
|
110
|
+
# reference_image: original_image_base64
|
|
111
|
+
# )
|
|
112
|
+
#
|
|
113
|
+
# @example Add elements
|
|
114
|
+
# result = client.images.edit(
|
|
115
|
+
# edit_instruction: "Add a rainbow in the sky",
|
|
116
|
+
# reference_image: landscape_base64
|
|
117
|
+
# )
|
|
118
|
+
#
|
|
119
|
+
# @see https://api.reve.com/console/docs#/Image/edit_v1_image_edit_post
|
|
120
|
+
def edit(edit_instruction:, reference_image:, aspect_ratio: nil, version: nil)
|
|
121
|
+
validate_prompt!(edit_instruction, field_name: "Edit instruction")
|
|
122
|
+
validate_reference_image!(reference_image)
|
|
123
|
+
|
|
124
|
+
body = { edit_instruction: edit_instruction, reference_image: reference_image }
|
|
125
|
+
body[:aspect_ratio] = aspect_ratio if aspect_ratio
|
|
126
|
+
body[:version] = version if version
|
|
127
|
+
|
|
128
|
+
response = post(EDIT_ENDPOINT, body)
|
|
129
|
+
ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Creates a new image by remixing multiple reference images.
|
|
133
|
+
#
|
|
134
|
+
# Use `<img>N</img>` tags in the prompt to reference specific images,
|
|
135
|
+
# where N is the 1-based index into the reference_images array.
|
|
136
|
+
#
|
|
137
|
+
# @param prompt [String] Text description with optional image references (max 2560 chars)
|
|
138
|
+
# @param reference_images [Array<String>] Array of base64 encoded images (1-6 images)
|
|
139
|
+
# @param aspect_ratio [String, nil] Output aspect ratio (defaults to model's choice)
|
|
140
|
+
# @param version [String, nil] Model version to use (defaults to "latest")
|
|
141
|
+
#
|
|
142
|
+
# @return [ImageResponse] Response containing base64 encoded remixed image
|
|
143
|
+
#
|
|
144
|
+
# @raise [ValidationError] if prompt is empty or exceeds max length
|
|
145
|
+
# @raise [ValidationError] if reference_images is empty or exceeds 6 images
|
|
146
|
+
# @raise [ValidationError] if any reference image is empty
|
|
147
|
+
# @raise [ValidationError] if aspect_ratio is invalid
|
|
148
|
+
# @raise [BadRequestError] if API rejects the request
|
|
149
|
+
#
|
|
150
|
+
# @example Combine two images
|
|
151
|
+
# result = client.images.remix(
|
|
152
|
+
# prompt: "Combine the landscape from <img>1</img> with the sky from <img>2</img>",
|
|
153
|
+
# reference_images: [landscape_base64, sky_base64]
|
|
154
|
+
# )
|
|
155
|
+
#
|
|
156
|
+
# @example Style transfer
|
|
157
|
+
# result = client.images.remix(
|
|
158
|
+
# prompt: "Apply the artistic style of <img>1</img> to the photo <img>2</img>",
|
|
159
|
+
# reference_images: [artwork_base64, photo_base64]
|
|
160
|
+
# )
|
|
161
|
+
#
|
|
162
|
+
# @example Multiple references
|
|
163
|
+
# result = client.images.remix(
|
|
164
|
+
# prompt: "Create a scene with the dog from <img>1</img>, " \
|
|
165
|
+
# "the background from <img>2</img>, and lighting from <img>3</img>",
|
|
166
|
+
# reference_images: [dog_base64, background_base64, lighting_ref_base64]
|
|
167
|
+
# )
|
|
168
|
+
#
|
|
169
|
+
# @see https://api.reve.com/console/docs#/Image/remix_v1_image_remix_post
|
|
170
|
+
def remix(prompt:, reference_images:, aspect_ratio: nil, version: nil)
|
|
171
|
+
validate_prompt!(prompt)
|
|
172
|
+
validate_reference_images!(reference_images)
|
|
173
|
+
validate_aspect_ratio!(aspect_ratio)
|
|
174
|
+
|
|
175
|
+
body = { prompt: prompt, reference_images: reference_images }
|
|
176
|
+
body[:aspect_ratio] = aspect_ratio if aspect_ratio
|
|
177
|
+
body[:version] = version if version
|
|
178
|
+
|
|
179
|
+
response = post(REMIX_ENDPOINT, body)
|
|
180
|
+
ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReveAI
|
|
4
|
+
# Base response wrapper for API responses.
|
|
5
|
+
#
|
|
6
|
+
# Provides access to HTTP status, headers, and parsed response body.
|
|
7
|
+
#
|
|
8
|
+
# @see ImageResponse
|
|
9
|
+
class Response
|
|
10
|
+
# @return [Integer] HTTP status code
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
# @return [Hash] Response headers
|
|
14
|
+
attr_reader :headers
|
|
15
|
+
|
|
16
|
+
# @return [Hash] Parsed response body
|
|
17
|
+
attr_reader :body
|
|
18
|
+
|
|
19
|
+
# Creates a new response wrapper.
|
|
20
|
+
#
|
|
21
|
+
# @param status [Integer] HTTP status code
|
|
22
|
+
# @param headers [Hash] Response headers
|
|
23
|
+
# @param body [Hash] Parsed response body
|
|
24
|
+
def initialize(status:, headers:, body:)
|
|
25
|
+
@status = status
|
|
26
|
+
@headers = headers
|
|
27
|
+
@body = body
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Checks if the response indicates success (2xx status).
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean] true if status is between 200 and 299
|
|
33
|
+
def success?
|
|
34
|
+
status >= 200 && status < 300
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the request ID for this response.
|
|
38
|
+
#
|
|
39
|
+
# Useful for debugging and support requests.
|
|
40
|
+
#
|
|
41
|
+
# @return [String, nil] Request ID from body or headers
|
|
42
|
+
def request_id
|
|
43
|
+
body[:request_id] || headers["x-reve-request-id"]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Response wrapper for image generation API responses.
|
|
48
|
+
#
|
|
49
|
+
# Provides convenient accessors for image data, version info,
|
|
50
|
+
# content policy status, and credit usage.
|
|
51
|
+
#
|
|
52
|
+
# @example Accessing image data
|
|
53
|
+
# result = client.images.create(prompt: "A sunset")
|
|
54
|
+
# png_data = Base64.decode64(result.base64)
|
|
55
|
+
# File.binwrite("image.png", png_data)
|
|
56
|
+
#
|
|
57
|
+
# @example Checking content policy
|
|
58
|
+
# result = client.images.create(prompt: "...")
|
|
59
|
+
# if result.content_violation?
|
|
60
|
+
# puts "Content policy violated"
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# @example Tracking credit usage
|
|
64
|
+
# result = client.images.create(prompt: "A cat")
|
|
65
|
+
# puts "Used #{result.credits_used} credits, #{result.credits_remaining} remaining"
|
|
66
|
+
#
|
|
67
|
+
# @see Response
|
|
68
|
+
class ImageResponse < Response
|
|
69
|
+
# Returns the base64 encoded image data.
|
|
70
|
+
#
|
|
71
|
+
# The image is in PNG format. Use Base64.decode64 to get raw bytes.
|
|
72
|
+
#
|
|
73
|
+
# @return [String, nil] Base64 encoded PNG image data
|
|
74
|
+
#
|
|
75
|
+
# @example Save to file
|
|
76
|
+
# require "base64"
|
|
77
|
+
# png_bytes = Base64.decode64(result.image)
|
|
78
|
+
# File.binwrite("output.png", png_bytes)
|
|
79
|
+
def image
|
|
80
|
+
body[:image]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Alias for {#image}.
|
|
84
|
+
#
|
|
85
|
+
# @return [String, nil] Base64 encoded PNG image data
|
|
86
|
+
# @see #image
|
|
87
|
+
def base64
|
|
88
|
+
image
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the model version used for generation.
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil] Model version (e.g., "reve-create@20250915")
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# result.version # => "reve-create@20250915"
|
|
97
|
+
def version
|
|
98
|
+
body[:version] || headers["x-reve-version"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Checks if the generated image violates content policy.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] true if content policy was violated
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# if result.content_violation?
|
|
107
|
+
# puts "Warning: Content policy violated"
|
|
108
|
+
# end
|
|
109
|
+
def content_violation?
|
|
110
|
+
body[:content_violation] == true ||
|
|
111
|
+
headers["x-reve-content-violation"] == "true"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns the number of credits used for this request.
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer, nil] Credits consumed by this generation
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# puts "This request used #{result.credits_used} credits"
|
|
120
|
+
def credits_used
|
|
121
|
+
body[:credits_used] || headers["x-reve-credits-used"]&.to_i
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns the number of credits remaining after this request.
|
|
125
|
+
#
|
|
126
|
+
# @return [Integer, nil] Remaining credit balance
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# if result.credits_remaining < 100
|
|
130
|
+
# puts "Warning: Low credit balance"
|
|
131
|
+
# end
|
|
132
|
+
def credits_remaining
|
|
133
|
+
body[:credits_remaining] || headers["x-reve-credits-remaining"]&.to_i
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|