reve_ai 0.1.1 → 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.
@@ -39,12 +39,46 @@ module ReveAI
39
39
  @configuration = configuration
40
40
  end
41
41
 
42
+ # Makes a GET request to the API.
43
+ #
44
+ # @param path [String] API endpoint path (e.g., "/v1/image/effect")
45
+ # @param params [Hash, nil] Query parameters to merge into the URL
46
+ # (e.g., { source: "project" })
47
+ #
48
+ # @return [Response] Parsed API response
49
+ #
50
+ # @raise [TimeoutError] if request times out
51
+ # @raise [ConnectionError] if connection fails
52
+ # @raise [NetworkError] for other network errors
53
+ # @raise [BadRequestError] on 400 responses
54
+ # @raise [UnauthorizedError] on 401 responses
55
+ # @raise [InsufficientCreditsError] on 402 responses
56
+ # @raise [ForbiddenError] on 403 responses
57
+ # @raise [NotFoundError] on 404 responses
58
+ # @raise [UnprocessableEntityError] on 422 responses
59
+ # @raise [RateLimitError] on 429 responses
60
+ # @raise [ServerError] on 5xx responses
61
+ #
62
+ # @api private
63
+ def get(path, params: nil)
64
+ normalized_path = path.sub(%r{^/}, "")
65
+ response = perform_request do |conn|
66
+ conn.get(normalized_path) { |req| req.params.update(params) if params }
67
+ end
68
+ handle_response(response)
69
+ end
70
+
42
71
  # Makes a POST request to the API.
43
72
  #
44
73
  # @param path [String] API endpoint path (e.g., "/v1/image/create")
45
74
  # @param body [Hash] Request body to send as JSON
75
+ # @param params [Hash, nil] Query parameters to merge into the URL
76
+ # (e.g., { breadcrumb: "my-tracking-value" })
77
+ # @param accept [String, nil] Per-request Accept header override
78
+ # (e.g., "image/webp"). The connection default stays "application/json".
46
79
  #
47
- # @return [Response] Parsed API response
80
+ # @return [Response] Parsed API response. When the response Content-Type
81
+ # is +image/*+, the body is the raw image String (bytes), not a Hash.
48
82
  #
49
83
  # @raise [TimeoutError] if request times out
50
84
  # @raise [ConnectionError] if connection fails
@@ -59,10 +93,31 @@ module ReveAI
59
93
  # @raise [ServerError] on 5xx responses
60
94
  #
61
95
  # @api private
62
- def post(path, body = {})
96
+ def post(path, body = {}, params: nil, accept: nil)
63
97
  normalized_path = path.sub(%r{^/}, "")
64
- response = connection.post(normalized_path) { |req| req.body = JSON.generate(body) }
98
+ response = perform_request do |conn|
99
+ conn.post(normalized_path) do |req|
100
+ req.params.update(params) if params
101
+ req.headers["Accept"] = accept if accept
102
+ req.body = JSON.generate(body)
103
+ end
104
+ end
65
105
  handle_response(response)
106
+ end
107
+
108
+ private
109
+
110
+ # Performs an HTTP request, mapping Faraday errors to gem errors.
111
+ #
112
+ # @yieldparam connection [Faraday::Connection] Connection to perform the request on
113
+ # @return [Faraday::Response] Raw Faraday response
114
+ #
115
+ # @raise [TimeoutError] if request times out
116
+ # @raise [ConnectionError] if connection fails
117
+ # @raise [NetworkError] for other network errors
118
+ # @api private
119
+ def perform_request
120
+ yield connection
66
121
  rescue Faraday::TimeoutError => e
67
122
  raise TimeoutError, "Request timed out: #{e.message}"
68
123
  rescue Faraday::ConnectionFailed => e
@@ -71,8 +126,6 @@ module ReveAI
71
126
  raise NetworkError, "Network error: #{e.message}"
72
127
  end
73
128
 
74
- private
75
-
76
129
  # Handles connection failed errors.
77
130
  #
78
131
  # Distinguishes between timeout errors (which may appear as connection failures)
@@ -116,7 +169,7 @@ module ReveAI
116
169
  # @api private
117
170
  def configure_retry(conn)
118
171
  conn.request :retry, max: configuration.max_retries, interval: 0.5,
