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.
@@ -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
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module ReveAI
6
+ module Resources
7
+ class V2
8
+ # v2 layout pipeline operations (experimental).
9
+ #
10
+ # The layout endpoints expose lower-level control over image
11
+ # composition: extracting structured layouts from images, generating
12
+ # layouts from prompts, and rendering images from layouts.
13
+ #
14
+ # @note Experimental: these endpoints require care and experimentation
15
+ # to achieve good results. For simple image generation and
16
+ # prompt-based editing, prefer {V2::Images#create}.
17
+ #
18
+ # @see https://api.reve.com/console/docs Reve API Documentation
19
+ class Layouts < Base
20
+ # @return [String] API endpoint for layout extraction
21
+ EXTRACT_ENDPOINT = "/v2/image/extract_layout"
22
+
23
+ # @return [String] API endpoint for layout generation
24
+ CREATE_ENDPOINT = "/v2/image/create_layout"
25
+
26
+ # @return [String] API endpoint for layout rendering
27
+ RENDER_ENDPOINT = "/v2/image/render_layout"
28
+
29
+ # Extracts a structured layout from an image.
30
+ #
31
+ # @param image [Hash] Source image: exactly one of +data+ (base64
32
+ # encoded image String) or +ref+ (identifier String: +"id:<uuid>"+
33
+ # or +"reference:@<name>"+)
34
+ # @param prompt [String, nil] Optional instruction for transforming
35
+ # the extracted layout (max 4000 chars)
36
+ # @param version [String, nil] Optional public model version alias
37
+ # @param breadcrumb [String, nil] Request tracking value sent as the
38
+ # +breadcrumb+ query parameter; ignored by the API
39
+ #
40
+ # @return [LayoutResponse] Response containing the extracted +layout+
41
+ #
42
+ # @raise [ValidationError] if image is malformed or prompt exceeds max length
43
+ #
44
+ # @example Extract a layout from an image
45
+ # result = client.v2.layouts.extract(image: { data: photo_base64 })
46
+ # result.layout # => { prompt: "...", regions: [...], width: 4672, height: 3520 }
47
+ #
48
+ # @note Experimental: layout extraction commonly takes 10-40 seconds.
49
+ def extract(image:, prompt: nil, version: nil, breadcrumb: nil)
50
+ validate_raw_image!(image, "Image")
51
+ validate_prompt!(prompt, max_length: Configuration::V2_MAX_PROMPT_LENGTH) if prompt
52
+
53
+ body = { image: image }
54
+ body[:prompt] = prompt if prompt
55
+ body[:version] = version if version
56
+
57
+ response = post(EXTRACT_ENDPOINT, body, params: breadcrumb_params(breadcrumb))
58
+ LayoutResponse.new(status: response.status, headers: response.headers, body: response.body)
59
+ end
60
+
61
+ # Generates (or edits) a structured layout without rendering an image.
62
+ #
63
+ # @param prompt [String, nil] Description of the desired layout
64
+ # (max 4000 chars); at least one of +prompt+ or +references+ is required
65
+ # @param references [Array<Hash>, nil] Up to 8 ordered compound
66
+ # references ({Configuration::V2_MAX_REFERENCES}); each entry may
67
+ # contain +image+ (a raw { data: }/{ ref: } image Hash), +layout+
68
+ # (a layout Hash), and/or +prompt+ (String) — at least one per entry
69
+ # @param commands [Array<Hash>, nil] Ordered imperative layout edits;
70
+ # each entry is a Hash with an +op+ key (add, place, shift, remove,
71
+ # keep, change) plus op-specific fields
72
+ # @param aspect_ratio [String, nil] Layout aspect ratio; supported set
73
+ # ({Configuration::ASPECT_RATIOS}), default "auto"
74
+ # @param version [String, nil] Optional public model version alias
75
+ # @param breadcrumb [String, nil] Request tracking value sent as the
76
+ # +breadcrumb+ query parameter; ignored by the API
77
+ #
78
+ # @return [LayoutResponse] Response containing the generated +layout+
79
+ #
80
+ # @raise [ValidationError] if neither prompt nor references is given,
81
+ # or any argument is malformed
82
+ #
83
+ # @example Free-form layout from a prompt
84
+ # result = client.v2.layouts.create(prompt: "a person at a cafe")
85
+ # result.layout[:regions] # => [{ label: "person", bbox: {...}, ... }]
86
+ #
87
+ # @note Experimental: layout generation commonly takes 10-40 seconds.
88
+ def create(prompt: nil, references: nil, commands: nil, aspect_ratio: nil, version: nil, breadcrumb: nil)
89
+ validate_prompt_or_references!(prompt, references)
90
+ validate_prompt!(prompt, max_length: Configuration::V2_MAX_PROMPT_LENGTH) if prompt
91
+ validate_compound_references!(references)
92
+ validate_commands!(commands)
93
+ validate_aspect_ratio!(aspect_ratio, Configuration::ASPECT_RATIOS)
94
+
95
+ body = build_create_body(prompt: prompt, references: references, commands: commands,
96
+ aspect_ratio: aspect_ratio, version: version)
97
+
98
+ response = post(CREATE_ENDPOINT, body, params: breadcrumb_params(breadcrumb))
99
+ LayoutResponse.new(status: response.status, headers: response.headers, body: response.body)
100
+ end
101
+
102
+ # Renders a final image from a target layout.
103
+ #
104
+ # @param layout [Hash] The layout to render; must include a non-empty
105
+ # +regions+ Array (each region: +label+, +prompt+, +bbox+ with
106
+ # normalized x0/y0/x1/y1; optional +parent+, +region_type+,
107
+ # +image_index+, +image_region_index+)
108
+ # @param references [Array<Hash>, nil] Up to 8 ordered compound
109
+ # references; same shape as {#create}
110
+ # @param postprocessing [Array<Hash>, nil] Postprocessing steps, each
111
+ # with a +process+ key (e.g., { process: "fit_image", max_dim: 2048 })
112
+ # @param version [String, nil] Optional public model version alias
113
+ # @param accept [String, nil] Per-request Accept header: "image/png",
114
+ # "image/jpeg", or "image/webp" for a binary image response, or
115
+ # "application/json" (default)
116
+ # @param breadcrumb [String, nil] Request tracking value sent as the
117
+ # +breadcrumb+ query parameter; ignored by the API
118
+ #
119
+ # @return [ImageResponse] Response containing the rendered image and
120
+ # the produced +layout+
121
+ #
122
+ # @raise [ValidationError] if layout is missing or malformed
123
+ #
124
+ # @example Render a layout to an image
125
+ # layout = { regions: [{ label: "cat", prompt: "a tabby cat",
126
+ # bbox: { x0: 0.2, y0: 0.2, x1: 0.8, y1: 0.8 } }] }
127
+ # result = client.v2.layouts.render(layout: layout)
128
+ # File.binwrite("cat.png", Base64.decode64(result.image))
129
+ #
130
+ # @note Experimental: rendering commonly takes 40-80 seconds.
131
+ def render(layout:, references: nil, postprocessing: nil, version: nil, accept: nil, breadcrumb: nil)
132
+ validate_layout!(layout)
133
+ validate_compound_references!(references)
134
+ validate_postprocessing!(postprocessing)
135
+
136
+ body = { layout: layout }
137
+ body[:references] = references if references
138
+ body[:postprocessing] = postprocessing if postprocessing
139
+ body[:version] = version if version
140
+
141
+ response = post(RENDER_ENDPOINT, body, params: breadcrumb_params(breadcrumb), accept: accept)
142
+ ImageResponse.new(status: response.status, headers: response.headers, body: response.body)
143
+ end
144
+
145
+ private
146
+
147
+ # Builds the request body for the create_layout endpoint.
148
+ #
149
+ # @return [Hash] Request body with only the provided options
150
+ # @api private
151
+ def build_create_body(prompt:, references:, commands:, aspect_ratio:, version:)
152
+ body = {}
153
+ body[:prompt] = prompt if prompt
154
+ body[:references] = references if references
155
+ body[:commands] = commands if commands
156
+ body[:aspect_ratio] = aspect_ratio if aspect_ratio
157
+ body[:version] = version if version
158
+ body
159
+ end
160
+
161
+ # Returns the query params Hash for a breadcrumb, or nil.
162
+ #
163
+ # @param breadcrumb [String, nil] Breadcrumb value
164
+ # @return [Hash, nil] Query params
165
+ # @api private
166
+ def breadcrumb_params(breadcrumb)
167
+ breadcrumb ? { breadcrumb: breadcrumb } : nil
168
+ end
169
+
170
+ # Reads a Hash value accepting symbol or string keys.
171
+ #
172
+ # @param hash [Hash] The Hash to read
173
+ # @param key [Symbol] The key to look up (symbol or string form)
174
+ # @return [Object, nil] The value, or nil when absent
175
+ # @api private
176
+ def fetch_value(hash, key)
177
+ hash[key] || hash[key.to_s]
178
+ end
179
+
180
+ # Validates that at least one of prompt or references is present.
181
+ #
182
+ # @raise [ValidationError] if both are nil/empty
183
+ # @api private
184
+ def validate_prompt_or_references!(prompt, references)
185
+ return if prompt && !prompt.empty?
186
+ return if references && !references.empty?
187
+
188
+ raise ValidationError, "At least one of prompt or references is required"
189
+ end
190
+
191
+ # Validates a raw image object ({ data: } or { ref: } shape).
192
+ #
193
+ # @param image [Object] The value to validate
194
+ # @param label [String] Label for error messages
195
+ # @raise [ValidationError] if not a Hash with exactly one non-empty
196
+ # String +data+ or +ref+ value
197
+ # @api private
198
+ def validate_raw_image!(image, label)
199
+ raise ValidationError, "#{label} must be a Hash with exactly one of 'data' or 'ref'" unless image.is_a?(Hash)
200
+
201
+ data = fetch_value(image, :data)
202
+ ref = fetch_value(image, :ref)
203
+
204
+ raise ValidationError, "#{label} must include exactly one of 'data' or 'ref'" if data.nil? == ref.nil?
205
+
206
+ value = data || ref
207
+ return if value.is_a?(String) && !value.empty?
208
+
209
+ raise ValidationError, "#{label} '#{data ? "data" : "ref"}' must be a non-empty String"
210
+ end
211
+
212
+ # Validates compound layout references.
213
+ #
214
+ # @param references [Array<Hash>, nil] Compound reference entries
215
+ # @raise [ValidationError] if not an Array of valid compound entries
216
+ # @api private
217
+ def validate_compound_references!(references)
218
+ return if references.nil?
219
+
220
+ unless references.is_a?(Array)
221
+ raise ValidationError, "References must be an Array of compound reference Hashes"
222
+ end
223
+
224
+ max = Configuration::V2_MAX_REFERENCES
225
+ raise ValidationError, "Maximum #{max} references allowed" if references.length > max
226
+
227
+ references.each_with_index { |reference, index| validate_compound_reference!(reference, index) }
228
+ end
229
+
230
+ # Validates a single compound reference entry.
231
+ #
232
+ # @param reference [Hash] Entry with at least one of image/layout/prompt
233
+ # @param index [Integer] Position in the references array
234
+ # @raise [ValidationError] if the entry carries none of the allowed
235
+ # fields, or carries a malformed image/layout
236
+ # @api private
237
+ def validate_compound_reference!(reference, index)
238
+ raise ValidationError, "Reference at index #{index} must be a Hash" unless reference.is_a?(Hash)
239
+
240
+ image = fetch_value(reference, :image)
241
+ layout = fetch_value(reference, :layout)
242
+ prompt = fetch_value(reference, :prompt)
243
+
244
+ if image.nil? && layout.nil? && prompt.nil?
245
+ raise ValidationError,
246
+ "Reference at index #{index} must include at least one of 'image', 'layout', 'prompt'"
247
+ end
248
+
249
+ validate_raw_image!(image, "Reference at index #{index} 'image'") if image
250
+ validate_reference_layout!(layout, index)
251
+ validate_reference_prompt!(prompt, index)
252
+ end
253
+
254
+ # Validates the layout value inside a compound reference.
255
+ #
256
+ # @param layout [Object] The value to check
257
+ # @param index [Integer] Position in the references array
258
+ # @raise [ValidationError] if present and not a Hash
259
+ # @api private
260
+ def validate_reference_layout!(layout, index)
261
+ return if layout.nil? || layout.is_a?(Hash)
262
+
263
+ raise ValidationError, "Reference at index #{index} 'layout' must be a Hash"
264
+ end
265
+
266
+ # Validates the prompt value inside a compound reference.
267
+ #
268
+ # @param prompt [Object] The value to check
269
+ # @param index [Integer] Position in the references array
270
+ # @raise [ValidationError] if present and not a non-empty String
271
+ # @api private
272
+ def validate_reference_prompt!(prompt, index)
273
+ return if prompt.nil?
274
+ return if prompt.is_a?(String) && !prompt.empty?
275
+
276
+ raise ValidationError, "Reference at index #{index} 'prompt' must be a non-empty String"
277
+ end
278
+
279
+ # Validates the target layout for rendering.
280
+ #
281
+ # @param layout [Object] The layout to validate
282
+ # @raise [ValidationError] if not a Hash with a non-empty regions Array
283
+ # @api private
284
+ def validate_layout!(layout)
285
+ regions = fetch_value(layout, :regions) if layout.is_a?(Hash)
286
+ return if regions.is_a?(Array) && !regions.empty?
287
+
288
+ raise ValidationError, "Layout must be a Hash with a non-empty 'regions' Array"
289
+ end
290
+
291
+ # Validates layout commands.
292
+ #
293
+ # @param commands [Array<Hash>, nil] Command entries
294
+ # @raise [ValidationError] if not an Array of Hashes with an +op+ key
295
+ # @api private
296
+ def validate_commands!(commands)
297
+ return if commands.nil?
298
+ raise ValidationError, "Commands must be an Array of Hashes with an 'op' key" unless commands.is_a?(Array)
299
+
300
+ commands.each_with_index do |command, index|
301
+ next if command.is_a?(Hash) && (command.key?(:op) || command.key?("op"))
302
+
303
+ raise ValidationError, "Command at index #{index} must be a Hash with an 'op' key"
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReveAI
4
+ module Resources
5
+ # Namespace for Reve API v2 resources.
6
+ #
7
+ # Accessed via `client.v2`. Groups the v2 image create endpoint and the
8
+ # experimental layout pipeline endpoints.
9
+ #
10
+ # @example Generate an image with the v2 API
11
+ # result = client.v2.images.create(prompt: "A sunset over mountains")
12
+ # result.layout # => { prompt: "...", regions: [...] }
13
+ #
14
+ # @see https://api.reve.com/console/docs Reve API Documentation
15
+ class V2
16
+ # @return [Client] The client instance for this namespace
17
+ attr_reader :client
18
+
19
+ # Creates a new v2 namespace.
20
+ #
21
+ # @param client [Client] The API client
22
+ # @api private
23
+ def initialize(client)
24
+ @client = client
25
+ end
26
+
27
+ # Returns the v2 images resource.
28
+ #
29
+ # @return [V2::Images] v2 image generation operations
30
+ def images
31
+ @images ||= Images.new(client)
32
+ end
33
+
34
+ # Returns the v2 layouts resource (experimental endpoints).
35
+ #
36
+ # @return [V2::Layouts] v2 layout pipeline operations
37
+ def layouts
38
+ @layouts ||= Layouts.new(client)
39
+ end
40
+ end
41
+ end
42
+ end