reve_ai 0.1.1 → 0.2.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.
@@ -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
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module ReveAI
6
+ module Resources
7
+ class V2
8
+ # v2 image generation operations.
9
+ #
10
+ # The v2 create endpoint unifies the v1 create, edit, and remix
11
+ # workflows: a single prompt plus an ordered list of reference images.
12
+ # JSON responses include a structured +layout+ alongside the image.
13
+ #
14
+ # @example Generate an image from text
15
+ # result = client.v2.images.create(prompt: "A sunset over mountains")
16
+ # result.base64 # Base64 encoded PNG
17
+ # result.layout # => { prompt: "...", regions: [...] }
18
+ #
19
+ # @note Image generation requests commonly take 40-80 seconds; the API
20
+ # documentation mandates request timeouts of at least 120 seconds
21
+ # (the gem default).
22
+ #
23
+ # @see https://api.reve.com/console/docs Reve API Documentation
24
+ class Images < Base
25
+ # @return [String] API endpoint for v2 image creation
26
+ CREATE_ENDPOINT = "/v2/image/create"
27
+
28
+ # Generates an image from a text prompt with optional reference images.
29
+ #
30
+ # Reference images are addressed from the prompt with `<frame>N</frame>`
31
+ # tags, where N is the 0-based index into the +references+ array (so
32
+ # the first reference is `<frame>0</frame>`).
33
+ #
34
+ # @param prompt [String] Text description of the desired image
35
+ # (max 4000 chars); may contain `<frame>N</frame>` reference tags
36
+ # @param references [Array<Hash>, nil] Up to 8 reference images
37
+ # ({Configuration::V2_MAX_REFERENCES}). Each entry is a Hash with
38
+ # exactly one of +data+ (base64 encoded image String) or +ref+
39
+ # (identifier String: +"id:<uuid>"+ for a previously stored image or
40
+ # generation, +"reference:@<name>"+ for a named reference in your
41
+ # project). Entries are serialized as given.
42
+ # @param aspect_ratio [String, nil] Output aspect ratio (defaults to
43
+ # the API default of "auto", which lets the model pick); v2 supports
44
+ # the full set in {Configuration::ASPECT_RATIOS}, including "auto"
45
+ # and "4:1"
46
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps, each
47
+ # with a +process+ key (e.g., { process: "upscale", upscale_factor: 2 })
48
+ # @param test_time_scaling [Numeric, nil] Scaling factor (1-15);
49
+ # values above 1 cost more credits
50
+ # @param version [String, nil] Optional public model version alias,
51
+ # passed through as-is (e.g., "latest", "reve-v2-create@260601")
52
+ # @param accept [String, nil] Per-request Accept header: "image/png",
53
+ # "image/jpeg", or "image/webp" for a binary image response, or
54
+ # "application/json" (default)
55
+ # @param breadcrumb [String, nil] Request tracking value sent as the
56
+ # +breadcrumb+ query parameter; ignored by the API
57
+ #
58
+ # @return [ImageResponse] Response containing the image and, on JSON
59
+ # responses, the generated +layout+
60
+ #
61
+ # @raise [ValidationError] if prompt is empty or exceeds 4000 characters
62
+ # @raise [ValidationError] if references is malformed or exceeds 8 entries
63
+ # @raise [ValidationError] if aspect_ratio is invalid
64
+ # @raise [ValidationError] if postprocessing or test_time_scaling is invalid
65
+ # @raise [BadRequestError] if API rejects the request
66
+ # @raise [UnauthorizedError] if API key is invalid
67
+ # @raise [InsufficientCreditsError] if account has no credits
68
+ # @raise [RateLimitError] if rate limit is exceeded
69
+ #
70
+ # @example Text-to-image (no references)
71
+ # result = client.v2.images.create(
72
+ # prompt: "A serene mountain landscape at sunset",
73
+ # aspect_ratio: "16:9"
74
+ # )
75
+ #
76
+ # @example Edit-style: one reference addressed as <frame>0</frame>
77
+ # result = client.v2.images.create(
78
+ # prompt: "Remove the people in the background of <frame>0</frame>.",
79
+ # references: [{ data: original_image_base64 }]
80
+ # )
81
+ #
82
+ # @example Remix-style: combine two references
83
+ # result = client.v2.images.create(
84
+ # prompt: "The woman from <frame>0</frame> driving the car from <frame>1</frame>.",
85
+ # references: [{ data: woman_base64 }, { data: car_base64 }]
86
+ # )
87
+ #
88
+ # @note v2 images are significantly larger than v1 images; the API
89
+ # documentation suggests capping the output size with
90
+ # +postprocessing: [{ process: "fit_image", max_dim: 2048 }]+.
91
+ # @note The API documentation does not recommend +test_time_scaling+
92
+ # for v2 models.
93
+ #
94
+ # @see https://api.reve.com/console/docs Reve API Documentation
95
+ def create(prompt:, references: nil, aspect_ratio: nil, postprocessing: nil,
96
+ test_time_scaling: nil, version: nil, accept: nil, breadcrumb: nil)
97
+ validate_prompt!(prompt, max_length: Configuration::V2_MAX_PROMPT_LENGTH)
98
+ validate_references!(references)
99
+ validate_aspect_ratio!(aspect_ratio, Configuration::ASPECT_RATIOS)
100
+ validate_postprocessing!(postprocessing)
101
+ validate_test_time_scaling!(test_time_scaling)
102
+
103
+ body = build_create_body(prompt: prompt, references: references, aspect_ratio: aspect_ratio,
104
+ postprocessing: postprocessing, test_time_scaling: test_time_scaling,
105
+ version: version)
106
+ params = breadcrumb ? { breadcrumb: breadcrumb } : nil
107
+
108
+ response = post(CREATE_ENDPOINT, body, params: params, accept: accept)
109
+ ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
110
+ end
111
+
112
+ private
113
+
114
+ # Builds the request body for the create endpoint.
115
+ #
116
+ # @return [Hash] Request body with only the provided options
117
+ # @api private
118
+ def build_create_body(prompt:, references:, aspect_ratio:, postprocessing:, test_time_scaling:, version:)
119
+ body = { prompt: prompt }
120
+ body[:references] = references if references
121
+ body[:aspect_ratio] = aspect_ratio if aspect_ratio
122
+ body[:postprocessing] = postprocessing if postprocessing
123
+ body[:test_time_scaling] = test_time_scaling if test_time_scaling
124
+ body[:version] = version if version
125
+ body
126
+ end
127
+
128
+ # Validates the references array.
129
+ #
130
+ # @param references [Array<Hash>, nil] Reference image entries
131
+ # @raise [ValidationError] if not an Array, exceeds the maximum, or
132
+ # contains a malformed entry
133
+ # @api private
134
+ def validate_references!(references)
135
+ return if references.nil?
136
+
137
+ unless references.is_a?(Array)
138
+ raise ValidationError, "References must be an Array of Hashes with exactly one of 'data' or 'ref'"
139
+ end
140
+
141
+ max = Configuration::V2_MAX_REFERENCES
142
+ raise ValidationError, "Maximum #{max} references allowed" if references.length > max
143
+
144
+ references.each_with_index { |reference, index| validate_reference!(reference, index) }
145
+ end
146
+
147
+ # Validates a single reference entry.
148
+ #
149
+ # @param reference [Hash] Reference entry with exactly one of +data+ or +ref+
150
+ # @param index [Integer] Position in the references array (for error messages)
151
+ # @raise [ValidationError] if the entry is not a Hash, has both or
152
+ # neither of +data+/+ref+, or has a non-String value
153
+ # @api private
154
+ def validate_reference!(reference, index)
155
+ unless reference.is_a?(Hash)
156
+ raise ValidationError, "Reference at index #{index} must be a Hash with exactly one of 'data' or 'ref'"
157
+ end
158
+
159
+ data = reference[:data] || reference["data"]
160
+ ref = reference[:ref] || reference["ref"]
161
+
162
+ if data.nil? == ref.nil?
163
+ raise ValidationError, "Reference at index #{index} must include exactly one of 'data' or 'ref'"
164
+ end
165
+
166
+ if data.nil?
167
+ validate_reference_value!(ref, "ref", index)
168
+ else
169
+ validate_reference_value!(data, "data", index)
170
+ end
171
+ end
172
+
173
+ # Validates a reference +data+ or +ref+ value.
174
+ #
175
+ # @param value [Object] The data or ref value
176
+ # @param key [String] Which key the value came from ("data" or "ref")
177
+ # @param index [Integer] Position in the references array
178
+ # @raise [ValidationError] if value is not a non-empty String
179
+ # @api private
180
+ def validate_reference_value!(value, key, index)
181
+ return if value.is_a?(String) && !value.empty?
182
+
183
+ raise ValidationError, "Reference at index #{index} '#{key}' must be a non-empty String"
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end