gpt-video 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GPT-Video
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.
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.5
2
+ Name: gpt-video
3
+ Version: 1.0.0
4
+ Summary: Python client for the GPT-Video API: upload a video, find its clips, edit and export them.
5
+ Project-URL: Homepage, https://www.gpt-video.com/developers
6
+ Project-URL: Repository, https://github.com/gptvideo/gpt-video-sdks
7
+ Project-URL: Issues, https://github.com/gptvideo/gpt-video-sdks/issues
8
+ Author-email: GPT-Video <hello@gpt-video.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,clips,gpt-video,sdk,video
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Multimedia :: Video
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ # gpt-video
28
+
29
+ The official Python SDK for the GPT-Video API.
30
+
31
+ GPT-Video turns a long video into short vertical clips with captions and face framing. Your program uploads a video, GPT-Video finds the moments that work as clips, and your program exports the ones it keeps as MP4 files.
32
+
33
+ - One method for each of the 67 operations of the API, with the names of the [API reference](https://www.gpt-video.com/developers/api): `analyze_project`.
34
+ - Retries that never pay twice: a call that spends sends the same Idempotency-Key on every attempt.
35
+ - Helpers for the work that takes more than one request: `upload_file`, `wait`, `paginate`, `download`, and webhook verification.
36
+ - Python 3.10 or later, the standard library only, and typed (`py.typed`).
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ pip install gpt-video
42
+ ```
43
+
44
+ The package is `gpt_video`.
45
+
46
+ ## Your first clip
47
+
48
+ 1. Create a GPT-Video account and confirm your email address.
49
+ 2. Open the Developers page of the app, https://www.gpt-video.com/app/developers, and create an API key with the scopes `clips:read`, `clips:write` and `spend`. The key is shown once, so copy it then.
50
+ 3. Put the key in the environment, where the SDK reads it, and run the quickstart on a video of yours:
51
+
52
+ ```sh
53
+ export GPT_VIDEO_API_KEY=gvk_...
54
+ python quickstart.py talk.mp4
55
+ ```
56
+
57
+ The program below is `examples/quickstart.py`. It checks the key, uploads the video, shows the price, analyzes the video, accepts the first clip, exports it and saves it as `clip.mp4`. Our test suite runs this file, unchanged, against a test server of the API.
58
+
59
+ ```python
60
+ """GPT-Video quickstart: from a video file to a vertical clip on your disk.
61
+
62
+ pip install gpt-video
63
+ export GPT_VIDEO_API_KEY=gvk_...
64
+ python quickstart.py talk.mp4
65
+
66
+ Steps 1 to 4 use no credits. Step 5 spends credits: one per started minute of the video.
67
+ Step 7 uses one export of your daily allowance, and one API bonus credit while the bonus pays.
68
+ """
69
+
70
+ import os
71
+ import sys
72
+
73
+ from gpt_video import GptVideo, GptVideoError
74
+
75
+
76
+ def spend_once(call):
77
+ """A call that spends and ends without a clear answer (the network failed, or the time ran out) may have been
78
+ charged. Calling again with the same idempotency_key returns the first answer and never pays twice."""
79
+ try:
80
+ return call(None)
81
+ except GptVideoError as error:
82
+ if not error.may_have_spent:
83
+ raise
84
+ print(f"No clear answer ({error.code}). Asking again with the same Idempotency-Key.")
85
+ return call(error.idempotency_key)
86
+
87
+
88
+ def main(path):
89
+ gv = GptVideo() # reads GPT_VIDEO_API_KEY
90
+
91
+ # 1. Check the key.
92
+ me = gv.get_me()
93
+ print(f"Key {me['key']['id']} works. Scopes: {', '.join(me['key']['scopes'])}. Plan: {me['account']['plan']}.")
94
+
95
+ # 2. Upload the video. The SDK sends it in parts and reads one part at a time from the disk.
96
+ asset = gv.upload_file(path, on_progress=lambda p: print(f"Uploaded {round(100 * p.uploaded_bytes / p.total_bytes)}%"))["asset"]
97
+ print(f"Video {asset['id']} is ready.")
98
+
99
+ # 3. Create an Auto Clip project. Auto Clip has no default settings: the analysis starts only when
100
+ # missingSettings is empty, so choose every one of them.
101
+ project = gv.create_project(
102
+ sourceAssetId=asset["id"],
103
+ title=os.path.basename(path),
104
+ settings={
105
+ "language": "auto",
106
+ "clipCount": 3,
107
+ "minDurationSec": 15,
108
+ "maxDurationSec": 60,
109
+ "objective": "viral",
110
+ "captionStyle": "word-pop",
111
+ "reframe": "face",
112
+ },
113
+ )["project"]
114
+ if project["missingSettings"]:
115
+ raise SystemExit(f"Settings still to choose: {', '.join(project['missingSettings'])}")
116
+
117
+ # 4. Ask the price of the analysis.
118
+ estimate = gv.estimate_analysis(projectId=project["id"])["estimate"]
119
+ print(f"The analysis costs {estimate['creditsToCharge']} credits.")
120
+ if not estimate["enough"]:
121
+ raise SystemExit("The account cannot pay for it. See https://www.gpt-video.com/developers/pricing")
122
+
123
+ # 5. Analyze the video, and wait until its clips are found.
124
+ analysis = spend_once(lambda key: gv.analyze_project(projectId=project["id"], idempotency_key=key))
125
+ print(f"Analysis started ({analysis['creditsCharged']} credits charged). Waiting for job {analysis['job']['id']}.")
126
+ gv.wait(analysis)
127
+
128
+ # 6. Read the clips in the order the analysis ranked them, and accept the first one. Accepting makes a
129
+ # composition, whose framing follows the face of the speaker; it can be exported once its framing job is done.
130
+ clips = list(gv.paginate("list_clips", projectId=project["id"], status="proposed"))
131
+ for clip in clips:
132
+ print(f"{clip['rank'] + 1}. {clip['title']} ({clip['startSec']}s to {clip['endSec']}s, score {clip['score']})")
133
+ if not clips:
134
+ raise SystemExit("The analysis found no clips in this video.")
135
+ composition = gv.accept_clip(clipId=clips[0]["id"])["composition"]
136
+ if composition["framing"]["jobId"]:
137
+ gv.wait_for_job(composition["framing"]["jobId"])
138
+
139
+ # 7. Export the composition to an MP4 file, wait for it, and download it. The download link is its own
140
+ # credential: the SDK never sends your API key to it.
141
+ exported = spend_once(lambda key: gv.create_export(compositionId=composition["id"], idempotency_key=key))
142
+ done = gv.wait(exported)
143
+ size = gv.download(done["downloadUrl"], "clip.mp4")
144
+ print(f"Saved clip.mp4 ({size} bytes).")
145
+
146
+
147
+ if __name__ == "__main__":
148
+ if len(sys.argv) != 2:
149
+ raise SystemExit("Usage: python quickstart.py <video file>")
150
+ main(sys.argv[1])
151
+ ```
152
+
153
+ ## Arguments
154
+
155
+ The fields of a call keep the names of the API, in camelCase: `projectId`, `sourceAssetId`. Pass them as keyword arguments, `gv.analyze_project(projectId="prj_...")`, or as one dict, `gv.analyze_project({"projectId": "prj_..."})`, but not both in the same call. The options of the SDK are keyword arguments in snake_case: `idempotency_key`, `timeout`, `deadline`, `max_retries`, `cancel`.
156
+
157
+ Every answer is the JSON body of the API as a dict, as the API sent it.
158
+
159
+ ## What uses credits
160
+
161
+ - `get_me`, `upload_file`, `create_project`, `estimate_analysis`, `list_clips`, `accept_clip` and every other read use no credits. An upload counts toward the storage of the account.
162
+ - `analyze_project` charges one credit per started minute of the source video when the analysis starts. The credits are not returned if the analysis fails, and analyzing again is charged again.
163
+ - `create_export` uses one export of the daily allowance, which starts again at 00:00 UTC. While the API bonus pays, it also uses one bonus credit.
164
+
165
+ Every method links to its page of the API reference, which says what it costs. [API pricing](https://www.gpt-video.com/developers/pricing) has the API bonus, the plans and the daily limits.
166
+
167
+ ## When a call fails
168
+
169
+ Every error the SDK raises is a `GptVideoError`, with these fields:
170
+
171
+ | Field | What it says |
172
+ |---|---|
173
+ | `code` | The code of the API, such as `NOT_READY`, or one of the SDK's: `CONNECTION_FAILED`, `TIMEOUT`, `ABORTED`, `BAD_RESPONSE`, `INVALID_ARGUMENT`, `JOB_FAILED`, `JOB_CANCELLED`, `WAIT_TIMEOUT`, `WEBHOOK_VERIFICATION_FAILED` |
174
+ | `retryable` | Whether the same call, sent again with the same `idempotency_key`, can succeed |
175
+ | `operation` | The operation of the call, such as `analyze_project` |
176
+ | `idempotency_key` | The Idempotency-Key the call sent, or None for a call that sends none |
177
+ | `may_have_spent` | True when a call that spends ended without a clear answer |
178
+
179
+ `GptVideoApiError` is an error the API answered, with `status`, `request_id`, `hint`, `details` and `retry_after_sec`. `GptVideoNetworkError` is a request that got no usable answer. `GptVideoJobError` is work that `wait` was following and that failed, was cancelled or ran past the time of the wait: `handle` is the job, the export, the tool run or the turn as the last read gave it, and `error_message` says why the work failed. The message of an error has the form `NOT_READY (409): message of the API Hint: what to do [request req_...]`. The API key never appears in an error.
180
+
181
+ When Python cannot check the TLS certificate of the API, the error is `CONNECTION_FAILED` and says so. On macOS, run `Install Certificates.command` from the folder of your Python version.
182
+
183
+ ### Retries
184
+
185
+ The SDK sends a call again after a failure only when that is safe: a read, a PATCH, PUT or DELETE, which has the same effect when sent twice, or a POST that carries an Idempotency-Key. It retries `CONNECTION_FAILED`, `TIMEOUT`, `NOT_READY`, `INTERNAL`, `UPSTREAM_FAILED`, `UNAVAILABLE` (unless the API is switched off) and a `BAD_RESPONSE` with a 5xx status, up to `max_retries` times (2). It waits the seconds of `Retry-After` when the API sends it, and otherwise a growing pause with jitter. A `Retry-After` longer than 60 seconds, or longer than the time the call has left, ends the call, and the error carries it in `retry_after_sec`. `RATE_LIMITED` is retried for every call, because the API refuses it before running anything. `on_retry` is told before every pause.
186
+
187
+ `IDEMPOTENCY_IN_PROGRESS` means the first request with the same key is still running on the server. The SDK waits and asks again with the same key until it gets the answer, and this does not count toward `max_retries`.
188
+
189
+ `timeout` (60 seconds) is the time of one attempt, and `deadline` (180 seconds) the time of the whole call, pauses included.
190
+
191
+ ### A call that may have spent
192
+
193
+ A call that spends gets an Idempotency-Key made once for the call, before the first attempt, and sent again on every retry. For 24 hours the API answers a key whose request succeeded with that same answer, marked replayed, and does not run the call again. A key whose request ended in an error can be sent again: the request runs again, and a charge it already made is not made a second time.
194
+
195
+ When a call that spends ends without a clear answer after its retries, its error has `may_have_spent` set to True: the API may have charged it. Call again with `idempotency_key=error.idempotency_key`. If the first request ran, you get its answer and pay nothing more; if it did not, it runs now. The `spend_once` function of the quickstart does this. A new key would be a new request, charged again.
196
+
197
+ Every POST that writes or spends sends a key, and you can pass one of your own as `idempotency_key`: 1 to 255 visible ASCII characters, with no spaces. A PATCH, PUT or DELETE takes none.
198
+
199
+ ### CONFLICT
200
+
201
+ A `CONFLICT` is never retried. When it says that the revision you sent is no longer the current one, read the composition or the timeline again and decide from what it holds now. A change you sent as a new call, after an earlier call of the same change ended in an error, may already be in it: to send a change again, pass the `idempotency_key` of that error, and if the first one was written you get its answer.
202
+
203
+ ### NOT_READY after accept_clip
204
+
205
+ When the project follows the face of the speaker, `accept_clip` starts a framing job on the new composition, and `composition["framing"]["jobId"]` is that job. Until it is done, `create_export` answers `NOT_READY`; the SDK waits the seconds of `Retry-After` and sends it again with the same key, up to `max_retries` times. The quickstart waits for the framing job with `wait_for_job` first, so the export starts at once.
206
+
207
+ ## Waiting for work
208
+
209
+ `analyze_project`, `create_export`, the tools and the Editor Assistant start work and answer at once with the job, the export, the tool run or the turn. `gv.wait(answer)` takes that answer and reads the work again until it is done, failed or cancelled. Each read waits on the server for up to 25 seconds, so a wait sends few requests.
210
+
211
+ - `timeout` is 3600 seconds. A longer wait raises `WAIT_TIMEOUT`, with the last object read. Ending a wait never stops the work: waiting again picks it up.
212
+ - Work that failed or was cancelled raises `GptVideoJobError`. With `throw_on_failure=False` the wait returns the object instead.
213
+ - `on_progress` is told of every read, and `cancel`, a `threading.Event`, stops the wait.
214
+ - `wait_for_job`, `wait_for_export`, `wait_for_tool_run` and `wait_for_assistant_turn` take an id or an object.
215
+
216
+ ## Lists
217
+
218
+ The nine list methods answer one page at a time. `gv.paginate` follows `nextCursor` for you, and asks for a page only when the items before it are used up:
219
+
220
+ - `gv.paginate("list_projects", limit=100)` goes through every project.
221
+ - `gv.paginate("list_clips", projectId=project_id, status="proposed")` goes through the clips of a project, in the order the analysis ranked them.
222
+
223
+ ## Uploads and downloads
224
+
225
+ `upload_file` takes the path of a file, bytes, or a file opened in binary mode. A file that cannot seek needs `size_bytes`. The SDK reads each part right before it sends it and never holds the whole file in memory.
226
+
227
+ - The API takes MP4, MOV, MKV, WebM, AVI and MPEG-TS files up to 8 GB, and chooses the size of the parts.
228
+ - Four parts travel at once (`concurrency`, up to 16), and each part is retried on its own.
229
+ - Each part has its own timeout: 60 seconds, or longer for a part that needs it at 100 KB a second, up to 10 minutes. After a part times out, the rest go one at a time.
230
+ - If the upload fails, the SDK cancels it.
231
+ - `on_progress` is told after every part the API received.
232
+
233
+ An upload takes as long as your connection needs to send the file. 1 GB is 8,000 megabits, so on a 10 Mbit/s upload link it takes at least 800 seconds, about 13 minutes.
234
+
235
+ `download(url, destination)` writes the file behind a signed link, such as the `downloadUrl` of a finished export, to a path or to a file opened in binary mode, and returns how many bytes it wrote. It never sends your API key, accepts only `https` links, and retries a failure that comes before the answer, as it does for a read. When a download fails, a path is left as it was. A link expires after a while: read the export again with `get_export` for a new one.
236
+
237
+ ## Webhooks
238
+
239
+ GPT-Video can call your server when a job or an export ends. You add the address on the Developers page of the app, which shows the signing secret of the webhook. It starts with `whsec_`. [Webhooks](https://www.gpt-video.com/developers/webhooks) describes the events.
240
+
241
+ `gpt_video.webhooks` checks a delivery on the raw request body, before you parse it as JSON:
242
+
243
+ - `verify_webhook(headers=..., body=..., secret=...)` checks it and returns the `id` and `timestamp` of the delivery.
244
+ - `construct_event(headers=..., body=..., secret=...)` checks it and returns the event.
245
+ - `Webhook(secret).verify(body, headers)` has the form of the Standard Webhooks libraries, so a receiver that uses one of them changes only its import.
246
+
247
+ A delivery that does not check out raises `WebhookVerificationError`, whose `reason` says why. A timestamp more than 300 seconds from your clock is refused (`tolerance_sec`).
248
+
249
+ ## Options
250
+
251
+ `GptVideo(...)` checks every option before anything is sent.
252
+
253
+ | Option | Default | What it does |
254
+ |---|---|---|
255
+ | `api_key` | `GPT_VIDEO_API_KEY` | The API key. It starts with `gvk_`. |
256
+ | `base_url` | `https://api.gpt-video.com/v1` | The address of the API, with or without `/v1` |
257
+ | `max_retries` | 2 | Failures retried after the first attempt |
258
+ | `timeout` | 60 | The seconds of one attempt |
259
+ | `deadline` | 180 | The seconds of a whole call, pauses included |
260
+ | `on_response` | None | Told of every answer, with its status and rate limit |
261
+ | `on_retry` | None | Told before every pause, with the code and the delay |
262
+ | `user_agent_suffix` | None | Added after `gpt-video-python/<version>` in the User-Agent |
263
+
264
+ The SDK sends requests with `urllib`. For an address that is not this machine it uses the proxy the environment sets, such as HTTPS_PROXY, read once when the client is made. It never follows a redirect with your API key.
265
+
266
+ ## From MCP to the SDK
267
+
268
+ If you used GPT-Video through its MCP server, most tools there have a method here that does the same work, with the same name, such as `get_credits`. These tools have another name here:
269
+
270
+ | MCP tool | SDK method | How they relate |
271
+ |---|---|---|
272
+ | `create_auto_clip_project` | `create_project` | The same operation |
273
+ | `configure_project` | `update_project` | Close work with other arguments |
274
+ | `find_clips` | `analyze_project` | Close work with other arguments |
275
+ | `export_clip` | `accept_clip` | Close work with other arguments |
276
+ | `export_clip` | `create_export` | Close work with other arguments |
277
+ | `wait_for_job` | `get_job` | The same operation |
278
+
279
+ For close work, read the page of the method before you switch.
280
+
281
+ The first tools of the MCP server were written by hand before the API, and five of them share a name with a method here but have arguments and answers of their own: `list_projects`, `create_project`, `get_project`, `list_clips` and `get_job`. On MCP, `create_project` makes an empty project for a video the user uploads in the app; `create_project` here takes the video in `sourceAssetId`. Read the page of the method for these too.
282
+
283
+ On MCP a tool that spends needs `confirm: true`. The SDK has no such argument: calling the method is the decision.
284
+
285
+ ## Versions
286
+
287
+ Version 1.0.0 of the SDK calls version 1 of the API. The answers come back as the API sends them, so a field the API adds reaches your code before the SDK knows it. [CHANGELOG.md](CHANGELOG.md) lists the changes.
288
+
289
+ Questions: hello@gpt-video.com. The API documentation: https://www.gpt-video.com/developers.
290
+
291
+ MIT license.
@@ -0,0 +1,265 @@
1
+ # gpt-video
2
+
3
+ The official Python SDK for the GPT-Video API.
4
+
5
+ GPT-Video turns a long video into short vertical clips with captions and face framing. Your program uploads a video, GPT-Video finds the moments that work as clips, and your program exports the ones it keeps as MP4 files.
6
+
7
+ - One method for each of the 67 operations of the API, with the names of the [API reference](https://www.gpt-video.com/developers/api): `analyze_project`.
8
+ - Retries that never pay twice: a call that spends sends the same Idempotency-Key on every attempt.
9
+ - Helpers for the work that takes more than one request: `upload_file`, `wait`, `paginate`, `download`, and webhook verification.
10
+ - Python 3.10 or later, the standard library only, and typed (`py.typed`).
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pip install gpt-video
16
+ ```
17
+
18
+ The package is `gpt_video`.
19
+
20
+ ## Your first clip
21
+
22
+ 1. Create a GPT-Video account and confirm your email address.
23
+ 2. Open the Developers page of the app, https://www.gpt-video.com/app/developers, and create an API key with the scopes `clips:read`, `clips:write` and `spend`. The key is shown once, so copy it then.
24
+ 3. Put the key in the environment, where the SDK reads it, and run the quickstart on a video of yours:
25
+
26
+ ```sh
27
+ export GPT_VIDEO_API_KEY=gvk_...
28
+ python quickstart.py talk.mp4
29
+ ```
30
+
31
+ The program below is `examples/quickstart.py`. It checks the key, uploads the video, shows the price, analyzes the video, accepts the first clip, exports it and saves it as `clip.mp4`. Our test suite runs this file, unchanged, against a test server of the API.
32
+
33
+ ```python
34
+ """GPT-Video quickstart: from a video file to a vertical clip on your disk.
35
+
36
+ pip install gpt-video
37
+ export GPT_VIDEO_API_KEY=gvk_...
38
+ python quickstart.py talk.mp4
39
+
40
+ Steps 1 to 4 use no credits. Step 5 spends credits: one per started minute of the video.
41
+ Step 7 uses one export of your daily allowance, and one API bonus credit while the bonus pays.
42
+ """
43
+
44
+ import os
45
+ import sys
46
+
47
+ from gpt_video import GptVideo, GptVideoError
48
+
49
+
50
+ def spend_once(call):
51
+ """A call that spends and ends without a clear answer (the network failed, or the time ran out) may have been
52
+ charged. Calling again with the same idempotency_key returns the first answer and never pays twice."""
53
+ try:
54
+ return call(None)
55
+ except GptVideoError as error:
56
+ if not error.may_have_spent:
57
+ raise
58
+ print(f"No clear answer ({error.code}). Asking again with the same Idempotency-Key.")
59
+ return call(error.idempotency_key)
60
+
61
+
62
+ def main(path):
63
+ gv = GptVideo() # reads GPT_VIDEO_API_KEY
64
+
65
+ # 1. Check the key.
66
+ me = gv.get_me()
67
+ print(f"Key {me['key']['id']} works. Scopes: {', '.join(me['key']['scopes'])}. Plan: {me['account']['plan']}.")
68
+
69
+ # 2. Upload the video. The SDK sends it in parts and reads one part at a time from the disk.
70
+ asset = gv.upload_file(path, on_progress=lambda p: print(f"Uploaded {round(100 * p.uploaded_bytes / p.total_bytes)}%"))["asset"]
71
+ print(f"Video {asset['id']} is ready.")
72
+
73
+ # 3. Create an Auto Clip project. Auto Clip has no default settings: the analysis starts only when
74
+ # missingSettings is empty, so choose every one of them.
75
+ project = gv.create_project(
76
+ sourceAssetId=asset["id"],
77
+ title=os.path.basename(path),
78
+ settings={
79
+ "language": "auto",
80
+ "clipCount": 3,
81
+ "minDurationSec": 15,
82
+ "maxDurationSec": 60,
83
+ "objective": "viral",
84
+ "captionStyle": "word-pop",
85
+ "reframe": "face",
86
+ },
87
+ )["project"]
88
+ if project["missingSettings"]:
89
+ raise SystemExit(f"Settings still to choose: {', '.join(project['missingSettings'])}")
90
+
91
+ # 4. Ask the price of the analysis.
92
+ estimate = gv.estimate_analysis(projectId=project["id"])["estimate"]
93
+ print(f"The analysis costs {estimate['creditsToCharge']} credits.")
94
+ if not estimate["enough"]:
95
+ raise SystemExit("The account cannot pay for it. See https://www.gpt-video.com/developers/pricing")
96
+
97
+ # 5. Analyze the video, and wait until its clips are found.
98
+ analysis = spend_once(lambda key: gv.analyze_project(projectId=project["id"], idempotency_key=key))
99
+ print(f"Analysis started ({analysis['creditsCharged']} credits charged). Waiting for job {analysis['job']['id']}.")
100
+ gv.wait(analysis)
101
+
102
+ # 6. Read the clips in the order the analysis ranked them, and accept the first one. Accepting makes a
103
+ # composition, whose framing follows the face of the speaker; it can be exported once its framing job is done.
104
+ clips = list(gv.paginate("list_clips", projectId=project["id"], status="proposed"))
105
+ for clip in clips:
106
+ print(f"{clip['rank'] + 1}. {clip['title']} ({clip['startSec']}s to {clip['endSec']}s, score {clip['score']})")
107
+ if not clips:
108
+ raise SystemExit("The analysis found no clips in this video.")
109
+ composition = gv.accept_clip(clipId=clips[0]["id"])["composition"]
110
+ if composition["framing"]["jobId"]:
111
+ gv.wait_for_job(composition["framing"]["jobId"])
112
+
113
+ # 7. Export the composition to an MP4 file, wait for it, and download it. The download link is its own
114
+ # credential: the SDK never sends your API key to it.
115
+ exported = spend_once(lambda key: gv.create_export(compositionId=composition["id"], idempotency_key=key))
116
+ done = gv.wait(exported)
117
+ size = gv.download(done["downloadUrl"], "clip.mp4")
118
+ print(f"Saved clip.mp4 ({size} bytes).")
119
+
120
+
121
+ if __name__ == "__main__":
122
+ if len(sys.argv) != 2:
123
+ raise SystemExit("Usage: python quickstart.py <video file>")
124
+ main(sys.argv[1])
125
+ ```
126
+
127
+ ## Arguments
128
+
129
+ The fields of a call keep the names of the API, in camelCase: `projectId`, `sourceAssetId`. Pass them as keyword arguments, `gv.analyze_project(projectId="prj_...")`, or as one dict, `gv.analyze_project({"projectId": "prj_..."})`, but not both in the same call. The options of the SDK are keyword arguments in snake_case: `idempotency_key`, `timeout`, `deadline`, `max_retries`, `cancel`.
130
+
131
+ Every answer is the JSON body of the API as a dict, as the API sent it.
132
+
133
+ ## What uses credits
134
+
135
+ - `get_me`, `upload_file`, `create_project`, `estimate_analysis`, `list_clips`, `accept_clip` and every other read use no credits. An upload counts toward the storage of the account.
136
+ - `analyze_project` charges one credit per started minute of the source video when the analysis starts. The credits are not returned if the analysis fails, and analyzing again is charged again.
137
+ - `create_export` uses one export of the daily allowance, which starts again at 00:00 UTC. While the API bonus pays, it also uses one bonus credit.
138
+
139
+ Every method links to its page of the API reference, which says what it costs. [API pricing](https://www.gpt-video.com/developers/pricing) has the API bonus, the plans and the daily limits.
140
+
141
+ ## When a call fails
142
+
143
+ Every error the SDK raises is a `GptVideoError`, with these fields:
144
+
145
+ | Field | What it says |
146
+ |---|---|
147
+ | `code` | The code of the API, such as `NOT_READY`, or one of the SDK's: `CONNECTION_FAILED`, `TIMEOUT`, `ABORTED`, `BAD_RESPONSE`, `INVALID_ARGUMENT`, `JOB_FAILED`, `JOB_CANCELLED`, `WAIT_TIMEOUT`, `WEBHOOK_VERIFICATION_FAILED` |
148
+ | `retryable` | Whether the same call, sent again with the same `idempotency_key`, can succeed |
149
+ | `operation` | The operation of the call, such as `analyze_project` |
150
+ | `idempotency_key` | The Idempotency-Key the call sent, or None for a call that sends none |
151
+ | `may_have_spent` | True when a call that spends ended without a clear answer |
152
+
153
+ `GptVideoApiError` is an error the API answered, with `status`, `request_id`, `hint`, `details` and `retry_after_sec`. `GptVideoNetworkError` is a request that got no usable answer. `GptVideoJobError` is work that `wait` was following and that failed, was cancelled or ran past the time of the wait: `handle` is the job, the export, the tool run or the turn as the last read gave it, and `error_message` says why the work failed. The message of an error has the form `NOT_READY (409): message of the API Hint: what to do [request req_...]`. The API key never appears in an error.
154
+
155
+ When Python cannot check the TLS certificate of the API, the error is `CONNECTION_FAILED` and says so. On macOS, run `Install Certificates.command` from the folder of your Python version.
156
+
157
+ ### Retries
158
+
159
+ The SDK sends a call again after a failure only when that is safe: a read, a PATCH, PUT or DELETE, which has the same effect when sent twice, or a POST that carries an Idempotency-Key. It retries `CONNECTION_FAILED`, `TIMEOUT`, `NOT_READY`, `INTERNAL`, `UPSTREAM_FAILED`, `UNAVAILABLE` (unless the API is switched off) and a `BAD_RESPONSE` with a 5xx status, up to `max_retries` times (2). It waits the seconds of `Retry-After` when the API sends it, and otherwise a growing pause with jitter. A `Retry-After` longer than 60 seconds, or longer than the time the call has left, ends the call, and the error carries it in `retry_after_sec`. `RATE_LIMITED` is retried for every call, because the API refuses it before running anything. `on_retry` is told before every pause.
160
+
161
+ `IDEMPOTENCY_IN_PROGRESS` means the first request with the same key is still running on the server. The SDK waits and asks again with the same key until it gets the answer, and this does not count toward `max_retries`.
162
+
163
+ `timeout` (60 seconds) is the time of one attempt, and `deadline` (180 seconds) the time of the whole call, pauses included.
164
+
165
+ ### A call that may have spent
166
+
167
+ A call that spends gets an Idempotency-Key made once for the call, before the first attempt, and sent again on every retry. For 24 hours the API answers a key whose request succeeded with that same answer, marked replayed, and does not run the call again. A key whose request ended in an error can be sent again: the request runs again, and a charge it already made is not made a second time.
168
+
169
+ When a call that spends ends without a clear answer after its retries, its error has `may_have_spent` set to True: the API may have charged it. Call again with `idempotency_key=error.idempotency_key`. If the first request ran, you get its answer and pay nothing more; if it did not, it runs now. The `spend_once` function of the quickstart does this. A new key would be a new request, charged again.
170
+
171
+ Every POST that writes or spends sends a key, and you can pass one of your own as `idempotency_key`: 1 to 255 visible ASCII characters, with no spaces. A PATCH, PUT or DELETE takes none.
172
+
173
+ ### CONFLICT
174
+
175
+ A `CONFLICT` is never retried. When it says that the revision you sent is no longer the current one, read the composition or the timeline again and decide from what it holds now. A change you sent as a new call, after an earlier call of the same change ended in an error, may already be in it: to send a change again, pass the `idempotency_key` of that error, and if the first one was written you get its answer.
176
+
177
+ ### NOT_READY after accept_clip
178
+
179
+ When the project follows the face of the speaker, `accept_clip` starts a framing job on the new composition, and `composition["framing"]["jobId"]` is that job. Until it is done, `create_export` answers `NOT_READY`; the SDK waits the seconds of `Retry-After` and sends it again with the same key, up to `max_retries` times. The quickstart waits for the framing job with `wait_for_job` first, so the export starts at once.
180
+
181
+ ## Waiting for work
182
+
183
+ `analyze_project`, `create_export`, the tools and the Editor Assistant start work and answer at once with the job, the export, the tool run or the turn. `gv.wait(answer)` takes that answer and reads the work again until it is done, failed or cancelled. Each read waits on the server for up to 25 seconds, so a wait sends few requests.
184
+
185
+ - `timeout` is 3600 seconds. A longer wait raises `WAIT_TIMEOUT`, with the last object read. Ending a wait never stops the work: waiting again picks it up.
186
+ - Work that failed or was cancelled raises `GptVideoJobError`. With `throw_on_failure=False` the wait returns the object instead.
187
+ - `on_progress` is told of every read, and `cancel`, a `threading.Event`, stops the wait.
188
+ - `wait_for_job`, `wait_for_export`, `wait_for_tool_run` and `wait_for_assistant_turn` take an id or an object.
189
+
190
+ ## Lists
191
+
192
+ The nine list methods answer one page at a time. `gv.paginate` follows `nextCursor` for you, and asks for a page only when the items before it are used up:
193
+
194
+ - `gv.paginate("list_projects", limit=100)` goes through every project.
195
+ - `gv.paginate("list_clips", projectId=project_id, status="proposed")` goes through the clips of a project, in the order the analysis ranked them.
196
+
197
+ ## Uploads and downloads
198
+
199
+ `upload_file` takes the path of a file, bytes, or a file opened in binary mode. A file that cannot seek needs `size_bytes`. The SDK reads each part right before it sends it and never holds the whole file in memory.
200
+
201
+ - The API takes MP4, MOV, MKV, WebM, AVI and MPEG-TS files up to 8 GB, and chooses the size of the parts.
202
+ - Four parts travel at once (`concurrency`, up to 16), and each part is retried on its own.
203
+ - Each part has its own timeout: 60 seconds, or longer for a part that needs it at 100 KB a second, up to 10 minutes. After a part times out, the rest go one at a time.
204
+ - If the upload fails, the SDK cancels it.
205
+ - `on_progress` is told after every part the API received.
206
+
207
+ An upload takes as long as your connection needs to send the file. 1 GB is 8,000 megabits, so on a 10 Mbit/s upload link it takes at least 800 seconds, about 13 minutes.
208
+
209
+ `download(url, destination)` writes the file behind a signed link, such as the `downloadUrl` of a finished export, to a path or to a file opened in binary mode, and returns how many bytes it wrote. It never sends your API key, accepts only `https` links, and retries a failure that comes before the answer, as it does for a read. When a download fails, a path is left as it was. A link expires after a while: read the export again with `get_export` for a new one.
210
+
211
+ ## Webhooks
212
+
213
+ GPT-Video can call your server when a job or an export ends. You add the address on the Developers page of the app, which shows the signing secret of the webhook. It starts with `whsec_`. [Webhooks](https://www.gpt-video.com/developers/webhooks) describes the events.
214
+
215
+ `gpt_video.webhooks` checks a delivery on the raw request body, before you parse it as JSON:
216
+
217
+ - `verify_webhook(headers=..., body=..., secret=...)` checks it and returns the `id` and `timestamp` of the delivery.
218
+ - `construct_event(headers=..., body=..., secret=...)` checks it and returns the event.
219
+ - `Webhook(secret).verify(body, headers)` has the form of the Standard Webhooks libraries, so a receiver that uses one of them changes only its import.
220
+
221
+ A delivery that does not check out raises `WebhookVerificationError`, whose `reason` says why. A timestamp more than 300 seconds from your clock is refused (`tolerance_sec`).
222
+
223
+ ## Options
224
+
225
+ `GptVideo(...)` checks every option before anything is sent.
226
+
227
+ | Option | Default | What it does |
228
+ |---|---|---|
229
+ | `api_key` | `GPT_VIDEO_API_KEY` | The API key. It starts with `gvk_`. |
230
+ | `base_url` | `https://api.gpt-video.com/v1` | The address of the API, with or without `/v1` |
231
+ | `max_retries` | 2 | Failures retried after the first attempt |
232
+ | `timeout` | 60 | The seconds of one attempt |
233
+ | `deadline` | 180 | The seconds of a whole call, pauses included |
234
+ | `on_response` | None | Told of every answer, with its status and rate limit |
235
+ | `on_retry` | None | Told before every pause, with the code and the delay |
236
+ | `user_agent_suffix` | None | Added after `gpt-video-python/<version>` in the User-Agent |
237
+
238
+ The SDK sends requests with `urllib`. For an address that is not this machine it uses the proxy the environment sets, such as HTTPS_PROXY, read once when the client is made. It never follows a redirect with your API key.
239
+
240
+ ## From MCP to the SDK
241
+
242
+ If you used GPT-Video through its MCP server, most tools there have a method here that does the same work, with the same name, such as `get_credits`. These tools have another name here:
243
+
244
+ | MCP tool | SDK method | How they relate |
245
+ |---|---|---|
246
+ | `create_auto_clip_project` | `create_project` | The same operation |
247
+ | `configure_project` | `update_project` | Close work with other arguments |
248
+ | `find_clips` | `analyze_project` | Close work with other arguments |
249
+ | `export_clip` | `accept_clip` | Close work with other arguments |
250
+ | `export_clip` | `create_export` | Close work with other arguments |
251
+ | `wait_for_job` | `get_job` | The same operation |
252
+
253
+ For close work, read the page of the method before you switch.
254
+
255
+ The first tools of the MCP server were written by hand before the API, and five of them share a name with a method here but have arguments and answers of their own: `list_projects`, `create_project`, `get_project`, `list_clips` and `get_job`. On MCP, `create_project` makes an empty project for a video the user uploads in the app; `create_project` here takes the video in `sourceAssetId`. Read the page of the method for these too.
256
+
257
+ On MCP a tool that spends needs `confirm: true`. The SDK has no such argument: calling the method is the decision.
258
+
259
+ ## Versions
260
+
261
+ Version 1.0.0 of the SDK calls version 1 of the API. The answers come back as the API sends them, so a field the API adds reaches your code before the SDK knows it. [CHANGELOG.md](CHANGELOG.md) lists the changes.
262
+
263
+ Questions: hello@gpt-video.com. The API documentation: https://www.gpt-video.com/developers.
264
+
265
+ MIT license.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gpt-video"
7
+ dynamic = ["version"]
8
+ description = "Python client for the GPT-Video API: upload a video, find its clips, edit and export them."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "GPT-Video", email = "hello@gpt-video.com" }]
13
+ keywords = ["gpt-video", "video", "clips", "api", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Multimedia :: Video",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = []
29
+
30
+ [tool.hatch.version]
31
+ path = "src/gpt_video/_version.py"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/gpt_video"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["src/gpt_video", "README.md", "LICENSE"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://www.gpt-video.com/developers"
41
+ Repository = "https://github.com/gptvideo/gpt-video-sdks"
42
+ Issues = "https://github.com/gptvideo/gpt-video-sdks/issues"
@@ -0,0 +1,33 @@
1
+ """The GPT-Video SDK for Python: one method for each operation of the GPT-Video API, and the helpers around them.
2
+
3
+ from gpt_video import GptVideo
4
+
5
+ client = GptVideo() # reads GPT_VIDEO_API_KEY
6
+ me = client.get_me()
7
+
8
+ A call that spends sends an Idempotency-Key, made once for the call and sent again on every retry, so a retry never
9
+ pays twice. Webhook verification is in gpt_video.webhooks, and the types of the API are in gpt_video.types.
10
+ """
11
+
12
+ from ._retry import RateLimit
13
+ from ._transport import ResponseEvent, RetryEvent
14
+ from ._version import VERSION
15
+ from .client import GptVideo
16
+ from .errors import GptVideoApiError, GptVideoError, GptVideoJobError, GptVideoNetworkError
17
+ from .uploads import UploadProgress
18
+
19
+ __version__ = VERSION
20
+
21
+ __all__ = [
22
+ "GptVideo",
23
+ "GptVideoApiError",
24
+ "GptVideoError",
25
+ "GptVideoJobError",
26
+ "GptVideoNetworkError",
27
+ "RateLimit",
28
+ "ResponseEvent",
29
+ "RetryEvent",
30
+ "UploadProgress",
31
+ "VERSION",
32
+ "__version__",
33
+ ]