119
- backoff_factor: 2, retry_statuses: RETRY_STATUSES, methods: [:post]
172
+ backoff_factor: 2, retry_statuses: RETRY_STATUSES, methods: %i[post get]
120
173
  end
121
174
 
122
175
  # Configures request headers.
@@ -162,7 +215,7 @@ module ReveAI
162
215
  # @raise [APIError] on error responses
163
216
  # @api private
164
217
  def handle_response(response)
165
- body = parse_body(response.body)
218
+ body = parse_body(response)
166
219
  return build_success_response(response, body) if response.status.between?(200, 299)
167
220
 
168
221
  raise_api_error(response.status, body, response.headers.to_h)
@@ -171,37 +224,66 @@ module ReveAI
171
224
  # Builds a successful response object.
172
225
  #
173
226
  # @param response [Faraday::Response] Raw response
174
- # @param body [Hash] Parsed body
227
+ # @param body [Hash, String] Parsed body, or raw bytes for binary responses
175
228
  # @return [Response] Response wrapper
176
229
  # @api private
177
230
  def build_success_response(response, body)
178
231
  Response.new(status: response.status, headers: response.headers.to_h, body: body)
179
232
  end
180
233
 
181
- # Parses the response body as JSON.
234
+ # Parses the response body.
235
+ #
236
+ # Bodies with an +image/*+ Content-Type are raw image bytes and are
237
+ # returned as-is; anything else is parsed as JSON.
182
238
  #
183
- # @param body [String, nil] Raw response body
184
- # @return [Hash] Parsed body, or empty hash if nil/empty
239
+ # @param response [Faraday::Response] Raw Faraday response
240
+ # @return [Hash, String] Parsed body, raw bytes String for image
241
+ # responses, or empty hash if body is nil/empty
185
242
  # @api private
186
- def parse_body(body)
187
- return {} if body.nil? || body.empty?
243
+ def parse_body(response)
244
+ raw = response.body
245
+ return {} if raw.nil? || raw.empty?
246
+ return raw if image_content?(response)
188
247
 
189
- JSON.parse(body, symbolize_names: true)
248
+ JSON.parse(raw, symbolize_names: true)
190
249
  rescue JSON::ParserError
191
- { raw: body }
250
+ { raw: raw }
251
+ end
252
+
253
+ # Checks whether the response carries raw image bytes.
254
+ #
255
+ # @param response [Faraday::Response] Raw Faraday response
256
+ # @return [Boolean] true if Content-Type starts with "image/"
257
+ # @api private
258
+ def image_content?(response)
259
+ response.headers["content-type"].to_s.start_with?("image/")
192
260
  end
193
261
 
194
262
  # Raises the appropriate API error for a status code.
195
263
  #
196
264
  # @param status [Integer] HTTP status code
197
- # @param body [Hash] Parsed response body
265
+ # @param body [Hash, String] Parsed response body
198
266
  # @param headers [Hash] Response headers
199
267
  # @raise [APIError] Appropriate error subclass
200
268
  # @api private
201
269
  def raise_api_error(status, body, headers)
202
270
  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)
271
+ raise error_class.new(extract_error_message(body, headers), status: status, body: body, headers: headers)
272
+ end
273
+
274
+ # Extracts the error message from the response body.
275
+ #
276
+ # Falls back to the X-Reve-Error-Code header when the body carries no
277
+ # message: with an image Accept header, the API answers errors with a
278
+ # small grey image instead of a JSON error body.
279
+ #
280
+ # @param body [Hash, String] Parsed response body
281
+ # @param headers [Hash] Response headers
282
+ # @return [String] Error message
283
+ # @api private
284
+ def extract_error_message(body, headers)
285
+ from_body = body.is_a?(Hash) ? body[:message] || body[:error] : nil
286
+ from_body || headers["x-reve-error-code"] || "Unknown error"
205
287
  end
206
288
  end
207
289
  end
@@ -41,26 +41,41 @@ module ReveAI
41
41
  client.configuration
42
42
  end
43
43
 
44
+ # Makes a GET request to the API.
45
+ #
46
+ # @param path [String] API endpoint path
47
+ # @param params [Hash, nil] Query parameters to merge into the URL
48
+ # @return [Response] API response
49
+ # @api private
50
+ def get(path, params: nil)
51
+ http_client.get(path, params: params)
52
+ end
53
+
44
54
  # Makes a POST request to the API.
