runway-ruby 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/.tool-versions +1 -0
- data/LICENSE.txt +21 -0
- data/OPENAPI_VALIDATION.md +100 -0
- data/README.md +789 -0
- data/Rakefile +11 -0
- data/lib/runway_ml/character_performance.rb +106 -0
- data/lib/runway_ml/client.rb +84 -0
- data/lib/runway_ml/contract_validator.rb +151 -0
- data/lib/runway_ml/errors.rb +179 -0
- data/lib/runway_ml/image_processor.rb +128 -0
- data/lib/runway_ml/image_to_video.rb +158 -0
- data/lib/runway_ml/media_processor.rb +343 -0
- data/lib/runway_ml/openapi_spec_loader.rb +120 -0
- data/lib/runway_ml/organization.rb +89 -0
- data/lib/runway_ml/sound_effect.rb +57 -0
- data/lib/runway_ml/spec_coverage_analyzer.rb +134 -0
- data/lib/runway_ml/speech_to_speech.rb +90 -0
- data/lib/runway_ml/task.rb +116 -0
- data/lib/runway_ml/test_client.rb +62 -0
- data/lib/runway_ml/text_to_speech.rb +52 -0
- data/lib/runway_ml/text_to_video.rb +103 -0
- data/lib/runway_ml/uploads.rb +74 -0
- data/lib/runway_ml/validator_builder.rb +95 -0
- data/lib/runway_ml/validators/base_validators.rb +338 -0
- data/lib/runway_ml/validators/character_performance_validator.rb +41 -0
- data/lib/runway_ml/validators/gen3a_turbo_validator.rb +48 -0
- data/lib/runway_ml/validators/gen4_turbo_validator.rb +50 -0
- data/lib/runway_ml/validators/prompt_image_validator.rb +97 -0
- data/lib/runway_ml/validators/sound_effect_validator.rb +27 -0
- data/lib/runway_ml/validators/speech_to_speech_validator.rb +57 -0
- data/lib/runway_ml/validators/text_to_speech_validator.rb +28 -0
- data/lib/runway_ml/validators/text_veo3_stable_validator.rb +35 -0
- data/lib/runway_ml/validators/text_veo3_validator.rb +33 -0
- data/lib/runway_ml/validators/uploads_validator.rb +44 -0
- data/lib/runway_ml/validators/veo3_stable_validator.rb +43 -0
- data/lib/runway_ml/validators/veo3_validator.rb +46 -0
- data/lib/runway_ml/validators/voice_dubbing_validator.rb +128 -0
- data/lib/runway_ml/validators/voice_isolation_validator.rb +35 -0
- data/lib/runway_ml/validators/voice_validator.rb +32 -0
- data/lib/runway_ml/version.rb +5 -0
- data/lib/runway_ml/voice_dubbing.rb +74 -0
- data/lib/runway_ml/voice_isolation.rb +57 -0
- data/lib/runway_ml.rb +81 -0
- data/lib/tasks/openapi.rake +33 -0
- metadata +90 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunwayML
|
|
4
|
+
# Builds validators from OpenAPI schema definitions
|
|
5
|
+
module ValidatorBuilder
|
|
6
|
+
class << self
|
|
7
|
+
def build_enum_validator(enum_values)
|
|
8
|
+
->(value, errors, field_name) {
|
|
9
|
+
unless enum_values.include?(value)
|
|
10
|
+
errors[field_name] = "must be one of: #{enum_values.join(', ')}, got #{value.inspect}"
|
|
11
|
+
end
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_string_length_validator(min_length: nil, max_length: nil)
|
|
16
|
+
->(value, errors, field_name) {
|
|
17
|
+
return if value.nil?
|
|
18
|
+
|
|
19
|
+
# Handle UTF-16 length for prompt text
|
|
20
|
+
if field_name.to_s.include?("prompt_text") || field_name.to_s.include?("promptText")
|
|
21
|
+
utf16_length = value.encode("UTF-16LE").bytesize / 2
|
|
22
|
+
length = utf16_length
|
|
23
|
+
else
|
|
24
|
+
length = value.to_s.length
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if min_length && length < min_length
|
|
28
|
+
errors[field_name] = "must be at least #{min_length} characters, got #{length}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if max_length && length > max_length
|
|
32
|
+
errors[field_name] = "must be at most #{max_length} characters, got #{length}"
|
|
33
|
+
end
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_numeric_range_validator(minimum: nil, maximum: nil)
|
|
38
|
+
->(value, errors, field_name) {
|
|
39
|
+
return if value.nil?
|
|
40
|
+
|
|
41
|
+
if minimum && value < minimum
|
|
42
|
+
errors[field_name] = "must be at least #{minimum}, got #{value}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if maximum && value > maximum
|
|
46
|
+
errors[field_name] = "must be at most #{maximum}, got #{value}"
|
|
47
|
+
end
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def extract_constraints_from_schema(schema)
|
|
52
|
+
constraints = {}
|
|
53
|
+
|
|
54
|
+
if schema["type"] == "string"
|
|
55
|
+
constraints[:string_length] = {
|
|
56
|
+
min_length: schema["minLength"],
|
|
57
|
+
max_length: schema["maxLength"]
|
|
58
|
+
}.compact
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if schema["type"] == "number" || schema["type"] == "integer"
|
|
62
|
+
constraints[:numeric_range] = {
|
|
63
|
+
minimum: schema["minimum"],
|
|
64
|
+
maximum: schema["maximum"]
|
|
65
|
+
}.compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if schema["enum"]
|
|
69
|
+
constraints[:enum] = schema["enum"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
constraints
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_from_openapi_property(property_schema, field_name)
|
|
76
|
+
constraints = extract_constraints_from_schema(property_schema)
|
|
77
|
+
validators = []
|
|
78
|
+
|
|
79
|
+
if constraints[:enum]
|
|
80
|
+
validators << build_enum_validator(constraints[:enum])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if constraints[:string_length].any?
|
|
84
|
+
validators << build_string_length_validator(**constraints[:string_length])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if constraints[:numeric_range].any?
|
|
88
|
+
validators << build_numeric_range_validator(**constraints[:numeric_range])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
validators
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunwayML
|
|
4
|
+
module Validators
|
|
5
|
+
class RatioValidator
|
|
6
|
+
def initialize(valid_ratios:)
|
|
7
|
+
@valid_ratios = valid_ratios
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate(ratio, errors)
|
|
11
|
+
unless valid_ratios.include?(ratio)
|
|
12
|
+
errors[:ratio] = "must be one of: #{valid_ratios.join(', ')}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :valid_ratios
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class PromptTextValidator
|
|
22
|
+
def initialize(max_length:, required: true)
|
|
23
|
+
@max_length = max_length
|
|
24
|
+
@required = required
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(prompt_text, errors)
|
|
28
|
+
if prompt_text.nil? || prompt_text.empty?
|
|
29
|
+
errors[:prompt_text] = "cannot be empty" if required
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
utf16_length = prompt_text.encode("UTF-16LE").bytesize / 2
|
|
34
|
+
if utf16_length > max_length
|
|
35
|
+
errors[:prompt_text] = "must be at most #{max_length} characters (UTF-16 code units), got #{utf16_length}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :max_length, :required
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class DurationValidator
|
|
45
|
+
def initialize(valid_values: nil, range: nil, exact: nil)
|
|
46
|
+
@valid_values = valid_values
|
|
47
|
+
@range = range
|
|
48
|
+
@exact = exact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate(duration, errors)
|
|
52
|
+
return unless duration
|
|
53
|
+
|
|
54
|
+
if exact
|
|
55
|
+
unless duration == exact
|
|
56
|
+
errors[:duration] = "must be exactly #{exact}"
|
|
57
|
+
end
|
|
58
|
+
elsif valid_values
|
|
59
|
+
unless valid_values.include?(duration)
|
|
60
|
+
errors[:duration] = "must be one of: #{valid_values.join(', ')}"
|
|
61
|
+
end
|
|
62
|
+
elsif range
|
|
63
|
+
unless range.include?(duration)
|
|
64
|
+
errors[:duration] = "must be between #{range.min} and #{range.max} seconds"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
attr_reader :valid_values, :range, :exact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class SeedValidator
|
|
75
|
+
def initialize(range:)
|
|
76
|
+
@range = range
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate(seed, errors)
|
|
80
|
+
return unless seed
|
|
81
|
+
|
|
82
|
+
unless range.include?(seed)
|
|
83
|
+
errors[:seed] = "must be between #{range.min} and #{range.max}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
attr_reader :range
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class PublicFigureThresholdValidator
|
|
93
|
+
def initialize(valid_thresholds:)
|
|
94
|
+
@valid_thresholds = valid_thresholds
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate(public_figure_threshold, errors)
|
|
98
|
+
return unless public_figure_threshold
|
|
99
|
+
|
|
100
|
+
unless valid_thresholds.include?(public_figure_threshold)
|
|
101
|
+
errors[:public_figure_threshold] = "must be one of: #{valid_thresholds.join(', ')}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
attr_reader :valid_thresholds
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class AudioValidator
|
|
111
|
+
def validate(audio, errors)
|
|
112
|
+
return if audio.nil?
|
|
113
|
+
|
|
114
|
+
unless [ true, false ].include?(audio)
|
|
115
|
+
errors[:audio] = "must be true or false"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class ImageUriValidator
|
|
121
|
+
VALID_CONTENT_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/webp" ].freeze
|
|
122
|
+
|
|
123
|
+
def validate(uri, errors, field: :prompt_image)
|
|
124
|
+
return unless uri.is_a?(String)
|
|
125
|
+
|
|
126
|
+
if uri.length < 13
|
|
127
|
+
errors[field] = "URI must be at least 13 characters"
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if uri.start_with?("https://")
|
|
132
|
+
validate_https_url(uri, errors, field)
|
|
133
|
+
elsif uri.start_with?("runway://")
|
|
134
|
+
validate_runway_uri(uri, errors, field)
|
|
135
|
+
elsif uri.start_with?("data:image/")
|
|
136
|
+
validate_data_uri(uri, errors, field)
|
|
137
|
+
else
|
|
138
|
+
errors[field] = "must be a valid HTTPS URL, Runway URI (runway://), or data URI (data:image/)"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def validate_https_url(uri, errors, field)
|
|
145
|
+
if uri.length > 2048
|
|
146
|
+
errors[field] = "HTTPS URL must be at most 2048 characters"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_runway_uri(uri, errors, field)
|
|
151
|
+
if uri.length > 5000
|
|
152
|
+
errors[field] = "Runway URI must be at most 5000 characters"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_data_uri(uri, errors, field)
|
|
157
|
+
if uri.length > 5242880
|
|
158
|
+
errors[field] = "Data URI must be at most 5242880 characters"
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
content_type_match = uri.match(/^data:(image\/[^;,]+)/)
|
|
163
|
+
return unless content_type_match
|
|
164
|
+
|
|
165
|
+
content_type = content_type_match[1]
|
|
166
|
+
return if VALID_CONTENT_TYPES.include?(content_type)
|
|
167
|
+
|
|
168
|
+
if content_type == "image/gif"
|
|
169
|
+
errors[field] = "GIF images are not supported. Use JPEG, PNG, or WebP"
|
|
170
|
+
else
|
|
171
|
+
errors[field] = "unsupported image type '#{content_type}'. Must be JPEG (image/jpeg or image/jpg), PNG (image/png), or WebP (image/webp)"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class VideoUriValidator
|
|
177
|
+
VALID_CONTENT_TYPES = [ "video/mp4", "video/quicktime", "video/x-matroska", "video/webm", "video/3gpp", "video/ogg", "video/x-msvideo", "video/x-flv", "video/mpeg" ].freeze
|
|
178
|
+
|
|
179
|
+
def validate(uri, errors, field: :video, max_data_uri_size: 16777216)
|
|
180
|
+
return unless uri.is_a?(String)
|
|
181
|
+
|
|
182
|
+
if uri.length < 13
|
|
183
|
+
errors[field] = "URI must be at least 13 characters"
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if uri.start_with?("https://")
|
|
188
|
+
validate_https_url(uri, errors, field)
|
|
189
|
+
elsif uri.start_with?("runway://")
|
|
190
|
+
validate_runway_uri(uri, errors, field)
|
|
191
|
+
elsif uri.start_with?("data:video/")
|
|
192
|
+
validate_data_uri(uri, errors, field, max_data_uri_size)
|
|
193
|
+
else
|
|
194
|
+
errors[field] = "must be a valid HTTPS URL, Runway URI (runway://), or data URI (data:video/)"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def validate_https_url(uri, errors, field)
|
|
201
|
+
if uri.length > 2048
|
|
202
|
+
errors[field] = "HTTPS URL must be at most 2048 characters"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def validate_runway_uri(uri, errors, field)
|
|
207
|
+
if uri.length > 5000
|
|
208
|
+
errors[field] = "Runway URI must be at most 5000 characters"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def validate_data_uri(uri, errors, field, max_size)
|
|
213
|
+
if uri.length > max_size
|
|
214
|
+
errors[field] = "Data URI must be at most #{max_size} characters"
|
|
215
|
+
return
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
content_type_match = uri.match(/^data:(video\/[^;,]+)/)
|
|
219
|
+
return unless content_type_match
|
|
220
|
+
|
|
221
|
+
content_type = content_type_match[1]
|
|
222
|
+
errors[field] = "unsupported video type '#{content_type}'" unless VALID_CONTENT_TYPES.include?(content_type)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class BodyControlValidator
|
|
227
|
+
def validate(body_control, errors)
|
|
228
|
+
return if body_control.nil?
|
|
229
|
+
|
|
230
|
+
unless [ true, false ].include?(body_control)
|
|
231
|
+
errors[:body_control] = "must be true or false"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class ExpressionIntensityValidator
|
|
237
|
+
def initialize(range: 1..5)
|
|
238
|
+
@range = range
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def validate(expression_intensity, errors)
|
|
242
|
+
return if expression_intensity.nil?
|
|
243
|
+
|
|
244
|
+
unless range.include?(expression_intensity)
|
|
245
|
+
errors[:expression_intensity] = "must be between #{range.min} and #{range.max}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
attr_reader :range
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class CharacterValidator
|
|
255
|
+
def initialize
|
|
256
|
+
@video_uri_validator = VideoUriValidator.new
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def validate(character, errors)
|
|
260
|
+
return if character.nil?
|
|
261
|
+
|
|
262
|
+
unless character.is_a?(Hash)
|
|
263
|
+
errors[:character] = "must be a Hash with 'type' and 'uri'"
|
|
264
|
+
return
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
type = character[:type]
|
|
268
|
+
uri = character[:uri]
|
|
269
|
+
|
|
270
|
+
unless [ "image", "video" ].include?(type)
|
|
271
|
+
errors[:character] = "type must be 'image' or 'video'"
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if type == "image"
|
|
276
|
+
# Reuse ImageUriValidator for image validation
|
|
277
|
+
ImageUriValidator.new.validate(uri, errors, field: :character)
|
|
278
|
+
elsif type == "video"
|
|
279
|
+
@video_uri_validator.validate(uri, errors, field: :character)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
attr_reader :video_uri_validator
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
class ReferenceVideoValidator
|
|
289
|
+
def initialize
|
|
290
|
+
@video_uri_validator = VideoUriValidator.new
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def validate(reference, errors)
|
|
294
|
+
return if reference.nil?
|
|
295
|
+
|
|
296
|
+
unless reference.is_a?(Hash)
|
|
297
|
+
errors[:reference] = "must be a Hash with 'type' and 'uri'"
|
|
298
|
+
return
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
type = reference[:type]
|
|
302
|
+
uri = reference[:uri]
|
|
303
|
+
|
|
304
|
+
unless type == "video"
|
|
305
|
+
errors[:reference] = "type must be 'video'"
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
@video_uri_validator.validate(uri, errors, field: :reference)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private
|
|
313
|
+
|
|
314
|
+
attr_reader :video_uri_validator
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
class UUIDValidator
|
|
318
|
+
# RFC 4122 UUID v4 pattern from OpenAPI spec
|
|
319
|
+
UUID_PATTERN = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$/
|
|
320
|
+
|
|
321
|
+
def validate(uuid, errors, field: :id)
|
|
322
|
+
if uuid.nil?
|
|
323
|
+
errors[field] = "cannot be nil"
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
unless uuid.is_a?(String)
|
|
328
|
+
errors[field] = "must be a string"
|
|
329
|
+
return
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
unless uuid.match?(UUID_PATTERN)
|
|
333
|
+
errors[field] = "must be a valid UUID (RFC 4122 v4)"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_validators"
|
|
4
|
+
|
|
5
|
+
module RunwayML
|
|
6
|
+
module Validators
|
|
7
|
+
class CharacterPerformanceValidator
|
|
8
|
+
def initialize
|
|
9
|
+
@ratio_validator = RatioValidator.new(
|
|
10
|
+
valid_ratios: [ "1280:720", "720:1280", "960:960", "1104:832", "832:1104", "1584:672" ]
|
|
11
|
+
)
|
|
12
|
+
@seed_validator = SeedValidator.new(range: 0..4294967295)
|
|
13
|
+
@body_control_validator = BodyControlValidator.new
|
|
14
|
+
@expression_intensity_validator = ExpressionIntensityValidator.new(range: 1..5)
|
|
15
|
+
@character_validator = CharacterValidator.new
|
|
16
|
+
@reference_video_validator = ReferenceVideoValidator.new
|
|
17
|
+
@public_figure_threshold_validator = PublicFigureThresholdValidator.new(
|
|
18
|
+
valid_thresholds: [ "auto", "low" ]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate(character:, reference:, ratio:, seed: nil, body_control: nil, expression_intensity: nil, public_figure_threshold: nil)
|
|
23
|
+
errors = {}
|
|
24
|
+
|
|
25
|
+
character_validator.validate(character, errors)
|
|
26
|
+
reference_video_validator.validate(reference, errors)
|
|
27
|
+
ratio_validator.validate(ratio, errors)
|
|
28
|
+
seed_validator.validate(seed, errors)
|
|
29
|
+
body_control_validator.validate(body_control, errors)
|
|
30
|
+
expression_intensity_validator.validate(expression_intensity, errors)
|
|
31
|
+
public_figure_threshold_validator.validate(public_figure_threshold, errors)
|
|
32
|
+
|
|
33
|
+
{ errors: errors }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :ratio_validator, :seed_validator, :body_control_validator, :expression_intensity_validator, :character_validator, :reference_video_validator, :public_figure_threshold_validator
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_validators"
|
|
4
|
+
require_relative "prompt_image_validator"
|
|
5
|
+
|
|
6
|
+
module RunwayML
|
|
7
|
+
module Validators
|
|
8
|
+
class Gen3aTurboValidator
|
|
9
|
+
def initialize(image_processor:)
|
|
10
|
+
@image_processor = image_processor
|
|
11
|
+
@ratio_validator = RatioValidator.new(valid_ratios: [ "768:1280", "1280:768" ])
|
|
12
|
+
@prompt_text_validator = PromptTextValidator.new(max_length: 1000, required: true)
|
|
13
|
+
@duration_validator = DurationValidator.new(valid_values: [ 5, 10 ])
|
|
14
|
+
@seed_validator = SeedValidator.new(range: 0..4294967295)
|
|
15
|
+
@public_figure_threshold_validator = PublicFigureThresholdValidator.new(
|
|
16
|
+
valid_thresholds: [ "auto", "low" ]
|
|
17
|
+
)
|
|
18
|
+
@prompt_image_validator = PromptImageValidator.new(
|
|
19
|
+
image_processor: image_processor,
|
|
20
|
+
array_size: 1..2,
|
|
21
|
+
valid_positions: [ "first", "last" ]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate(prompt_image:, prompt_text:, ratio:, duration:, seed:, public_figure_threshold:)
|
|
26
|
+
errors = {}
|
|
27
|
+
|
|
28
|
+
ratio_validator.validate(ratio, errors)
|
|
29
|
+
prompt_text_validator.validate(prompt_text, errors)
|
|
30
|
+
duration_validator.validate(duration, errors)
|
|
31
|
+
seed_validator.validate(seed, errors)
|
|
32
|
+
public_figure_threshold_validator.validate(public_figure_threshold, errors)
|
|
33
|
+
|
|
34
|
+
# Process and validate prompt_image
|
|
35
|
+
processed_prompt_image = image_processor.process(prompt_image, errors)
|
|
36
|
+
prompt_image_validator.validate(processed_prompt_image, errors)
|
|
37
|
+
|
|
38
|
+
{ errors: errors, processed_prompt_image: processed_prompt_image }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :image_processor, :ratio_validator, :prompt_text_validator,
|
|
44
|
+
:duration_validator, :seed_validator, :public_figure_threshold_validator,
|
|
45
|
+
:prompt_image_validator
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_validators"
|
|
4
|
+
require_relative "prompt_image_validator"
|
|
5
|
+
|
|
6
|
+
module RunwayML
|
|
7
|
+
module Validators
|
|
8
|
+
class Gen4TurboValidator
|
|
9
|
+
def initialize(image_processor:)
|
|
10
|
+
@image_processor = image_processor
|
|
11
|
+
@ratio_validator = RatioValidator.new(
|
|
12
|
+
valid_ratios: [ "1280:720", "720:1280", "1104:832", "832:1104", "960:960", "1584:672" ]
|
|
13
|
+
)
|
|
14
|
+
@prompt_text_validator = PromptTextValidator.new(max_length: 1000, required: true)
|
|
15
|
+
@duration_validator = DurationValidator.new(range: 2..10)
|
|
16
|
+
@seed_validator = SeedValidator.new(range: 0..4294967295)
|
|
17
|
+
@public_figure_threshold_validator = PublicFigureThresholdValidator.new(
|
|
18
|
+
valid_thresholds: [ "auto", "low" ]
|
|
19
|
+
)
|
|
20
|
+
@prompt_image_validator = PromptImageValidator.new(
|
|
21
|
+
image_processor: image_processor,
|
|
22
|
+
array_size: 1,
|
|
23
|
+
valid_positions: [ "first" ]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(prompt_image:, prompt_text:, ratio:, duration:, seed:, public_figure_threshold:)
|
|
28
|
+
errors = {}
|
|
29
|
+
|
|
30
|
+
ratio_validator.validate(ratio, errors)
|
|
31
|
+
prompt_text_validator.validate(prompt_text, errors)
|
|
32
|
+
duration_validator.validate(duration, errors)
|
|
33
|
+
seed_validator.validate(seed, errors)
|
|
34
|
+
public_figure_threshold_validator.validate(public_figure_threshold, errors)
|
|
35
|
+
|
|
36
|
+
# Process and validate prompt_image
|
|
37
|
+
processed_prompt_image = image_processor.process(prompt_image, errors)
|
|
38
|
+
prompt_image_validator.validate(processed_prompt_image, errors)
|
|
39
|
+
|
|
40
|
+
{ errors: errors, processed_prompt_image: processed_prompt_image }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :image_processor, :ratio_validator, :prompt_text_validator,
|
|
46
|
+
:duration_validator, :seed_validator, :public_figure_threshold_validator,
|
|
47
|
+
:prompt_image_validator
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_validators"
|
|
4
|
+
|
|
5
|
+
module RunwayML
|
|
6
|
+
module Validators
|
|
7
|
+
class PromptImageValidator
|
|
8
|
+
def initialize(image_processor:, array_size:, valid_positions:, only_first_frame: false)
|
|
9
|
+
@image_processor = image_processor
|
|
10
|
+
@array_size = array_size
|
|
11
|
+
@valid_positions = valid_positions
|
|
12
|
+
@only_first_frame = only_first_frame
|
|
13
|
+
@image_uri_validator = ImageUriValidator.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(prompt_image, errors)
|
|
17
|
+
if prompt_image.nil?
|
|
18
|
+
errors[:prompt_image] = "is required"
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if prompt_image.is_a?(String)
|
|
23
|
+
image_uri_validator.validate(prompt_image, errors)
|
|
24
|
+
elsif prompt_image.is_a?(Array)
|
|
25
|
+
validate_array_format(prompt_image, errors)
|
|
26
|
+
else
|
|
27
|
+
errors[:prompt_image] = "must be a string or an array"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :image_processor, :array_size, :valid_positions, :only_first_frame, :image_uri_validator
|
|
34
|
+
|
|
35
|
+
def validate_array_format(prompt_image, errors)
|
|
36
|
+
if array_size.is_a?(Range)
|
|
37
|
+
unless array_size.include?(prompt_image.length)
|
|
38
|
+
errors[:prompt_image] = "array must contain #{array_size.min} to #{array_size.max} items"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
elsif prompt_image.length != array_size
|
|
42
|
+
errors[:prompt_image] = "array must contain exactly #{array_size} item#{'s' if array_size > 1}"
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
validate_array_items(prompt_image, errors)
|
|
47
|
+
validate_array_rules(prompt_image, errors)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_array_items(prompt_image, errors)
|
|
51
|
+
prompt_image.each_with_index do |item, index|
|
|
52
|
+
unless item.is_a?(Hash)
|
|
53
|
+
errors[:prompt_image] = "array items must be objects with 'uri' and 'position' fields"
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
unless item.key?(:uri) || item.key?("uri")
|
|
58
|
+
errors[:prompt_image] = "array item #{index + 1} must have 'uri' field"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless item.key?(:position) || item.key?("position")
|
|
62
|
+
errors[:prompt_image] = "array item #{index + 1} must have 'position' field"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
position = item[:position] || item["position"]
|
|
66
|
+
if position && !valid_positions.include?(position)
|
|
67
|
+
errors[:prompt_image] = "position must be one of: #{valid_positions.join(', ')}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
uri = item[:uri] || item["uri"]
|
|
71
|
+
image_uri_validator.validate(uri, errors) if uri
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_array_rules(prompt_image, errors)
|
|
76
|
+
return if errors[:prompt_image] # Skip if there are already errors
|
|
77
|
+
|
|
78
|
+
# Rule: cannot generate with only a last frame (veo3.1 specific)
|
|
79
|
+
if only_first_frame && prompt_image.length == 1
|
|
80
|
+
item = prompt_image.first
|
|
81
|
+
position = item[:position] || item["position"]
|
|
82
|
+
if position == "last"
|
|
83
|
+
errors[:prompt_image] = "cannot generate with only a last frame. Must include a first frame."
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Rule: with 2 items, ensure one is "first" and one is "last"
|
|
88
|
+
if prompt_image.length == 2 && valid_positions.sort == [ "first", "last" ]
|
|
89
|
+
positions = prompt_image.map { |item| item[:position] || item["position"] }.compact
|
|
90
|
+
unless positions.sort == [ "first", "last" ]
|
|
91
|
+
errors[:prompt_image] = "array with 2 items must have one 'first' and one 'last' position"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_validators"
|
|
4
|
+
|
|
5
|
+
module RunwayML
|
|
6
|
+
module Validators
|
|
7
|
+
class SoundEffectValidator
|
|
8
|
+
def initialize
|
|
9
|
+
@prompt_text_validator = PromptTextValidator.new(max_length: 3000, required: true)
|
|
10
|
+
@duration_validator = DurationValidator.new(range: 0.5..30)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def validate(prompt_text:, duration:, loop:)
|
|
14
|
+
errors = {}
|
|
15
|
+
|
|
16
|
+
prompt_text_validator.validate(prompt_text, errors)
|
|
17
|
+
duration_validator.validate(duration, errors) unless duration.nil?
|
|
18
|
+
|
|
19
|
+
{ errors: errors }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :prompt_text_validator, :duration_validator
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|