movoa 0.1.0__tar.gz

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.
movoa-0.1.0/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.
movoa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: movoa
3
+ Version: 0.1.0
4
+ Summary: 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. Official Python SDK for Movoa (movoa.ai): text to video and photo to video with sound from your own code.
5
+ Author: Movoa (https://movoa.ai)
6
+ License: MIT
7
+ Project-URL: Homepage, https://movoa.ai/?utm_source=pypi&utm_medium=package
8
+ Project-URL: Text to video, https://movoa.ai/video/text-to-video
9
+ Project-URL: API keys, https://movoa.ai/settings/apikeys
10
+ Project-URL: Pricing, https://movoa.ai/pricing
11
+ Project-URL: Source, https://github.com/hanshs474/movoa-py
12
+ Keywords: ai,video-generation,ai-video-generator,text-to-video,image-to-video,photo-to-video,ai-video-with-audio,minimax-h3,seedance-2-5,veo-3,movoa
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Multimedia :: Video
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # movoa
24
+
25
+ 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 Python SDK for [Movoa](https://movoa.ai/?utm_source=pypi&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).
26
+
27
+ ```bash
28
+ pip install movoa
29
+ ```
30
+
31
+ ```python
32
+ from movoa import generate_video
33
+
34
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle, slow push-in")
35
+ print(clip) # URL of the finished MP4, sound included
36
+ ```
37
+
38
+ Standard library only, no dependencies. `generate_video` submits the run, waits for it and returns the URL of the finished file.
39
+
40
+ ## API key
41
+
42
+ Sign in on [movoa.ai](https://movoa.ai/?utm_source=pypi&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:
43
+
44
+ ```bash
45
+ export MOVOA_API_KEY=sk-...
46
+ ```
47
+
48
+ Runs cost credits on your Movoa account, at the prices on [movoa.ai/pricing](https://movoa.ai/pricing).
49
+
50
+ ```python
51
+ from movoa import credits
52
+ print(credits()) # remaining credits on the account
53
+ ```
54
+
55
+ ## Text to video and photo to video
56
+
57
+ ```python
58
+ from movoa import generate_video
59
+
60
+ # text to video, vertical, 10 seconds
61
+ generate_video("a fox trotting through fresh snow at dawn, tracking shot",
62
+ duration=10, aspect_ratio="9:16")
63
+
64
+ # photo to video: the photo becomes the first frame
65
+ generate_video("the camera slowly orbits the statue", image_url="https://example.com/statue.jpg")
66
+
67
+ # another model
68
+ generate_video("a street drummer in the rain, close-up", model="seedance-2-5", duration=10, resolution="720p")
69
+ ```
70
+
71
+ | model | options |
72
+ |---|---|
73
+ | `minimax-h3` (default) | `duration` 4 / 5 / 6 / 8 / 10 / 15 · `resolution` 480p / 768p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
74
+ | `veo-3-1-fast` | `duration` 8 · `resolution` 720p · `aspect_ratio` 16:9 / 9:16 (photo to video: 16:9) |
75
+ | `seedance-2-5` | `duration` 5 / 10 / 15 / 20 / 25 / 30 · `resolution` 480p / 720p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
76
+ | `seedance-2`, `seedance-2-fast`, `seedance-2-mini` | `duration` 4 / 6 / 8 / 10 · `resolution` 480p / 720p · `aspect_ratio` |
77
+
78
+ Any extra 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).
79
+
80
+ ## Submit now, collect later
81
+
82
+ ```python
83
+ from movoa import submit, status, wait
84
+
85
+ task_id = submit("a lighthouse in a storm, waves crashing", "minimax-h3", duration=6)
86
+ print(status(task_id)["status"]) # pending | processing | success | failed | canceled
87
+ url = wait(task_id) # polls every 10 seconds, 30 minutes by default
88
+ ```
89
+
90
+ While a run waits in line, `status()` returns `queued: True` and `queueEta` in seconds.
91
+
92
+ ## Errors worth catching
93
+
94
+ Each failure is its own subclass of `MovoaError`, so you can branch on what to do next:
95
+
96
+ - `AuthError`: no key, or the key is wrong or deleted; create one at [movoa.ai/settings/apikeys](https://movoa.ai/settings/apikeys)
97
+ - `QuotaError`: the account does not have enough credits for this run
98
+ - `PlanError`: that model or quality needs a plan on the account
99
+ - `BusyError`: too many runs on this account are generating at once
100
+ - `RejectedError`: the content filter refused the prompt, or a cooldown is active; reword it
101
+ - `GenerationError`: the run was accepted but failed or was canceled; `.error_message` says why
102
+ - `TimeoutError`: the wait ran out; `.task_id` lets you check the run later with `status()`
103
+
104
+ Any other refusal raises `MovoaError` with the server's message.
105
+
106
+ MIT licensed. Made by [Movoa](https://movoa.ai/?utm_source=pypi&utm_medium=package).
movoa-0.1.0/README.md ADDED
@@ -0,0 +1,84 @@
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 Python SDK for [Movoa](https://movoa.ai/?utm_source=pypi&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
+ ```bash
6
+ pip install movoa
7
+ ```
8
+
9
+ ```python
10
+ from movoa import generate_video
11
+
12
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle, slow push-in")
13
+ print(clip) # URL of the finished MP4, sound included
14
+ ```
15
+
16
+ Standard library only, no dependencies. `generate_video` submits the run, waits for it and returns the URL of the finished file.
17
+
18
+ ## API key
19
+
20
+ Sign in on [movoa.ai](https://movoa.ai/?utm_source=pypi&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:
21
+
22
+ ```bash
23
+ export MOVOA_API_KEY=sk-...
24
+ ```
25
+
26
+ Runs cost credits on your Movoa account, at the prices on [movoa.ai/pricing](https://movoa.ai/pricing).
27
+
28
+ ```python
29
+ from movoa import credits
30
+ print(credits()) # remaining credits on the account
31
+ ```
32
+
33
+ ## Text to video and photo to video
34
+
35
+ ```python
36
+ from movoa import generate_video
37
+
38
+ # text to video, vertical, 10 seconds
39
+ generate_video("a fox trotting through fresh snow at dawn, tracking shot",
40
+ duration=10, aspect_ratio="9:16")
41
+
42
+ # photo to video: the photo becomes the first frame
43
+ generate_video("the camera slowly orbits the statue", image_url="https://example.com/statue.jpg")
44
+
45
+ # another model
46
+ generate_video("a street drummer in the rain, close-up", model="seedance-2-5", duration=10, 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 extra 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
+ ```python
61
+ from movoa import submit, status, wait
62
+
63
+ task_id = submit("a lighthouse in a storm, waves crashing", "minimax-h3", duration=6)
64
+ print(status(task_id)["status"]) # pending | processing | success | failed | canceled
65
+ url = wait(task_id) # polls every 10 seconds, 30 minutes by default
66
+ ```
67
+
68
+ While a run waits in line, `status()` returns `queued: True` and `queueEta` in seconds.
69
+
70
+ ## Errors worth catching
71
+
72
+ Each failure is its own subclass of `MovoaError`, so you can branch on what to do next:
73
+
74
+ - `AuthError`: no key, or the key is wrong or deleted; create one at [movoa.ai/settings/apikeys](https://movoa.ai/settings/apikeys)
75
+ - `QuotaError`: the account does not have enough credits for this run
76
+ - `PlanError`: that model or quality needs a plan on the account
77
+ - `BusyError`: too many runs on this account are generating at once
78
+ - `RejectedError`: the content filter refused the prompt, or a cooldown is active; reword it
79
+ - `GenerationError`: the run was accepted but failed or was canceled; `.error_message` says why
80
+ - `TimeoutError`: the wait ran out; `.task_id` lets you check the run later with `status()`
81
+
82
+ Any other refusal raises `MovoaError` with the server's message.
83
+
84
+ MIT licensed. Made by [Movoa](https://movoa.ai/?utm_source=pypi&utm_medium=package).
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "movoa"
7
+ version = "0.1.0"
8
+ description = "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. Official Python SDK for Movoa (movoa.ai): text to video and photo to video with sound from your own code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Movoa (https://movoa.ai)" }]
13
+ keywords = ["ai", "video-generation", "ai-video-generator", "text-to-video", "image-to-video", "photo-to-video", "ai-video-with-audio", "minimax-h3", "seedance-2-5", "veo-3", "movoa"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Multimedia :: Video",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://movoa.ai/?utm_source=pypi&utm_medium=package"
24
+ "Text to video" = "https://movoa.ai/video/text-to-video"
25
+ "API keys" = "https://movoa.ai/settings/apikeys"
26
+ Pricing = "https://movoa.ai/pricing"
27
+ Source = "https://github.com/hanshs474/movoa-py"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
movoa-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,305 @@
1
+ """Official Python SDK for Movoa (https://movoa.ai), the AI video generator with sound.
2
+
3
+ from movoa import generate_video
4
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle")
5
+ still = generate_video("the camera slowly orbits the statue",
6
+ image_url="https://example.com/statue.jpg")
7
+
8
+ Create a key at https://movoa.ai/settings/apikeys and pass it as ``api_key=``
9
+ or set ``MOVOA_API_KEY``. Runs cost credits on your Movoa account, at the
10
+ prices on https://movoa.ai/pricing.
11
+
12
+ Standard library only.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import time
20
+ import urllib.error
21
+ import urllib.request
22
+ from typing import Any, Callable, Dict, Optional, Tuple
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = [
26
+ "generate_video",
27
+ "submit",
28
+ "status",
29
+ "wait",
30
+ "credits",
31
+ "MovoaError",
32
+ "AuthError",
33
+ "QuotaError",
34
+ "PlanError",
35
+ "BusyError",
36
+ "RejectedError",
37
+ "GenerationError",
38
+ "TimeoutError",
39
+ "MODELS",
40
+ "DEFAULT_MODEL",
41
+ "BASE_URL",
42
+ ]
43
+
44
+ BASE_URL = "https://movoa.ai"
45
+ KEYS_URL = BASE_URL + "/settings/apikeys"
46
+
47
+ #: Model ids. ``minimax-h3`` is the default and the cheapest.
48
+ MODELS = (
49
+ "minimax-h3",
50
+ "veo-3-1-fast",
51
+ "seedance-2-5",
52
+ "seedance-2",
53
+ "seedance-2-fast",
54
+ "seedance-2-mini",
55
+ )
56
+ DEFAULT_MODEL = "minimax-h3"
57
+
58
+ POLL_EVERY = 10.0
59
+ TIMEOUT = 30 * 60.0
60
+
61
+
62
+ class MovoaError(RuntimeError):
63
+ """Base class. Catch a subclass to decide what to do next.
64
+
65
+ ``status`` is the HTTP status, ``code`` the envelope code (0, -1, -2, -3),
66
+ ``reason`` the ``data.reason`` the service sent, if any.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ message: str,
72
+ status: Optional[int] = None,
73
+ code: Optional[int] = None,
74
+ reason: Optional[str] = None,
75
+ task_id: Optional[str] = None,
76
+ ):
77
+ super().__init__(message)
78
+ self.status = status
79
+ self.code = code
80
+ self.reason = reason
81
+ self.task_id = task_id
82
+
83
+
84
+ class AuthError(MovoaError):
85
+ """No API key, or the key is invalid or deleted. Create one at /settings/apikeys."""
86
+
87
+
88
+ class QuotaError(MovoaError):
89
+ """The account does not have enough credits for this run."""
90
+
91
+
92
+ class PlanError(MovoaError):
93
+ """This model or quality needs a plan, or the account has never bought credits."""
94
+
95
+
96
+ class BusyError(MovoaError):
97
+ """Too many runs on this account are generating at once. Wait for one to finish."""
98
+
99
+
100
+ class RejectedError(MovoaError):
101
+ """The content filter refused the prompt (code -2) or a cooldown is active (code -3). Reword it."""
102
+
103
+
104
+ class GenerationError(MovoaError):
105
+ """The run was accepted but ended as failed or canceled.
106
+
107
+ ``error_message`` and ``failure_kind`` carry what the service reported.
108
+ """
109
+
110
+ def __init__(self, message: str, task_id: str, error_message: Optional[str] = None, failure_kind: Optional[str] = None):
111
+ super().__init__(message, task_id=task_id)
112
+ self.error_message = error_message
113
+ self.failure_kind = failure_kind
114
+
115
+
116
+ class TimeoutError(MovoaError): # noqa: A001 - mirrors the SDK error set of every language
117
+ """The wait ran out. The run may still finish; ``task_id`` lets you check it later."""
118
+
119
+
120
+ #: (method, url, body, headers) -> (http_status, raw_body_bytes). Raise OSError on network failure.
121
+ Transport = Callable[[str, str, Optional[dict], dict], Tuple[int, bytes]]
122
+
123
+
124
+ def _http(method: str, url: str, body: Optional[dict], headers: dict) -> Tuple[int, bytes]:
125
+ data = json.dumps(body).encode() if body is not None else b""
126
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
127
+ try:
128
+ with urllib.request.urlopen(req, timeout=60) as r:
129
+ return r.status, r.read()
130
+ except urllib.error.HTTPError as e:
131
+ return e.code, e.read() or b""
132
+
133
+
134
+ def _base(base_url: Optional[str]) -> str:
135
+ return (base_url or os.environ.get("MOVOA_BASE_URL") or BASE_URL).rstrip("/")
136
+
137
+
138
+ def _key(api_key: Optional[str]) -> str:
139
+ key = (api_key or os.environ.get("MOVOA_API_KEY") or "").strip()
140
+ if not key:
141
+ raise AuthError(f"No API key: pass api_key= or set MOVOA_API_KEY. Create a key at {KEYS_URL}")
142
+ return key
143
+
144
+
145
+ def _raise_for(status_code: int, env: dict) -> None:
146
+ """Map a refusal to the subclass that says what to do next.
147
+
148
+ movoa.ai answers almost every refusal as HTTP 200 with a non-zero ``code``
149
+ and a ``data.reason``, so the status code alone reads a refusal as success.
150
+ """
151
+ code = env.get("code") if isinstance(env.get("code"), int) else None
152
+ data = env.get("data") if isinstance(env.get("data"), dict) else {}
153
+ reason = data.get("reason") if isinstance(data.get("reason"), str) else None
154
+ msg = str(env.get("message") or f"request failed (HTTP {status_code})")
155
+ kw = dict(status=status_code, code=code, reason=reason)
156
+ if status_code == 401:
157
+ raise AuthError(f"{msg} (create a key at {KEYS_URL})" if KEYS_URL not in msg else msg, **kw)
158
+ if code in (-2, -3):
159
+ raise RejectedError(msg, **kw)
160
+ if reason == "insufficient_credits":
161
+ raise QuotaError(msg, **kw)
162
+ if reason == "plan_required":
163
+ raise PlanError(msg, **kw)
164
+ if reason == "concurrency":
165
+ raise BusyError(msg, **kw)
166
+ if "no auth" in msg.lower():
167
+ raise AuthError(f"{msg}. Create a key at {KEYS_URL}", **kw)
168
+ raise MovoaError(msg, **kw)
169
+
170
+
171
+ def _call(path: str, body: Optional[dict], api_key: Optional[str], base_url: Optional[str], transport: Optional[Transport]) -> dict:
172
+ headers = {
173
+ "Authorization": f"Bearer {_key(api_key)}",
174
+ "Content-Type": "application/json",
175
+ "User-Agent": f"movoa-python/{__version__}",
176
+ }
177
+ st, raw = (transport or _http)("POST", _base(base_url) + path, body, headers)
178
+ try:
179
+ env = json.loads(raw) if raw else {}
180
+ except ValueError:
181
+ env = None
182
+ if st == 401:
183
+ _raise_for(401, env if isinstance(env, dict) else {})
184
+ if not isinstance(env, dict):
185
+ raise MovoaError(f"movoa.ai returned a non-JSON response (HTTP {st})", status=st)
186
+ if st < 400 and env.get("code") == 0:
187
+ return env
188
+ _raise_for(st, env)
189
+ raise AssertionError("unreachable")
190
+
191
+
192
+ def submit(
193
+ prompt: str,
194
+ model: str = DEFAULT_MODEL,
195
+ image_url: Optional[str] = None,
196
+ *,
197
+ api_key: Optional[str] = None,
198
+ base_url: Optional[str] = None,
199
+ transport: Optional[Transport] = None,
200
+ **options: Any,
201
+ ) -> str:
202
+ """Start one run and return its task id without waiting.
203
+
204
+ ``image_url`` (a public https image) makes it image-to-video, with the
205
+ photo as the first frame. Extra keyword arguments go into ``options`` as
206
+ they are, e.g. ``duration=6, resolution="768p", aspect_ratio="9:16"``.
207
+ """
208
+ if not str(prompt or "").strip():
209
+ raise ValueError("prompt is required")
210
+ opts: Dict[str, Any] = {"sound": True}
211
+ opts.update({k: v for k, v in options.items() if v is not None})
212
+ if image_url:
213
+ opts["image_input"] = [image_url]
214
+ body = {
215
+ # The server routes by model; this only has to be non-empty.
216
+ "provider": "auto",
217
+ "mediaType": "video",
218
+ "model": model,
219
+ "scene": "image-to-video" if image_url else "text-to-video",
220
+ "prompt": prompt,
221
+ "options": opts,
222
+ }
223
+ env = _call("/api/ai/generate", body, api_key, base_url, transport)
224
+ task_id = (env.get("data") or {}).get("id")
225
+ if not task_id:
226
+ raise MovoaError("movoa.ai accepted the run but returned no id")
227
+ return str(task_id)
228
+
229
+
230
+ def status(
231
+ task_id: str,
232
+ *,
233
+ api_key: Optional[str] = None,
234
+ base_url: Optional[str] = None,
235
+ transport: Optional[Transport] = None,
236
+ ) -> Dict[str, Any]:
237
+ """Return the run record. ``status`` is pending, processing, success, failed
238
+ or canceled; ``videos`` holds the result URLs on success; ``queued`` and
239
+ ``queueEta`` (seconds) are set while it waits in line."""
240
+ env = _call("/api/ai/query", {"taskId": task_id}, api_key, base_url, transport)
241
+ data = env.get("data")
242
+ return data if isinstance(data, dict) else {}
243
+
244
+
245
+ def wait(
246
+ task_id: str,
247
+ *,
248
+ api_key: Optional[str] = None,
249
+ timeout: float = TIMEOUT,
250
+ poll_every: float = POLL_EVERY,
251
+ base_url: Optional[str] = None,
252
+ transport: Optional[Transport] = None,
253
+ ) -> str:
254
+ """Poll a run every ``poll_every`` seconds until it finishes and return the video URL."""
255
+ deadline = time.monotonic() + timeout
256
+ while True:
257
+ try:
258
+ data = status(task_id, api_key=api_key, base_url=base_url, transport=transport)
259
+ except OSError:
260
+ data = {} # a dropped poll is not a failed generation
261
+ state = str(data.get("status") or "").lower()
262
+ if state == "success":
263
+ videos = data.get("videos") or []
264
+ if not videos:
265
+ raise GenerationError(f"run {task_id} finished with no video", task_id)
266
+ return str(videos[0])
267
+ if state in ("failed", "canceled"):
268
+ em = data.get("errorMessage")
269
+ raise GenerationError(
270
+ f"run {task_id} {state}" + (f": {em}" if em else ""), task_id, em, data.get("failureKind")
271
+ )
272
+ if time.monotonic() + poll_every > deadline:
273
+ raise TimeoutError(
274
+ f"run {task_id} is still generating after {timeout:g}s; check it later with status()",
275
+ task_id=task_id,
276
+ )
277
+ time.sleep(poll_every)
278
+
279
+
280
+ def generate_video(
281
+ prompt: str,
282
+ model: str = DEFAULT_MODEL,
283
+ image_url: Optional[str] = None,
284
+ *,
285
+ api_key: Optional[str] = None,
286
+ timeout: float = TIMEOUT,
287
+ poll_every: float = POLL_EVERY,
288
+ base_url: Optional[str] = None,
289
+ transport: Optional[Transport] = None,
290
+ **options: Any,
291
+ ) -> str:
292
+ """Generate one clip with sound and return its URL."""
293
+ task_id = submit(prompt, model, image_url, api_key=api_key, base_url=base_url, transport=transport, **options)
294
+ return wait(task_id, api_key=api_key, timeout=timeout, poll_every=poll_every, base_url=base_url, transport=transport)
295
+
296
+
297
+ def credits(
298
+ *,
299
+ api_key: Optional[str] = None,
300
+ base_url: Optional[str] = None,
301
+ transport: Optional[Transport] = None,
302
+ ) -> int:
303
+ """Return the remaining credits on the account."""
304
+ env = _call("/api/user/get-user-credits", None, api_key, base_url, transport)
305
+ return int((env.get("data") or {}).get("remainingCredits") or 0)
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: movoa
3
+ Version: 0.1.0
4
+ Summary: 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. Official Python SDK for Movoa (movoa.ai): text to video and photo to video with sound from your own code.
5
+ Author: Movoa (https://movoa.ai)
6
+ License: MIT
7
+ Project-URL: Homepage, https://movoa.ai/?utm_source=pypi&utm_medium=package
8
+ Project-URL: Text to video, https://movoa.ai/video/text-to-video
9
+ Project-URL: API keys, https://movoa.ai/settings/apikeys
10
+ Project-URL: Pricing, https://movoa.ai/pricing
11
+ Project-URL: Source, https://github.com/hanshs474/movoa-py
12
+ Keywords: ai,video-generation,ai-video-generator,text-to-video,image-to-video,photo-to-video,ai-video-with-audio,minimax-h3,seedance-2-5,veo-3,movoa
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Multimedia :: Video
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # movoa
24
+
25
+ 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 Python SDK for [Movoa](https://movoa.ai/?utm_source=pypi&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).
26
+
27
+ ```bash
28
+ pip install movoa
29
+ ```
30
+
31
+ ```python
32
+ from movoa import generate_video
33
+
34
+ clip = generate_video("a paper boat drifting down a rain gutter, low angle, slow push-in")
35
+ print(clip) # URL of the finished MP4, sound included
36
+ ```
37
+
38
+ Standard library only, no dependencies. `generate_video` submits the run, waits for it and returns the URL of the finished file.
39
+
40
+ ## API key
41
+
42
+ Sign in on [movoa.ai](https://movoa.ai/?utm_source=pypi&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:
43
+
44
+ ```bash
45
+ export MOVOA_API_KEY=sk-...
46
+ ```
47
+
48
+ Runs cost credits on your Movoa account, at the prices on [movoa.ai/pricing](https://movoa.ai/pricing).
49
+
50
+ ```python
51
+ from movoa import credits
52
+ print(credits()) # remaining credits on the account
53
+ ```
54
+
55
+ ## Text to video and photo to video
56
+
57
+ ```python
58
+ from movoa import generate_video
59
+
60
+ # text to video, vertical, 10 seconds
61
+ generate_video("a fox trotting through fresh snow at dawn, tracking shot",
62
+ duration=10, aspect_ratio="9:16")
63
+
64
+ # photo to video: the photo becomes the first frame
65
+ generate_video("the camera slowly orbits the statue", image_url="https://example.com/statue.jpg")
66
+
67
+ # another model
68
+ generate_video("a street drummer in the rain, close-up", model="seedance-2-5", duration=10, resolution="720p")
69
+ ```
70
+
71
+ | model | options |
72
+ |---|---|
73
+ | `minimax-h3` (default) | `duration` 4 / 5 / 6 / 8 / 10 / 15 · `resolution` 480p / 768p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
74
+ | `veo-3-1-fast` | `duration` 8 · `resolution` 720p · `aspect_ratio` 16:9 / 9:16 (photo to video: 16:9) |
75
+ | `seedance-2-5` | `duration` 5 / 10 / 15 / 20 / 25 / 30 · `resolution` 480p / 720p · `aspect_ratio` 16:9 / 9:16 / 1:1 |
76
+ | `seedance-2`, `seedance-2-fast`, `seedance-2-mini` | `duration` 4 / 6 / 8 / 10 · `resolution` 480p / 720p · `aspect_ratio` |
77
+
78
+ Any extra 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).
79
+
80
+ ## Submit now, collect later
81
+
82
+ ```python
83
+ from movoa import submit, status, wait
84
+
85
+ task_id = submit("a lighthouse in a storm, waves crashing", "minimax-h3", duration=6)
86
+ print(status(task_id)["status"]) # pending | processing | success | failed | canceled
87
+ url = wait(task_id) # polls every 10 seconds, 30 minutes by default
88
+ ```
89
+
90
+ While a run waits in line, `status()` returns `queued: True` and `queueEta` in seconds.
91
+
92
+ ## Errors worth catching
93
+
94
+ Each failure is its own subclass of `MovoaError`, so you can branch on what to do next:
95
+
96
+ - `AuthError`: no key, or the key is wrong or deleted; create one at [movoa.ai/settings/apikeys](https://movoa.ai/settings/apikeys)
97
+ - `QuotaError`: the account does not have enough credits for this run
98
+ - `PlanError`: that model or quality needs a plan on the account
99
+ - `BusyError`: too many runs on this account are generating at once
100
+ - `RejectedError`: the content filter refused the prompt, or a cooldown is active; reword it
101
+ - `GenerationError`: the run was accepted but failed or was canceled; `.error_message` says why
102
+ - `TimeoutError`: the wait ran out; `.task_id` lets you check the run later with `status()`
103
+
104
+ Any other refusal raises `MovoaError` with the server's message.
105
+
106
+ MIT licensed. Made by [Movoa](https://movoa.ai/?utm_source=pypi&utm_medium=package).
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/movoa/__init__.py
5
+ src/movoa.egg-info/PKG-INFO
6
+ src/movoa.egg-info/SOURCES.txt
7
+ src/movoa.egg-info/dependency_links.txt
8
+ src/movoa.egg-info/top_level.txt
9
+ tests/test_movoa.py
@@ -0,0 +1 @@
1
+ movoa
@@ -0,0 +1,135 @@
1
+ import json
2
+ import os
3
+ import unittest
4
+
5
+ import movoa
6
+
7
+
8
+ def reply(status, body):
9
+ raw = body if isinstance(body, bytes) else json.dumps(body).encode()
10
+ return status, raw
11
+
12
+
13
+ def fake(calls, polls, submit_reply=None):
14
+ """Answers in the real response shapes of movoa.ai."""
15
+ polls = list(polls)
16
+ submit_reply = submit_reply or reply(200, {"code": 0, "message": "ok", "data": {"id": "t1", "status": "pending"}})
17
+
18
+ def t(method, url, body, headers):
19
+ calls.append((method, url, body, headers))
20
+ if url.endswith("/api/ai/generate"):
21
+ return submit_reply
22
+ if url.endswith("/api/ai/query"):
23
+ return reply(200, {"code": 0, "message": "ok", "data": polls.pop(0)})
24
+ return reply(200, {"code": 0, "message": "ok", "data": {"remainingCredits": 42}})
25
+
26
+ return t
27
+
28
+
29
+ class MovoaTests(unittest.TestCase):
30
+ def setUp(self):
31
+ self.live_key = os.environ.pop("MOVOA_API_KEY", None)
32
+ os.environ.pop("MOVOA_BASE_URL", None)
33
+
34
+ def test_text_to_video_success(self):
35
+ calls = []
36
+ polls = [
37
+ {"id": "t1", "status": "pending", "queued": True, "queueEta": 30},
38
+ {"id": "t1", "status": "processing"},
39
+ {"id": "t1", "status": "success", "videos": ["https://cdn/v.mp4"]},
40
+ ]
41
+ url = movoa.generate_video("a boat", api_key="sk-abc", duration=6, poll_every=0, base_url="https://t", transport=fake(calls, polls))
42
+ self.assertEqual(url, "https://cdn/v.mp4")
43
+ method, u, body, headers = calls[0]
44
+ self.assertEqual((method, u), ("POST", "https://t/api/ai/generate"))
45
+ self.assertEqual(headers["Authorization"], "Bearer sk-abc")
46
+ self.assertEqual(body, {
47
+ "provider": "auto", "mediaType": "video", "model": "minimax-h3", "scene": "text-to-video",
48
+ "prompt": "a boat", "options": {"sound": True, "duration": 6},
49
+ })
50
+ self.assertEqual(calls[1][2], {"taskId": "t1"})
51
+ self.assertEqual(len(calls), 4)
52
+
53
+ def test_image_to_video_body(self):
54
+ calls = []
55
+ movoa.submit("orbit", "seedance-2-5", image_url="https://x/y.jpg", api_key="sk", aspect_ratio="9:16", transport=fake(calls, []))
56
+ body = calls[0][2]
57
+ self.assertEqual(body["scene"], "image-to-video")
58
+ self.assertEqual(body["model"], "seedance-2-5")
59
+ self.assertEqual(body["options"], {"sound": True, "aspect_ratio": "9:16", "image_input": ["https://x/y.jpg"]})
60
+
61
+ def test_base_url_from_env(self):
62
+ os.environ["MOVOA_BASE_URL"] = "http://localhost:3000/"
63
+ calls = []
64
+ movoa.credits(api_key="sk", transport=fake(calls, []))
65
+ self.assertEqual(calls[0][1], "http://localhost:3000/api/user/get-user-credits")
66
+
67
+ def test_validates_prompt(self):
68
+ with self.assertRaises(ValueError):
69
+ movoa.submit(" ", api_key="sk")
70
+
71
+ def test_needs_a_key(self):
72
+ def boom(*a):
73
+ raise AssertionError("no request without a key")
74
+
75
+ with self.assertRaises(movoa.AuthError):
76
+ movoa.credits(transport=boom)
77
+
78
+ def test_error_mapping(self):
79
+ cases = [
80
+ (reply(401, {"code": -1, "message": "Invalid API key. Create one at https://movoa.ai/settings/apikeys", "data": {"reason": "invalid_api_key"}}), movoa.AuthError),
81
+ (reply(401, b"<html>unauthorized</html>"), movoa.AuthError),
82
+ (reply(200, {"code": -1, "message": "insufficient credits", "data": {"reason": "insufficient_credits"}}), movoa.QuotaError),
83
+ (reply(200, {"code": -1, "message": "This model or quality needs a plan.", "data": {"reason": "plan_required"}}), movoa.PlanError),
84
+ (reply(200, {"code": -1, "message": "busy", "data": {"reason": "concurrency"}}), movoa.BusyError),
85
+ (reply(200, {"code": -2, "message": "blocked"}), movoa.RejectedError),
86
+ (reply(200, {"code": -3, "message": "cooldown"}), movoa.RejectedError),
87
+ (reply(200, {"code": -1, "message": "no auth, please sign in"}), movoa.AuthError),
88
+ ]
89
+ for r, exc in cases:
90
+ with self.assertRaises(exc, msg=str(r)):
91
+ movoa.submit("x", api_key="sk", transport=fake([], [], r))
92
+ for r in (reply(200, {"code": -1, "message": "generation failed, please try again"}), reply(502, b"Bad gateway"), reply(500, {"code": -1, "message": "oops"})):
93
+ with self.assertRaises(movoa.MovoaError) as cm:
94
+ movoa.submit("x", api_key="sk", transport=fake([], [], r))
95
+ self.assertIs(type(cm.exception), movoa.MovoaError)
96
+ with self.assertRaises(movoa.MovoaError) as cm:
97
+ movoa.submit("x", api_key="sk", transport=fake([], [], reply(200, {"code": -1, "message": "generation failed, please try again"})))
98
+ self.assertIn("generation failed", str(cm.exception))
99
+
100
+ def test_failed_and_canceled(self):
101
+ with self.assertRaises(movoa.GenerationError) as cm:
102
+ movoa.generate_video("x", api_key="sk", poll_every=0, transport=fake([], [{"id": "t1", "status": "failed", "errorMessage": "provider down", "failureKind": "upstream"}]))
103
+ self.assertEqual(cm.exception.error_message, "provider down")
104
+ self.assertEqual(cm.exception.failure_kind, "upstream")
105
+ self.assertEqual(cm.exception.task_id, "t1")
106
+ self.assertIn("provider down", str(cm.exception))
107
+ with self.assertRaises(movoa.GenerationError):
108
+ movoa.wait("t1", api_key="sk", poll_every=0, transport=fake([], [{"id": "t1", "status": "canceled"}]))
109
+
110
+ def test_timeout_carries_task_id(self):
111
+ polls = [{"id": "t1", "status": "processing", "queued": True, "queueEta": 60}] * 5
112
+ with self.assertRaises(movoa.TimeoutError) as cm:
113
+ movoa.wait("t1", api_key="sk", timeout=0.001, poll_every=0.01, transport=fake([], polls))
114
+ self.assertEqual(cm.exception.task_id, "t1")
115
+
116
+ def test_status_and_credits(self):
117
+ st = movoa.status("t1", api_key="sk", transport=fake([], [{"id": "t1", "status": "success", "videos": ["https://cdn/v.mp4"]}]))
118
+ self.assertEqual(st["videos"][0], "https://cdn/v.mp4")
119
+ self.assertEqual(movoa.credits(api_key="sk", transport=fake([], [])), 42)
120
+
121
+ def no_auth(method, url, body, headers):
122
+ return reply(200, {"code": -1, "message": "no auth, please sign in"})
123
+
124
+ with self.assertRaises(movoa.AuthError):
125
+ movoa.credits(api_key="sk", transport=no_auth)
126
+
127
+ @unittest.skipUnless(os.environ.get("MOVOA_LIVE"), "set MOVOA_LIVE=1 MOVOA_API_KEY=sk-... after key auth is live")
128
+ def test_live_smoke(self):
129
+ self.assertGreaterEqual(movoa.credits(api_key=self.live_key), 0)
130
+ with self.assertRaises(movoa.AuthError):
131
+ movoa.submit("test", api_key="sk-wrong-key-for-test")
132
+
133
+
134
+ if __name__ == "__main__":
135
+ unittest.main()