45
55
  #
46
56
  # @param path [String] API endpoint path
47
57
  # @param body [Hash] Request body
58
+ # @param params [Hash, nil] Query parameters to merge into the URL
59
+ # (e.g., { breadcrumb: "my-tracking-value" })
60
+ # @param accept [String, nil] Per-request Accept header override
61
+ # (e.g., "image/webp")
48
62
  # @return [Response] API response
49
63
  # @api private
50
- def post(path, body = {})
51
- http_client.post(path, body)
64
+ def post(path, body = {}, params: nil, accept: nil)
65
+ http_client.post(path, body, params: params, accept: accept)
52
66
  end
53
67
 
54
68
  # Validates a text prompt.
55
69
  #
56
70
  # @param prompt [String] The prompt to validate
57
71
  # @param field_name [String] Name for error messages (default: "Prompt")
72
+ # @param max_length [Integer] Maximum allowed length in characters
73
+ # (default: v1 limit of {Configuration::MAX_PROMPT_LENGTH})
58
74
  # @raise [ValidationError] if prompt is nil, empty, or exceeds max length
59
75
  # @api private
60
- def validate_prompt!(prompt, field_name: "Prompt")
76
+ def validate_prompt!(prompt, field_name: "Prompt", max_length: Configuration::MAX_PROMPT_LENGTH)
61
77
  raise ValidationError, "#{field_name} is required" if prompt.nil? || prompt.empty?
62
78
 
63
- max_length = Configuration::MAX_PROMPT_LENGTH
64
79
  return unless prompt.length > max_length
65
80
 
66
81
  raise ValidationError, "#{field_name} exceeds maximum length of #{max_length} characters"
@@ -69,17 +84,58 @@ module ReveAI
69
84
  # Validates an aspect ratio value.
70
85
  #
71
86
  # @param aspect_ratio [String, nil] The aspect ratio to validate
87
+ # @param valid_ratios [Array<String>] Allowed aspect ratios
88
+ # (default: v1 set {Configuration::VALID_ASPECT_RATIOS})
72
89
  # @raise [ValidationError] if aspect ratio is invalid
73
90
  # @api private
74
- def validate_aspect_ratio!(aspect_ratio)
91
+ def validate_aspect_ratio!(aspect_ratio, valid_ratios = Configuration::VALID_ASPECT_RATIOS)
75
92
  return if aspect_ratio.nil?
76
-
77
- valid_ratios = Configuration::VALID_ASPECT_RATIOS
78
93
  return if valid_ratios.include?(aspect_ratio)
79
94
 
80
95
  raise ValidationError, "Invalid aspect_ratio '#{aspect_ratio}'. Must be one of: #{valid_ratios.join(", ")}"
81
96
  end
82
97
 
98
+ # Validates a postprocessing steps array.
99
+ #
100
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps, each
101
+ # requiring a +process+ key (e.g., { process: "upscale", upscale_factor: 2 })
102
+ # @raise [ValidationError] if not an Array of Hashes, or a step lacks a +process+ key
103
+ # @api private
104
+ def validate_postprocessing!(postprocessing)
105
+ return if postprocessing.nil?
106
+
107
+ unless postprocessing_steps?(postprocessing)
108
+ raise ValidationError, "Postprocessing must be an Array of Hashes with a 'process' key"
109
+ end
110
+
111
+ postprocessing.each_with_index do |step, index|
112
+ next if step.key?(:process) || step.key?("process")
113
+
114
+ raise ValidationError, "Postprocessing step at index #{index} must include a 'process' key"
115
+ end
116
+ end
117
+
118
+ # Checks whether a value is an Array of Hashes (postprocessing shape).
119
+ #
120
+ # @param value [Object] The value to check
121
+ # @return [Boolean] true if value is an Array of Hashes
122
+ # @api private
123
+ def postprocessing_steps?(value)
124
+ value.is_a?(Array) && value.all?(Hash)
125
+ end
126
+
127
+ # Validates a test_time_scaling value.
128
+ #
129
+ # @param value [Numeric, nil] Scaling factor (1-15)
130
+ # @raise [ValidationError] if not a Numeric between 1 and 15
131
+ # @api private
132
+ def validate_test_time_scaling!(value)
133
+ return if value.nil?
134
+ return if value.is_a?(Numeric) && value.between?(1, 15)
135
+
136
+ raise ValidationError, "test_time_scaling must be a number between 1 and 15"
137
+ end
138
+
83
139
  # Validates a single reference image.
