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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "validators/sound_effect_validator"
|
|
5
|
+
|
|
6
|
+
module RunwayML
|
|
7
|
+
class SoundEffect
|
|
8
|
+
VALID_MODELS = [ "eleven_text_to_sound_v2" ].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(client:)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(model:, prompt_text:, duration: nil, loop: false)
|
|
15
|
+
errors = {}
|
|
16
|
+
|
|
17
|
+
unless VALID_MODELS.include?(model)
|
|
18
|
+
errors[:model] = "must be one of: #{VALID_MODELS.join(', ')}"
|
|
19
|
+
raise ValidationError, errors
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
validator = Validators::SoundEffectValidator.new
|
|
23
|
+
result = validator.validate(
|
|
24
|
+
prompt_text: prompt_text,
|
|
25
|
+
duration: duration,
|
|
26
|
+
loop: loop
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
errors = result[:errors]
|
|
30
|
+
raise ValidationError, errors unless errors.empty?
|
|
31
|
+
|
|
32
|
+
inputs = build_inputs(prompt_text, duration, loop)
|
|
33
|
+
|
|
34
|
+
response = client.post(
|
|
35
|
+
"sound_effect",
|
|
36
|
+
inputs.merge(model: model)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
Task.new(id: response["id"], client: client)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :client
|
|
45
|
+
|
|
46
|
+
def build_inputs(prompt_text, duration, loop_flag)
|
|
47
|
+
inputs = {
|
|
48
|
+
promptText: prompt_text,
|
|
49
|
+
loop: loop_flag
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
inputs[:duration] = duration unless duration.nil?
|
|
53
|
+
|
|
54
|
+
inputs
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunwayML
|
|
4
|
+
# Analyzes coverage of SDK methods against OpenAPI spec endpoints
|
|
5
|
+
class SpecCoverageAnalyzer
|
|
6
|
+
CRITICAL_ENDPOINTS = {
|
|
7
|
+
"POST /v1/text_to_video" => { class: "TextToVideo", method: "create" },
|
|
8
|
+
"POST /v1/image_to_video" => { class: "ImageToVideo", method: "create" },
|
|
9
|
+
"POST /v1/text_to_speech" => { class: "TextToSpeech", method: "create" },
|
|
10
|
+
"POST /v1/speech_to_speech" => { class: "SpeechToSpeech", method: "create" },
|
|
11
|
+
"POST /v1/sound_effect" => { class: "SoundEffect", method: "create" },
|
|
12
|
+
"POST /v1/voice_isolation" => { class: "VoiceIsolation", method: "create" },
|
|
13
|
+
"POST /v1/voice_dubbing" => { class: "VoiceDubbing", method: "create" },
|
|
14
|
+
"POST /v1/character_performance" => { class: "CharacterPerformance", method: "create" },
|
|
15
|
+
"POST /v1/video_to_video" => { class: "VideoToVideo", method: "create" },
|
|
16
|
+
"POST /v1/text_to_image" => { class: "TextToImage", method: "create" },
|
|
17
|
+
"GET /v1/tasks/{id}" => { class: "Task", method: "retrieve" },
|
|
18
|
+
"DELETE /v1/tasks/{id}" => { class: "Task", method: "delete" },
|
|
19
|
+
"GET /v1/organization" => { class: "Organization", method: "retrieve" },
|
|
20
|
+
"POST /v1/uploads" => { class: "Uploads", method: "create" }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def analyze
|
|
25
|
+
spec = OpenAPISpecLoader.load_spec
|
|
26
|
+
return nil unless spec
|
|
27
|
+
|
|
28
|
+
coverage = {}
|
|
29
|
+
|
|
30
|
+
CRITICAL_ENDPOINTS.each do |endpoint, info|
|
|
31
|
+
method, path = endpoint.split(" ")
|
|
32
|
+
schema = OpenAPISpecLoader.get_endpoint_schema(method, path)
|
|
33
|
+
|
|
34
|
+
coverage[endpoint] = {
|
|
35
|
+
sdk_class: info[:class],
|
|
36
|
+
sdk_method: info[:method],
|
|
37
|
+
spec_exists: !schema.nil?,
|
|
38
|
+
has_request_schema: has_request_schema_for_endpoint?(method, path),
|
|
39
|
+
has_response_schema: has_response_schema_for_endpoint?(method, path),
|
|
40
|
+
models_supported: extract_supported_models(schema, path),
|
|
41
|
+
path_object: schema
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
coverage
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print_coverage_report
|
|
49
|
+
coverage = analyze
|
|
50
|
+
return puts "Could not load OpenAPI spec" unless coverage
|
|
51
|
+
|
|
52
|
+
puts "\n" + "=" * 100
|
|
53
|
+
puts "SDK SPEC COVERAGE REPORT".center(100)
|
|
54
|
+
puts "=" * 100 + "\n"
|
|
55
|
+
|
|
56
|
+
total_endpoints = coverage.size
|
|
57
|
+
fully_compliant = coverage.count { |_, v| v[:spec_exists] && v[:has_request_schema] && v[:has_response_schema] }
|
|
58
|
+
|
|
59
|
+
puts "Summary:"
|
|
60
|
+
puts " Total Critical Endpoints: #{total_endpoints}"
|
|
61
|
+
puts " Fully Defined in Spec: #{fully_compliant}/#{total_endpoints}"
|
|
62
|
+
puts " Compliance Rate: #{(fully_compliant.to_f / total_endpoints * 100).round(1)}%\n\n"
|
|
63
|
+
|
|
64
|
+
puts "Endpoint Details:"
|
|
65
|
+
puts "-" * 100
|
|
66
|
+
|
|
67
|
+
coverage.each do |endpoint, data|
|
|
68
|
+
status = data[:spec_exists] ? "✓" : "✗"
|
|
69
|
+
models = data[:models_supported].any? ? " [Models: #{data[:models_supported].join(', ')}]" : ""
|
|
70
|
+
|
|
71
|
+
puts "#{status} #{endpoint.ljust(35)} -> #{data[:sdk_class]}.#{data[:sdk_method]}#{models}"
|
|
72
|
+
|
|
73
|
+
unless data[:spec_exists]
|
|
74
|
+
puts " ⚠️ Endpoint not found in OpenAPI spec"
|
|
75
|
+
else
|
|
76
|
+
unless data[:has_request_schema]
|
|
77
|
+
puts " ⚠️ Missing request schema (expected for GET/DELETE)"
|
|
78
|
+
end
|
|
79
|
+
unless data[:has_response_schema]
|
|
80
|
+
puts " ⚠️ Missing response schema"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
puts "\n" + "=" * 100 + "\n"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def has_request_schema_for_endpoint?(method, path)
|
|
91
|
+
# GET and DELETE methods don't have request bodies, so skip schema check
|
|
92
|
+
return true if method.upcase == "GET" || method.upcase == "DELETE"
|
|
93
|
+
|
|
94
|
+
OpenAPISpecLoader.get_request_schema(method, path) != nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def has_request_schema_for_endpoint?(method, path)
|
|
100
|
+
# GET and DELETE methods don't have request bodies, so skip schema check
|
|
101
|
+
return true if method.upcase == "GET" || method.upcase == "DELETE"
|
|
102
|
+
|
|
103
|
+
OpenAPISpecLoader.get_request_schema(method, path) != nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def has_response_schema_for_endpoint?(method, path)
|
|
107
|
+
# 204 No Content responses are valid without a schema
|
|
108
|
+
operation = OpenAPISpecLoader.get_endpoint_schema(method, path)
|
|
109
|
+
return true unless operation
|
|
110
|
+
|
|
111
|
+
responses = operation["responses"] || {}
|
|
112
|
+
|
|
113
|
+
# Check if 204 No Content is defined (DELETE endpoints)
|
|
114
|
+
return true if responses.key?("204")
|
|
115
|
+
|
|
116
|
+
# Otherwise check for response schema
|
|
117
|
+
OpenAPISpecLoader.get_response_schema(method, path) != nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_supported_models(schema, path)
|
|
121
|
+
return [] unless schema
|
|
122
|
+
|
|
123
|
+
# For endpoints with oneOf discriminated by model
|
|
124
|
+
request_schema = schema.dig("requestBody", "content", "application/json", "schema")
|
|
125
|
+
return [] unless request_schema&.dig("oneOf")
|
|
126
|
+
|
|
127
|
+
request_schema["oneOf"].map do |variant|
|
|
128
|
+
variant.dig("properties", "model", "const") ||
|
|
129
|
+
variant.dig("title")
|
|
130
|
+
end.compact
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "media_processor"
|
|
5
|
+
require_relative "validators/speech_to_speech_validator"
|
|
6
|
+
|
|
7
|
+
module RunwayML
|
|
8
|
+
class SpeechToSpeech
|
|
9
|
+
VALID_MODELS = [ "eleven_multilingual_sts_v2" ].freeze
|
|
10
|
+
VALID_MEDIA_TYPES = [ "audio", "video" ].freeze
|
|
11
|
+
VALID_VOICE_TYPES = [ "runway-preset" ].freeze
|
|
12
|
+
VALID_PRESET_IDS = [
|
|
13
|
+
"Maya", "Arjun", "Serene", "Bernard", "Billy", "Mark", "Clint", "Mabel", "Chad", "Leslie",
|
|
14
|
+
"Eleanor", "Elias", "Elliot", "Grungle", "Brodie", "Sandra", "Kirk", "Kylie", "Lara", "Lisa",
|
|
15
|
+
"Malachi", "Marlene", "Martin", "Miriam", "Monster", "Paula", "Pip", "Rusty", "Ragnar", "Xylar",
|
|
16
|
+
"Maggie", "Jack", "Katie", "Noah", "James", "Rina", "Ella", "Mariah", "Frank", "Claudia",
|
|
17
|
+
"Niki", "Vincent", "Kendrick", "Myrna", "Tom", "Wanda", "Benjamin", "Kiana", "Rachel"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(client:)
|
|
21
|
+
@client = client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create(model:, media:, voice:, remove_background_noise: false, auto_upload: true)
|
|
25
|
+
errors = {}
|
|
26
|
+
|
|
27
|
+
unless VALID_MODELS.include?(model)
|
|
28
|
+
errors[:model] = "must be one of: #{VALID_MODELS.join(', ')}"
|
|
29
|
+
raise ValidationError, errors
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Process media with auto-upload support
|
|
33
|
+
processed_media = process_media_with_upload(media, errors, auto_upload)
|
|
34
|
+
|
|
35
|
+
validator = Validators::SpeechToSpeechValidator.new
|
|
36
|
+
result = validator.validate(
|
|
37
|
+
media: processed_media,
|
|
38
|
+
voice: voice,
|
|
39
|
+
remove_background_noise: remove_background_noise
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
errors = result[:errors]
|
|
43
|
+
raise ValidationError, errors unless errors.empty?
|
|
44
|
+
|
|
45
|
+
inputs = build_inputs(processed_media, voice, remove_background_noise)
|
|
46
|
+
|
|
47
|
+
response = client.post(
|
|
48
|
+
"speech_to_speech",
|
|
49
|
+
inputs.merge(model: model)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
Task.new(id: response["id"], client: client)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
attr_reader :client
|
|
58
|
+
|
|
59
|
+
def process_media_with_upload(media, errors, auto_upload)
|
|
60
|
+
return media unless media.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
media = media.dup
|
|
63
|
+
uri = media[:uri] || media["uri"]
|
|
64
|
+
|
|
65
|
+
# Process the URI with auto-upload
|
|
66
|
+
processed_uri = MediaProcessor.process(
|
|
67
|
+
uri,
|
|
68
|
+
errors,
|
|
69
|
+
client: client,
|
|
70
|
+
auto_upload: auto_upload
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if media.key?(:uri)
|
|
74
|
+
media[:uri] = processed_uri
|
|
75
|
+
else
|
|
76
|
+
media["uri"] = processed_uri
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
media
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_inputs(media, voice, remove_background_noise)
|
|
83
|
+
{
|
|
84
|
+
media: media,
|
|
85
|
+
voice: voice,
|
|
86
|
+
removeBackgroundNoise: remove_background_noise
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "validators/base_validators"
|
|
4
|
+
|
|
5
|
+
module RunwayML
|
|
6
|
+
class Task
|
|
7
|
+
POLL_TIME = 6
|
|
8
|
+
POLL_JITTER = 3
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :status, :created_at, :progress, :failure, :failure_code, :output, :data
|
|
11
|
+
|
|
12
|
+
def initialize(id:, client: nil, data: nil)
|
|
13
|
+
# Only validate UUID for real clients (not TestClient used in specs)
|
|
14
|
+
validate_task_id(id) if client && !client.is_a?(TestClient)
|
|
15
|
+
@id = id
|
|
16
|
+
@client = client
|
|
17
|
+
apply_data(data) if data
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def retrieve
|
|
21
|
+
ensure_client!
|
|
22
|
+
|
|
23
|
+
response = client.get("tasks/#{id}")
|
|
24
|
+
apply_data(response)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete
|
|
29
|
+
ensure_client!
|
|
30
|
+
|
|
31
|
+
client.delete("tasks/#{id}")
|
|
32
|
+
true
|
|
33
|
+
rescue NotFoundError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def wait_for_output(timeout: 60 * 10)
|
|
38
|
+
ensure_client!
|
|
39
|
+
|
|
40
|
+
start_time = Time.now.to_f
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
retrieve
|
|
44
|
+
|
|
45
|
+
case status
|
|
46
|
+
when "SUCCEEDED"
|
|
47
|
+
return self
|
|
48
|
+
when "FAILED", "CANCELLED"
|
|
49
|
+
raise TaskFailedError.new(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if !timeout.nil? && (Time.now.to_f - start_time) > timeout
|
|
53
|
+
raise TaskTimeoutError.new(self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sleep(POLL_TIME + (rand * POLL_JITTER) - (POLL_JITTER / 2.0))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ==(other)
|
|
61
|
+
other.is_a?(Task) && other.id == id
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
hash = { id: id }
|
|
66
|
+
hash[:status] = status if status
|
|
67
|
+
hash[:created_at] = created_at if created_at
|
|
68
|
+
hash[:progress] = progress if progress
|
|
69
|
+
hash[:failure] = failure if failure
|
|
70
|
+
hash[:failure_code] = failure_code if failure_code
|
|
71
|
+
hash[:output] = output if output
|
|
72
|
+
hash
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_json
|
|
76
|
+
to_h.to_json
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_s
|
|
80
|
+
"#<RunwayML::Task id=#{id}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def inspect
|
|
84
|
+
to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
attr_reader :client
|
|
90
|
+
|
|
91
|
+
def ensure_client!
|
|
92
|
+
return if client
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "Task client is required to retrieve or delete tasks"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_task_id(task_id)
|
|
98
|
+
validator = Validators::UUIDValidator.new
|
|
99
|
+
errors = {}
|
|
100
|
+
validator.validate(task_id, errors, field: :id)
|
|
101
|
+
raise ValidationError, errors if errors.any?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def apply_data(payload)
|
|
105
|
+
return unless payload.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
@data = payload
|
|
108
|
+
@status = payload["status"] || payload[:status]
|
|
109
|
+
@created_at = payload["createdAt"] || payload[:created_at]
|
|
110
|
+
@progress = payload["progress"] || payload[:progress]
|
|
111
|
+
@failure = payload["failure"] || payload[:failure]
|
|
112
|
+
@failure_code = payload["failureCode"] || payload[:failure_code]
|
|
113
|
+
@output = payload["output"] || payload[:output]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module RunwayML
|
|
2
|
+
class TestClient
|
|
3
|
+
def initialize(api_secret: nil, base_url: nil, api_version: nil)
|
|
4
|
+
reset!
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def post(path, params)
|
|
8
|
+
record_request(:post, path, params)
|
|
9
|
+
fetch_response(:post, path, params)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get(path)
|
|
13
|
+
record_request(:get, path, nil)
|
|
14
|
+
fetch_response(:get, path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def delete(path)
|
|
18
|
+
record_request(:delete, path, nil)
|
|
19
|
+
fetch_response(:delete, path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Inject a response for a given method/path/params
|
|
23
|
+
def inject_response(method, path, params: nil, response:)
|
|
24
|
+
key = response_key(method, path, params)
|
|
25
|
+
@responses[key] = response
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def requests
|
|
29
|
+
@requests.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset!
|
|
33
|
+
@responses = {}
|
|
34
|
+
@requests = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def fetch_response(method, path, params = nil)
|
|
40
|
+
key = response_key(method, path, params)
|
|
41
|
+
unless @responses.key?(key)
|
|
42
|
+
raise "No injected response for #{method.upcase} #{path} with params: #{params.inspect}"
|
|
43
|
+
end
|
|
44
|
+
response = @responses[key]
|
|
45
|
+
if response.is_a?(Array)
|
|
46
|
+
raise "No injected response remaining for #{method.upcase} #{path} with params: #{params.inspect}" if response.empty?
|
|
47
|
+
response = response.shift
|
|
48
|
+
end
|
|
49
|
+
raise response if response.is_a?(Exception)
|
|
50
|
+
|
|
51
|
+
response
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_request(method, path, params)
|
|
55
|
+
@requests << { method: method, path: path, params: params }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def response_key(method, path, params)
|
|
59
|
+
[ method, path, params ]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "validators/text_to_speech_validator"
|
|
5
|
+
|
|
6
|
+
module RunwayML
|
|
7
|
+
class TextToSpeech
|
|
8
|
+
VALID_MODELS = [ "eleven_multilingual_v2" ].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(client:)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(model:, prompt_text:, voice:)
|
|
15
|
+
errors = {}
|
|
16
|
+
|
|
17
|
+
unless VALID_MODELS.include?(model)
|
|
18
|
+
errors[:model] = "must be one of: #{VALID_MODELS.join(', ')}"
|
|
19
|
+
raise ValidationError, errors
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
validator = Validators::TextToSpeechValidator.new
|
|
23
|
+
result = validator.validate(
|
|
24
|
+
prompt_text: prompt_text,
|
|
25
|
+
voice: voice
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
errors = result[:errors]
|
|
29
|
+
raise ValidationError, errors unless errors.empty?
|
|
30
|
+
|
|
31
|
+
inputs = build_inputs(prompt_text, voice)
|
|
32
|
+
|
|
33
|
+
response = client.post(
|
|
34
|
+
"text_to_speech",
|
|
35
|
+
inputs.merge(model: model)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
Task.new(id: response["id"], client: client)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :client
|
|
44
|
+
|
|
45
|
+
def build_inputs(prompt_text, voice)
|
|
46
|
+
{
|
|
47
|
+
promptText: prompt_text,
|
|
48
|
+
voice: voice
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "media_processor"
|
|
5
|
+
require_relative "validators/text_veo3_validator"
|
|
6
|
+
require_relative "validators/text_veo3_stable_validator"
|
|
7
|
+
|
|
8
|
+
module RunwayML
|
|
9
|
+
class TextToVideo
|
|
10
|
+
VALID_MODELS = [ "veo3.1", "veo3.1_fast", "veo3" ].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(client:)
|
|
13
|
+
@client = client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create(model:, prompt_text:, ratio:, duration:, audio: nil, auto_upload: true)
|
|
17
|
+
errors = {}
|
|
18
|
+
|
|
19
|
+
unless VALID_MODELS.include?(model)
|
|
20
|
+
errors[:model] = "must be one of: #{VALID_MODELS.join(', ')}"
|
|
21
|
+
raise ValidationError, errors
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Process audio with auto-upload support if provided
|
|
25
|
+
processed_audio = if audio && audio != false
|
|
26
|
+
MediaProcessor.process(
|
|
27
|
+
audio,
|
|
28
|
+
errors,
|
|
29
|
+
client: client,
|
|
30
|
+
auto_upload: auto_upload
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
validator = get_validator(model)
|
|
35
|
+
result = validator.validate(**build_validator_params(model, prompt_text, ratio, duration, processed_audio || audio))
|
|
36
|
+
|
|
37
|
+
errors = result[:errors]
|
|
38
|
+
raise ValidationError, errors unless errors.empty?
|
|
39
|
+
|
|
40
|
+
inputs = build_inputs(model, prompt_text, ratio, duration, processed_audio || audio)
|
|
41
|
+
|
|
42
|
+
response = client.post(
|
|
43
|
+
"text_to_video",
|
|
44
|
+
inputs.merge(model: model)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
Task.new(id: response["id"], client: client)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :client
|
|
53
|
+
|
|
54
|
+
def get_validator(model)
|
|
55
|
+
case model
|
|
56
|
+
when "veo3.1", "veo3.1_fast"
|
|
57
|
+
Validators::TextVeo3Validator.new
|
|
58
|
+
when "veo3"
|
|
59
|
+
Validators::TextVeo3StableValidator.new
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "No validator configured for model: #{model}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_validator_params(model, prompt_text, ratio, duration, audio)
|
|
66
|
+
case model
|
|
67
|
+
when "veo3.1", "veo3.1_fast"
|
|
68
|
+
{
|
|
69
|
+
prompt_text: prompt_text,
|
|
70
|
+
ratio: ratio,
|
|
71
|
+
duration: duration,
|
|
72
|
+
audio: audio
|
|
73
|
+
}
|
|
74
|
+
when "veo3"
|
|
75
|
+
{
|
|
76
|
+
prompt_text: prompt_text,
|
|
77
|
+
ratio: ratio,
|
|
78
|
+
duration: duration,
|
|
79
|
+
audio: audio
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
{}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_inputs(model, prompt_text, ratio, duration, audio)
|
|
87
|
+
inputs = {
|
|
88
|
+
promptText: prompt_text,
|
|
89
|
+
ratio: ratio,
|
|
90
|
+
duration: duration
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case model
|
|
94
|
+
when "veo3.1", "veo3.1_fast"
|
|
95
|
+
inputs[:audio] = audio unless audio.nil?
|
|
96
|
+
when "veo3"
|
|
97
|
+
# audio not supported for veo3
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
inputs
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "validators/uploads_validator"
|
|
5
|
+
|
|
6
|
+
module RunwayML
|
|
7
|
+
class Uploads
|
|
8
|
+
VALID_TYPES = [ "ephemeral" ].freeze
|
|
9
|
+
SUPPORTED_EXTENSIONS = {
|
|
10
|
+
# Image extensions
|
|
11
|
+
"jpg" => "image/jpeg",
|
|
12
|
+
"jpeg" => "image/jpeg",
|
|
13
|
+
"png" => "image/png",
|
|
14
|
+
"webp" => "image/webp",
|
|
15
|
+
# Video extensions
|
|
16
|
+
"mp4" => "video/mp4",
|
|
17
|
+
"mov" => "video/quicktime",
|
|
18
|
+
"mkv" => "video/x-matroska",
|
|
19
|
+
"webm" => "video/webm",
|
|
20
|
+
"3gp" => "video/3gpp",
|
|
21
|
+
"ogv" => "video/ogg",
|
|
22
|
+
"avi" => "video/x-msvideo",
|
|
23
|
+
"flv" => "video/x-flv",
|
|
24
|
+
"mpg" => "video/mpeg",
|
|
25
|
+
"mpeg" => "video/mpeg",
|
|
26
|
+
# Audio extensions
|
|
27
|
+
"mp3" => "audio/mpeg",
|
|
28
|
+
"wav" => "audio/wav",
|
|
29
|
+
"flac" => "audio/flac",
|
|
30
|
+
"m4a" => "audio/mp4",
|
|
31
|
+
"aac" => "audio/aac",
|
|
32
|
+
"ogg" => "audio/ogg",
|
|
33
|
+
"weba" => "audio/webp"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def initialize(client:)
|
|
37
|
+
@client = client
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_ephemeral(file_or_path, filename: nil)
|
|
41
|
+
if file_or_path.is_a?(String)
|
|
42
|
+
# It's a file path
|
|
43
|
+
unless File.exist?(file_or_path)
|
|
44
|
+
raise ValidationError, { file_path: "file does not exist" }
|
|
45
|
+
end
|
|
46
|
+
file_path = file_or_path
|
|
47
|
+
filename ||= File.basename(file_path)
|
|
48
|
+
elsif file_or_path.respond_to?(:read)
|
|
49
|
+
# It's a File object or StringIO
|
|
50
|
+
filename ||= file_or_path.respond_to?(:path) ? File.basename(file_or_path.path) : "upload"
|
|
51
|
+
else
|
|
52
|
+
raise ValidationError, { file: "must be a file path string or File-like object" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
errors = {}
|
|
56
|
+
|
|
57
|
+
# Validate filename
|
|
58
|
+
validator = Validators::UploadsValidator.new
|
|
59
|
+
result = validator.validate(filename: filename)
|
|
60
|
+
errors.merge!(result[:errors])
|
|
61
|
+
|
|
62
|
+
raise ValidationError, errors unless errors.empty?
|
|
63
|
+
|
|
64
|
+
# Request upload URL from API
|
|
65
|
+
response = client.post("uploads", { filename: filename, type: "ephemeral" })
|
|
66
|
+
|
|
67
|
+
response["runwayUri"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
attr_reader :client
|
|
73
|
+
end
|
|
74
|
+
end
|