movoa 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/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/movoa/version.rb +3 -0
- data/lib/movoa.rb +205 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c27764ce172e504594e4d5e73fb1033b33dbfb71252e6b038f8acae9c9b3499
|
|
4
|
+
data.tar.gz: 3d5c574ac6b5fa38dff69f697d0026ff3c0c33db750875e186f909dacd589666
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 313b2434777fc2d16993748c8fdf8cae73a963a41d88dd3f819106eb15a61b2e1ffffe11f76d26d641615f2372be38ce4070994bedf09466fb04061c1e35af85
|
|
7
|
+
data.tar.gz: 87021147981dd72efee7bb9dd4f328284812eb6a6e6872514a693bb0c5ffab3c2f10d7ad17eccb9dfc235edc2fc544cb1b70178adf69c763d7c6eed1bbbb3faa
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Movoa (https://movoa.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# movoa
|
|
2
|
+
|
|
3
|
+
Unlimited AI video generator with sound: on paid plans the tenth take of a shot costs what the first did, and your first clip is free to make right now. This is the official Ruby SDK for [Movoa](https://movoa.ai/?utm_source=rubygems&utm_medium=package): one API key turns a prompt into [text to video](https://movoa.ai/video/text-to-video), or a still into [photo to video](https://movoa.ai/video/photo-to-video), and every clip comes back as [AI video with audio](https://movoa.ai/video/ai-video-with-audio).
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require "movoa"
|
|
7
|
+
|
|
8
|
+
clip = Movoa.generate_video("a paper lantern drifting over a night river, slow dolly")
|
|
9
|
+
puts clip # URL of the finished MP4, sound included
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`generate_video` submits the run, waits for it and returns the URL of the finished file.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install movoa
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Standard library only (`net/http`, `json`), Ruby 2.6+.
|
|
21
|
+
|
|
22
|
+
## API key
|
|
23
|
+
|
|
24
|
+
Sign in on [movoa.ai](https://movoa.ai/?utm_source=rubygems&utm_medium=package), create a key at [movoa.ai/settings/apikeys](https://movoa.ai/settings/apikeys), then pass it as `api_key:` or export it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export MOVOA_API_KEY=sk-...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Runs cost credits on your Movoa account, at the prices on [movoa.ai/pricing](https://movoa.ai/pricing).
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Movoa.credits # remaining credits on the account
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Text to video and photo to video
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# text to video, vertical, 10 seconds
|
|
40
|
+
Movoa.generate_video("a fox trotting through fresh snow at dawn", duration: 10, aspect_ratio: "9:16")
|
|
41
|
+
|
|
42
|
+
# photo to video: the photo becomes the first frame
|
|
43
|
+
Movoa.generate_video("the camera slowly orbits the statue", image_url: "https://example.com/statue.jpg")
|
|
44
|
+
|
|
45
|
+
# another model
|
|
46
|
+
Movoa.generate_video("a street drummer in the rain", model: "seedance-2-5", resolution: "720p")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| model | options |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `minimax-h3` (default) | `duration` 4 / 5 / 6 / 8 / 10 / 15 · `resolution` 480p / 768p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
|
|
52
|
+
| `veo-3-1-fast` | `duration` 8 · `resolution` 720p · `aspect_ratio` 16:9 / 9:16 (photo to video: 16:9) |
|
|
53
|
+
| `seedance-2-5` | `duration` 5 / 10 / 15 / 20 / 25 / 30 · `resolution` 480p / 720p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
|
|
54
|
+
| `seedance-2`, `seedance-2-fast`, `seedance-2-mini` | `duration` 4 / 6 / 8 / 10 · `resolution` 480p / 720p · `aspect_ratio` |
|
|
55
|
+
|
|
56
|
+
Any other keyword argument is sent in `options` as it is. Every run asks for sound. On the site: [Seedance 2.5](https://movoa.ai/video/seedance-2-5) and [Veo 3](https://movoa.ai/video/google-veo-3).
|
|
57
|
+
|
|
58
|
+
## Submit now, collect later
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
c = Movoa::Client.new # reads MOVOA_API_KEY
|
|
62
|
+
id = c.submit("a lighthouse in a storm", duration: 6)
|
|
63
|
+
c.status(id)["status"] # pending | processing | success | failed | canceled
|
|
64
|
+
url = c.wait(id) # polls every 10 seconds, 30 minutes by default
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
While a run waits in line, `status` returns `"queued" => true` and `"queueEta"` in seconds.
|
|
68
|
+
|
|
69
|
+
## Errors worth rescuing
|
|
70
|
+
|
|
71
|
+
All inherit from `Movoa::Error`:
|
|
72
|
+
|
|
73
|
+
- `Movoa::AuthError`: no key, or the key is wrong or deleted; create one at [movoa.ai/settings/apikeys](https://movoa.ai/settings/apikeys)
|
|
74
|
+
- `Movoa::QuotaError`: the account does not have enough credits for this run
|
|
75
|
+
- `Movoa::PlanError`: that model or quality needs a plan on the account
|
|
76
|
+
- `Movoa::BusyError`: too many runs on this account are generating at once
|
|
77
|
+
- `Movoa::RejectedError`: the content filter refused the prompt, or a cooldown is active; reword it
|
|
78
|
+
- `Movoa::GenerationError`: the run was accepted but failed or was canceled; `#error_message` says why
|
|
79
|
+
- `Movoa::TimeoutError`: the wait ran out; `#task_id` lets you check the run later
|
|
80
|
+
|
|
81
|
+
Any other refusal raises `Movoa::Error` with the server's message.
|
|
82
|
+
|
|
83
|
+
MIT licensed. Made by [Movoa](https://movoa.ai/?utm_source=rubygems&utm_medium=package).
|
data/lib/movoa.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Official SDK for Movoa (https://movoa.ai), the AI video generator with sound.
|
|
2
|
+
#
|
|
3
|
+
# require "movoa"
|
|
4
|
+
# clip = Movoa.generate_video("a paper lantern drifting over a night river")
|
|
5
|
+
# moved = Movoa.generate_video("the camera slowly orbits the statue",
|
|
6
|
+
# image_url: "https://example.com/statue.jpg")
|
|
7
|
+
#
|
|
8
|
+
# Set MOVOA_API_KEY (create a key at https://movoa.ai/settings/apikeys).
|
|
9
|
+
# Runs cost credits on your Movoa account, at the prices on https://movoa.ai/pricing.
|
|
10
|
+
require "json"
|
|
11
|
+
require "net/http"
|
|
12
|
+
require "uri"
|
|
13
|
+
require_relative "movoa/version"
|
|
14
|
+
|
|
15
|
+
module Movoa
|
|
16
|
+
BASE_URL = "https://movoa.ai".freeze
|
|
17
|
+
KEYS_URL = "https://movoa.ai/settings/apikeys".freeze
|
|
18
|
+
DEFAULT_MODEL = "minimax-h3".freeze
|
|
19
|
+
MODELS = %w[minimax-h3 veo-3-1-fast seedance-2-5 seedance-2 seedance-2-fast seedance-2-mini].freeze
|
|
20
|
+
|
|
21
|
+
# Base of every error this gem raises. #status is the HTTP status, #code the
|
|
22
|
+
# envelope code, #reason the data.reason the service sent, if any.
|
|
23
|
+
class Error < StandardError
|
|
24
|
+
attr_reader :status, :code, :reason, :task_id
|
|
25
|
+
def initialize(msg = nil, status: nil, code: nil, reason: nil, task_id: nil)
|
|
26
|
+
super(msg)
|
|
27
|
+
@status = status
|
|
28
|
+
@code = code
|
|
29
|
+
@reason = reason
|
|
30
|
+
@task_id = task_id
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
# No API key, or the key is invalid or deleted.
|
|
34
|
+
class AuthError < Error; end
|
|
35
|
+
# The account does not have enough credits for this run.
|
|
36
|
+
class QuotaError < Error; end
|
|
37
|
+
# The model or quality needs a plan, or the account has never bought credits.
|
|
38
|
+
class PlanError < Error; end
|
|
39
|
+
# Too many runs on this account are generating at once.
|
|
40
|
+
class BusyError < Error; end
|
|
41
|
+
# The content filter refused the prompt (code -2) or a cooldown is active (code -3); reword it.
|
|
42
|
+
class RejectedError < Error; end
|
|
43
|
+
# The run was accepted but ended as failed or canceled. #error_message says why.
|
|
44
|
+
class GenerationError < Error
|
|
45
|
+
attr_reader :error_message, :failure_kind
|
|
46
|
+
def initialize(msg, task_id:, error_message: nil, failure_kind: nil)
|
|
47
|
+
super(msg, task_id: task_id)
|
|
48
|
+
@error_message = error_message
|
|
49
|
+
@failure_kind = failure_kind
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
# The wait ran out; the run may still finish. #task_id lets you check it later.
|
|
53
|
+
class TimeoutError < Error; end
|
|
54
|
+
|
|
55
|
+
# Map a refusal to the error that says what to do next. movoa.ai answers
|
|
56
|
+
# almost every refusal as HTTP 200 with a non-zero code and a data.reason.
|
|
57
|
+
def self.classify(status, body)
|
|
58
|
+
body = {} unless body.is_a?(Hash)
|
|
59
|
+
msg = body["message"].to_s
|
|
60
|
+
msg = "request failed (HTTP #{status})" if msg.empty?
|
|
61
|
+
code = body["code"].is_a?(Integer) ? body["code"] : nil
|
|
62
|
+
reason = body["data"].is_a?(Hash) && body["data"]["reason"].is_a?(String) ? body["data"]["reason"] : nil
|
|
63
|
+
klass =
|
|
64
|
+
if status == 401 then AuthError
|
|
65
|
+
elsif [-2, -3].include?(code) then RejectedError
|
|
66
|
+
elsif reason == "insufficient_credits" then QuotaError
|
|
67
|
+
elsif reason == "plan_required" then PlanError
|
|
68
|
+
elsif reason == "concurrency" then BusyError
|
|
69
|
+
elsif msg.downcase.include?("no auth") then AuthError
|
|
70
|
+
else Error
|
|
71
|
+
end
|
|
72
|
+
msg = "#{msg} (create a key at #{KEYS_URL})" if klass == AuthError && !msg.include?(KEYS_URL)
|
|
73
|
+
klass.new(msg, status: status, code: code, reason: reason)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A client bound to one API key. transport: is a callable (url, body, key) -> [status, raw_body_string], for tests.
|
|
77
|
+
class Client
|
|
78
|
+
attr_accessor :poll_every, :timeout
|
|
79
|
+
|
|
80
|
+
def initialize(api_key = nil, base_url: nil, poll_every: 10, timeout: 1800, transport: nil)
|
|
81
|
+
@key = (api_key || ENV["MOVOA_API_KEY"]).to_s.strip
|
|
82
|
+
base = base_url || ENV["MOVOA_BASE_URL"]
|
|
83
|
+
@base = (base.nil? || base.strip.empty? ? BASE_URL : base.strip).chomp("/")
|
|
84
|
+
@poll_every = poll_every
|
|
85
|
+
@timeout = timeout
|
|
86
|
+
@transport = transport || method(:http)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Start a run and return its task id without waiting. image_url: makes it photo to video.
|
|
90
|
+
# Other keyword arguments go into "options" as they are (duration:, resolution:, aspect_ratio:).
|
|
91
|
+
def submit(prompt, model: DEFAULT_MODEL, image_url: nil, **options)
|
|
92
|
+
raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
|
|
93
|
+
|
|
94
|
+
opts = { "sound" => true }
|
|
95
|
+
options.each { |k, v| opts[k.to_s] = v unless v.nil? }
|
|
96
|
+
opts["image_input"] = [image_url] if image_url
|
|
97
|
+
body = {
|
|
98
|
+
# The server routes by model; this only has to be non-empty.
|
|
99
|
+
"provider" => "auto",
|
|
100
|
+
"mediaType" => "video",
|
|
101
|
+
"model" => model,
|
|
102
|
+
"scene" => image_url ? "image-to-video" : "text-to-video",
|
|
103
|
+
"prompt" => prompt,
|
|
104
|
+
"options" => opts
|
|
105
|
+
}
|
|
106
|
+
data = call("/api/ai/generate", body)["data"]
|
|
107
|
+
id = data.is_a?(Hash) ? data["id"] : nil
|
|
108
|
+
raise Error, "movoa.ai accepted the run but returned no id" if id.nil? || id.to_s.empty?
|
|
109
|
+
|
|
110
|
+
id.to_s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Look up a run; returns the API's data hash ("id", "status", "videos", "errorMessage", "queued", "queueEta", ...).
|
|
114
|
+
def status(task_id)
|
|
115
|
+
data = call("/api/ai/query", { "taskId" => task_id })["data"]
|
|
116
|
+
data.is_a?(Hash) ? data : {}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Credits left on the account.
|
|
120
|
+
def credits
|
|
121
|
+
data = call("/api/user/get-user-credits", nil)["data"]
|
|
122
|
+
data.is_a?(Hash) ? data["remainingCredits"].to_i : 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Submit, wait, and return the URL of the finished clip.
|
|
126
|
+
def generate_video(prompt, model: DEFAULT_MODEL, image_url: nil, **options)
|
|
127
|
+
wait(submit(prompt, model: model, image_url: image_url, **options))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Wait for a submitted run and return its video URL.
|
|
131
|
+
def wait(task_id)
|
|
132
|
+
deadline = Time.now + @timeout
|
|
133
|
+
loop do
|
|
134
|
+
d = status(task_id)
|
|
135
|
+
case d["status"]
|
|
136
|
+
when "success"
|
|
137
|
+
videos = d["videos"]
|
|
138
|
+
raise GenerationError.new("run #{task_id} finished with no video", task_id: task_id) unless videos.is_a?(Array) && !videos.empty?
|
|
139
|
+
|
|
140
|
+
return videos.first.to_s
|
|
141
|
+
when "failed", "canceled"
|
|
142
|
+
em = d["errorMessage"]
|
|
143
|
+
raise GenerationError.new("run #{task_id} #{d['status']}#{em ? ": #{em}" : ''}",
|
|
144
|
+
task_id: task_id, error_message: em, failure_kind: d["failureKind"])
|
|
145
|
+
end
|
|
146
|
+
if Time.now + @poll_every > deadline
|
|
147
|
+
raise TimeoutError.new("run #{task_id} is still generating; check it later with status", task_id: task_id)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sleep @poll_every
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def call(path, body)
|
|
157
|
+
raise AuthError, "set MOVOA_API_KEY or pass api_key; create a key at #{KEYS_URL}" if @key.empty?
|
|
158
|
+
|
|
159
|
+
status, raw = @transport.call("#{@base}#{path}", body, @key)
|
|
160
|
+
parsed = begin
|
|
161
|
+
raw.to_s.strip.empty? ? {} : JSON.parse(raw.to_s)
|
|
162
|
+
rescue JSON::ParserError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
raise Movoa.classify(401, {}) if status == 401 && !parsed.is_a?(Hash)
|
|
166
|
+
raise Error.new("movoa.ai returned a non-JSON response (HTTP #{status})", status: status) unless parsed.is_a?(Hash)
|
|
167
|
+
return parsed if status < 400 && parsed["code"] == 0
|
|
168
|
+
|
|
169
|
+
raise Movoa.classify(status, parsed)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def http(url, body, key)
|
|
173
|
+
uri = URI(url)
|
|
174
|
+
req = Net::HTTP::Post.new(uri)
|
|
175
|
+
req["Content-Type"] = "application/json"
|
|
176
|
+
req["Authorization"] = "Bearer #{key}"
|
|
177
|
+
req["User-Agent"] = "movoa-ruby/#{VERSION}"
|
|
178
|
+
req.body = JSON.generate(body) unless body.nil?
|
|
179
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
|
|
180
|
+
[res.code.to_i, res.body.to_s]
|
|
181
|
+
rescue SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
|
|
182
|
+
raise Error, "could not reach movoa.ai: #{e.message}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Generate one clip with sound (minimax-h3 by default) and return its URL.
|
|
187
|
+
def self.generate_video(prompt, model: DEFAULT_MODEL, image_url: nil, api_key: nil, **options)
|
|
188
|
+
Client.new(api_key).generate_video(prompt, model: model, image_url: image_url, **options)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Start a run and return its task id.
|
|
192
|
+
def self.submit(prompt, model: DEFAULT_MODEL, image_url: nil, api_key: nil, **options)
|
|
193
|
+
Client.new(api_key).submit(prompt, model: model, image_url: image_url, **options)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Look up a run.
|
|
197
|
+
def self.status(task_id, api_key: nil)
|
|
198
|
+
Client.new(api_key).status(task_id)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Credits left on the account.
|
|
202
|
+
def self.credits(api_key: nil)
|
|
203
|
+
Client.new(api_key).credits
|
|
204
|
+
end
|
|
205
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: movoa
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Movoa (https://movoa.ai)
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Unlimited AI video generator with sound: on paid plans the tenth take
|
|
14
|
+
of a shot costs what the first did, and your first clip is free to make right now.
|
|
15
|
+
A zero-dependency Ruby client for Movoa (movoa.ai): text to video and photo to video
|
|
16
|
+
with sound, on MiniMax H3, Seedance 2.5, Seedance 2.0 and Veo 3.1 Fast, with one
|
|
17
|
+
API key.'
|
|
18
|
+
email:
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/movoa.rb
|
|
26
|
+
- lib/movoa/version.rb
|
|
27
|
+
homepage: https://movoa.ai/?utm_source=rubygems&utm_medium=package
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://movoa.ai/?utm_source=rubygems&utm_medium=package
|
|
32
|
+
source_code_uri: https://github.com/hanshs474/movoa-rb
|
|
33
|
+
documentation_uri: https://movoa.ai/video/text-to-video
|
|
34
|
+
bug_tracker_uri: https://github.com/hanshs474/movoa-rb/issues
|
|
35
|
+
rubygems_mfa_required: 'false'
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '2.6'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.0.3.1
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: Official SDK for Movoa (movoa.ai), the AI video generator with sound.
|
|
55
|
+
test_files: []
|