84
140
  #
85
141
  # @param image [String] Base64 encoded image data
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ module Resources
5
+ # Effects listing operations.
6
+ #
7
+ # Lists the effects available to the project associated with the API key,
8
+ # including saved project effects and built-in presets. Names from this
9
+ # list can be applied to any generation via the +postprocessing+ parameter
10
+ # of the image resources (see Images#create).
11
+ #
12
+ # @example List all effects
13
+ # client = ReveAI::Client.new(api_key: "your-key")
14
+ # result = client.effects.list
15
+ # result.body[:effects].each { |effect| puts effect[:name] }
16
+ #
17
+ # @note The list returns effect names only — effect parameter definitions
18
+ # are not included. Configure effect presets in the Reve application,
19
+ # save them with a name, and apply that name from the API.
20
+ # @see https://api.reve.com/console/docs Reve API Documentation
21
+ class Effects < Base
22
+ # @return [String] API endpoint for listing effects
23
+ LIST_ENDPOINT = "/v1/image/effect"
24
+
25
+ # @return [Array<String>] Valid values for the +source+ filter
26
+ VALID_SOURCES = %w[all project preset].freeze
27
+
28
+ # Lists effects available to the project.
29
+ #
30
+ # The default response includes both saved project effects (+source+
31
+ # "saved") and built-in presets (+source+ "builtin"). Each entry in
32
+ # +body[:effects]+ carries +name+ and +source+, plus optional
33
+ # +description+ and +category+ (e.g., "color", "textures") when
34
+ # available. Use a returned +name+ as +effect_name+ in postprocessing
35
+ # requests.
36
+ #
37
+ # @param source [String, nil] Filter by effect origin: "all" (default),
38
+ # "project" (saved project effects only), or "preset" (builtin only)
39
+ # @param breadcrumb [String, nil] Request tracking label sent as the
40
+ # +breadcrumb+ query param; ignored by the API, searchable in the
41
+ # Usage page
42
+ #
43
+ # @return [Response] Response whose +body[:effects]+ is an Array of
44
+ # effect Hashes with +name+, +source+, and optional +description+
45
+ # and +category+ keys
46
+ #
47
+ # @raise [ValidationError] if source is not one of "all", "project", "preset"
48
+ # @raise [UnauthorizedError] if API key is invalid
49
+ # @raise [RateLimitError] if rate limit is exceeded
50
+ #
51
+ # @example List all effects
52
+ # result = client.effects.list
53
+ # result.body[:effects].map { |effect| effect[:name] }
54
+ #
55
+ # @example List only saved project effects
56
+ # result = client.effects.list(source: "project")
57
+ #
58
+ # @see https://api.reve.com/console/docs Reve API Documentation
59
+ def list(source: nil, breadcrumb: nil)
60
+ validate_source!(source)
61
+
62
+ params = {}
63
+ params[:source] = source if source
64
+ params[:breadcrumb] = breadcrumb if breadcrumb
65
+
66
+ get(LIST_ENDPOINT, params: params.empty? ? nil : params)
67
+ end
68
+
69
+ private
70
+
71
+ # Validates the source filter.
72
+ #
73
+ # @param source [String, nil] The source filter to validate
74
+ # @raise [ValidationError] if source is not one of {VALID_SOURCES}
75
+ # @api private
76
+ def validate_source!(source)
77
+ return if source.nil? || VALID_SOURCES.include?(source)
78
+
79
+ raise ValidationError, "Invalid source '#{source}'. Must be one of: #{VALID_SOURCES.join(", ")}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -23,11 +23,13 @@ module ReveAI
23
23
  #
24
24
  # @example Remix multiple images
25
25
  # result = client.images.remix(
26
- # prompt: "Combine the style of <img>1</img> with the subject of <img>2</img>",
26
+ # prompt: "Combine the style of <img>0</img> with the subject of <img>1</img>",
27
27
  # reference_images: [style_image_base64, subject_image_base64]
28
28
  # )
