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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/lib/movoa/version.rb +3 -0
  5. data/lib/movoa.rb +205 -0
  6. 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).
@@ -0,0 +1,3 @@
1
+ module Movoa
2
+ VERSION = "0.1.0"
3
+ end
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: []