29
29
  #
30
- # @note All images are returned as base64 encoded PNG data.
30
+ # @note By default all images are returned as base64 encoded PNG data;
31
+ # pass accept: "image/png", "image/jpeg", or "image/webp" to any method
32
+ # for raw image bytes instead (see method docs).
31
33
  # @see https://api.reve.com/console/docs Reve API Documentation
32
34
  class Images < Base
33
35
  # @return [String] API endpoint for image creation
@@ -44,6 +46,22 @@ module ReveAI
44
46
  # @param prompt [String] Text description of the desired image (max 2560 chars)
45
47
  # @param aspect_ratio [String, nil] Output aspect ratio (defaults to API default)
46
48
  # @param version [String, nil] Model version to use (defaults to "latest")
49
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps applied after
50
+ # generation; each step requires a +process+ key:
51
+ # +upscale+ (+upscale_factor+ 2-4, adds credits cost),
52
+ # +remove_background+,
53
+ # +fit_image+ (+max_dim+/+max_width+/+max_height+, max 4096, free),
54
+ # +effect+ (+effect_name+, optional +effect_parameters+ overrides nested
55
+ # as +{ filter_id: { uniform_id: value } }+)
56
+ # @param test_time_scaling [Numeric, nil] Effort scaling factor 1-15
57
+ # (default 1); values above 1 add credits cost, values above 5 only
58
+ # occasionally improve results
59
+ # @param accept [String, nil] Response format: "application/json" (default)
60
+ # or "image/png", "image/jpeg", "image/webp" for raw image bytes, e.g.
61
+ # accept: "image/webp" returns the raw image via result.image (metadata
62
+ # moves to the X-Reve-* response headers)
63
+ # @param breadcrumb [String, nil] Request tracking label sent as the
64
+ # +breadcrumb+ query param; ignored by the API, searchable in the Usage page
47
65
  #
48
66
  # @option aspect_ratio [String] "16:9" Widescreen landscape
49
67
  # @option aspect_ratio [String] "9:16" Portrait (phone)
@@ -53,10 +71,13 @@ module ReveAI
53
71
  # @option aspect_ratio [String] "3:4" Standard portrait
54
72
  # @option aspect_ratio [String] "1:1" Square
55
73
  #
56
- # @return [ImageResponse] Response containing base64 encoded image
74
+ # @return [ImageResponse] Response containing base64 encoded image,
75
+ # or raw image bytes when +accept+ is an image format
57
76
  #
58
77
  # @raise [ValidationError] if prompt is empty or exceeds max length
59
78
  # @raise [ValidationError] if aspect_ratio is invalid
79
+ # @raise [ValidationError] if postprocessing is not an Array of Hashes with a +process+ key
80
+ # @raise [ValidationError] if test_time_scaling is not a number between 1 and 15
60
81
  # @raise [BadRequestError] if API rejects the request
61
82
  # @raise [UnauthorizedError] if API key is invalid
62
83
  # @raise [InsufficientCreditsError] if account has no credits
@@ -71,20 +92,38 @@ module ReveAI
71
92
  # aspect_ratio: "16:9"
72
93
  # )
73
94
  #
95
+ # @example With postprocessing (upscale, then fit within 2048px)
96
+ # result = client.images.create(
97
+ # prompt: "A panoramic mountain landscape",
98
+ # postprocessing: [{ process: "upscale", upscale_factor: 2 },
99
+ # { process: "fit_image", max_dim: 2048 }]
100
+ # )
101
+ #
102
+ # @example Request raw WebP bytes instead of JSON
103
+ # result = client.images.create(prompt: "A sunset", accept: "image/webp")
104
+ # File.binwrite("sunset.webp", result.image) # raw bytes, no Base64 decode
105
+ #
74
106
  # @example Save to file
75
107
  # result = client.images.create(prompt: "A sunset")
76
108
  # File.binwrite("image.png", Base64.decode64(result.base64))
77
109
  #
78
110
  # @see https://api.reve.com/console/docs#/Image/create_v1_image_create_post
79
- def create(prompt:, aspect_ratio: nil, version: nil)
111
+ def create(prompt:, aspect_ratio: nil, version: nil, postprocessing: nil, test_time_scaling: nil,
112
+ accept: nil, breadcrumb: nil)
80
113
  validate_prompt!(prompt)
81
114
  validate_aspect_ratio!(aspect_ratio)
115
+ validate_postprocessing!(postprocessing)
116
+ validate_test_time_scaling!(test_time_scaling)
82
117
 
83
118
  body = { prompt: prompt }
84
119
  body[:aspect_ratio] = aspect_ratio if aspect_ratio
85
120
  body[:version] = version if version
121
+ body[:postprocessing] = postprocessing if postprocessing
122
+ body[:test_time_scaling] = test_time_scaling if test_time_scaling
123
+
124
+ params = breadcrumb ? { breadcrumb: breadcrumb } : nil
86
125
 
87
- response = post(CREATE_ENDPOINT, body)
126
+ response = post(CREATE_ENDPOINT, body, params: params, accept: accept)
88
127
  ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
89
128
  end
90
129
 
@@ -94,12 +133,24 @@ module ReveAI
94
133
  # @param reference_image [String] Base64 encoded image to edit
95
134
  # @param aspect_ratio [String, nil] Output aspect ratio (defaults to reference image ratio)
96
135
  # @param version [String, nil] Model version to use (defaults to "latest")
136
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps applied after
137
+ # generation; see {#create} for the supported step shapes
138
+ # @param test_time_scaling [Numeric, nil] Effort scaling factor 1-15
139
+ # (default 1); values above 1 add credits cost
140
+ # @param accept [String, nil] Response format: "application/json" (default)
141
+ # or "image/png", "image/jpeg", "image/webp" for raw image bytes via
142
+ # result.image (metadata moves to the X-Reve-* response headers)
143
+ # @param breadcrumb [String, nil] Request tracking label sent as the
144
+ # +breadcrumb+ query param; ignored by the API, searchable in the Usage page
97
145
  #
98
- # @return [ImageResponse] Response containing base64 encoded edited image
146
+ # @return [ImageResponse] Response containing base64 encoded edited image,
147
+ # or raw image bytes when +accept+ is an image format
99
148
  #
100
149
  # @raise [ValidationError] if edit_instruction is empty or exceeds max length
101
150
  # @raise [ValidationError] if reference_image is empty
102
151
  # @raise [ValidationError] if aspect_ratio is invalid
152
+ # @raise [ValidationError] if postprocessing is not an Array of Hashes with a +process+ key
153
+ # @raise [ValidationError] if test_time_scaling is not a number between 1 and 15
103
154
  # @raise [UnprocessableEntityError] if reference_image is not valid base64
104
155
  # @raise [BadRequestError] if API rejects the request
105
156
  # @raise [UnauthorizedError] if API key is invalid
@@ -116,67 +167,100 @@ module ReveAI
116
167
  # reference_image: landscape_base64
117
168
  # )
118
169
  #
170
+ # @example Remove the background after editing
171
+ # result = client.images.edit(
172
+ # edit_instruction: "Change the car color from red to blue",
173
+ # reference_image: original_image_base64,
174
+ # postprocessing: [{ process: "remove_background" }]
175
+ # )
176
+ #
119
177
  # @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)
178
+ def edit(edit_instruction:, reference_image:, aspect_ratio: nil, version: nil, postprocessing: nil,
179
+ test_time_scaling: nil, accept: nil, breadcrumb: nil)
121
180
  validate_prompt!(edit_instruction, field_name: "Edit instruction")
122
181
  validate_reference_image!(reference_image)
182
+ validate_postprocessing!(postprocessing)
183
+ validate_test_time_scaling!(test_time_scaling)
123
184
 
124
185
  body = { edit_instruction: edit_instruction, reference_image: reference_image }
125
186
  body[:aspect_ratio] = aspect_ratio if aspect_ratio
126
187
  body[:version] = version if version
188
+ body[:postprocessing] = postprocessing if postprocessing
189
+ body[:test_time_scaling] = test_time_scaling if test_time_scaling
190
+
191
+ params = breadcrumb ? { breadcrumb: breadcrumb } : nil
127
192
 
128
- response = post(EDIT_ENDPOINT, body)
193
+ response = post(EDIT_ENDPOINT, body, params: params, accept: accept)
129
194
  ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
130
195
  end
131
196
 
132
197
  # Creates a new image by remixing multiple reference images.
133
198
  #
134
199
  # 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.
200
+ # where N is the 0-based index into the reference_images array.
136
201
  #
137
202
  # @param prompt [String] Text description with optional image references (max 2560 chars)
138
203
  # @param reference_images [Array<String>] Array of base64 encoded images (1-6 images)
139
204
  # @param aspect_ratio [String, nil] Output aspect ratio (defaults to model's choice)
140
205
  # @param version [String, nil] Model version to use (defaults to "latest")
206
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps applied after
207
+ # generation; see {#create} for the supported step shapes
208
+ # @param test_time_scaling [Numeric, nil] Effort scaling factor 1-15
209
+ # (default 1); values above 1 add credits cost
210
+ # @param accept [String, nil] Response format: "application/json" (default)
211
+ # or "image/png", "image/jpeg", "image/webp" for raw image bytes via
212
+ # result.image (metadata moves to the X-Reve-* response headers)
213
+ # @param breadcrumb [String, nil] Request tracking label sent as the
214
+ # +breadcrumb+ query param; ignored by the API, searchable in the Usage page
141
215
  #
142
- # @return [ImageResponse] Response containing base64 encoded remixed image
216
+ # @return [ImageResponse] Response containing base64 encoded remixed image,
217
+ # or raw image bytes when +accept+ is an image format
143
218
  #
144
219
  # @raise [ValidationError] if prompt is empty or exceeds max length
145
220
  # @raise [ValidationError] if reference_images is empty or exceeds 6 images
146
221
  # @raise [ValidationError] if any reference image is empty
147
222
  # @raise [ValidationError] if aspect_ratio is invalid
223
+ # @raise [ValidationError] if postprocessing is not an Array of Hashes with a +process+ key
224
+ # @raise [ValidationError] if test_time_scaling is not a number between 1 and 15
148
225
  # @raise [BadRequestError] if API rejects the request
149
226
  #
150
227
  # @example Combine two images
151
228
  # result = client.images.remix(
152
- # prompt: "Combine the landscape from <img>1</img> with the sky from <img>2</img>",
229
+ # prompt: "Combine the landscape from <img>0</img> with the sky from <img>1</img>",
153
230
  # reference_images: [landscape_base64, sky_base64]
154
231
  # )
155
232
  #
156
233
  # @example Style transfer
157
234
  # result = client.images.remix(
158
- # prompt: "Apply the artistic style of <img>1</img> to the photo <img>2</img>",
235
+ # prompt: "Apply the artistic style of <img>0</img> to the photo <img>1</img>",
159
236
  # reference_images: [artwork_base64, photo_base64]
160
237
  # )
161
238
  #
162
239
  # @example Multiple references
163
240
  # 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>",
241
+ # prompt: "Create a scene with the dog from <img>0</img>, " \
242
+ # "the background from <img>1</img>, and lighting from <img>2</img>",
166
243
  # reference_images: [dog_base64, background_base64, lighting_ref_base64]
167
244
  # )
168
245
  #
169
246
  # @see https://api.reve.com/console/docs#/Image/remix_v1_image_remix_post
170
- def remix(prompt:, reference_images:, aspect_ratio: nil, version: nil)
247
+ def remix(prompt:, reference_images:, aspect_ratio: nil, version: nil, postprocessing: nil,
248
+ test_time_scaling: nil, accept: nil, breadcrumb: nil)
171
249
  validate_prompt!(prompt)
172
250
  validate_reference_images!(reference_images)
173
251
  validate_aspect_ratio!(aspect_ratio)
252
+ validate_postprocessing!(postprocessing)
253
+ validate_test_time_scaling!(test_time_scaling)
174
254
 
175
255
  body = { prompt: prompt, reference_images: reference_images }
176
256
  body[:aspect_ratio] = aspect_ratio if aspect_ratio
177
257
  body[:version] = version if version
258
+ body[:postprocessing] = postprocessing if postprocessing
259
+ body[:test_time_scaling] = test_time_scaling if test_time_scaling
260
+
261
+ params = breadcrumb ? { breadcrumb: breadcrumb } : nil
178
262
 
179
- response = post(REMIX_ENDPOINT, body)
263
+ response = post(REMIX_ENDPOINT, body, params: params, accept: accept)
180
264
  ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
181
265
  end
182
266
  end