clipwise 0.1.0b1__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,94 @@
1
+ # Ignore compiled Python files
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ # Ignore the virtual environment
6
+ venv/
7
+ .venv/
8
+ venv-py310/
9
+ clipwise_env/
10
+ lib/
11
+ venv_py310
12
+
13
+ # Ignore local development settings
14
+ *.env
15
+
16
+ # Ignore static files
17
+ staticfiles/
18
+
19
+ # Ignore media files
20
+ media/
21
+
22
+ # Ignore database file
23
+ db.sqlite3
24
+
25
+ # Ignore logs and other generated files
26
+ *.log
27
+ *.pot
28
+ *.pyc
29
+ *.swp
30
+ *.swo
31
+
32
+ # Ignore secret keys and sensitive information
33
+ *.secret_key
34
+ *.env
35
+
36
+ # Ignore local database settings
37
+ *.sqlite3
38
+
39
+ # Ignore IDE specific files
40
+ .vscode/
41
+ .idea/
42
+
43
+ # Ignore OS generated files
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # Ignore node modules
48
+ node_modules/
49
+ package-lock.json
50
+ cache/*
51
+ local_scripts/*
52
+ /dump.rdb
53
+ API Collection.postman_collection.json
54
+ example_request.txt
55
+ jsconfig.json
56
+ kling_cache.json
57
+ postman_collection.json
58
+ dump.rdb
59
+ kling_cache.json
60
+ jsconfig.json
61
+ .firebaserc
62
+ memory-bank/
63
+ delete_linear_issues.js
64
+ .clinerules
65
+ local-tests
66
+ mlartifacts
67
+ mlruns/
68
+ .ruff_cache/
69
+ .pytest_cache/
70
+ .parcel-cache/
71
+ .claude/
72
+ final_plan.md
73
+ centrifugo_config.json
74
+ centrifugo
75
+ restart_dev.sh
76
+ stop_dev.sh
77
+ start_dev.sh
78
+ uv.lock
79
+ mlflow.pid
80
+
81
+ # Local Codex worktree bootstrap
82
+ .githooks/
83
+ scripts/setup_worktree_from_main.sh
84
+
85
+ .env.precommit
86
+ .dspy_cache/
87
+ *.un~
88
+ .colima/
89
+ .docker/
90
+ .ipython/
91
+ .python_history
92
+ .ssh/
93
+ .viminfo
94
+ Library/
@@ -0,0 +1,54 @@
1
+ # Clipwise CLI
2
+
3
+ Global environment:
4
+
5
+ ```bash
6
+ export CLIPWISE_API_KEY="cw_live_..."
7
+ export CLIPWISE_BASE_URL="https://app.clipwise.ai"
8
+ ```
9
+
10
+ Commands:
11
+
12
+ ```bash
13
+ clipwise auth set-key cw_live_...
14
+ clipwise auth show
15
+ clipwise auth clear
16
+
17
+ clipwise formats list --kind all --platform all --json
18
+ clipwise formats show ugc_video --json
19
+
20
+ clipwise resources types --json
21
+ clipwise resources list avatars --page 1 --limit 20 --json
22
+ clipwise resources list poses --video-format ugc_product_background --json
23
+ clipwise resources list background_music --json
24
+ clipwise resources list overlays --type overlay --json
25
+ clipwise resources upload product_images ./product.png --product-type skincare --json
26
+ clipwise resources upload reference_ads ./ad.mp4 --type video --json
27
+ clipwise resources upload background_music ./track.mp3 --json
28
+ clipwise resources upload overlays ./overlay.webm --type underlay --json
29
+ clipwise resources generate avatars --prompt "Female presenter in a studio" --video-format ugc_video --json
30
+ clipwise resources generate background_images --prompt "Luxury bathroom counter" --video-format ugc_product_background --json
31
+
32
+ clipwise uploads create ./input.mp4 --purpose video --json
33
+ clipwise uploads list --page 1 --limit 20 --json
34
+
35
+ clipwise products list --page 1 --limit 20 --json
36
+ clipwise products create --name "Serum" --image-id UPLOAD_ID --product-type skincare --json
37
+
38
+ clipwise voices list --json
39
+
40
+ clipwise generate --format smart --prompt "..." --json
41
+ clipwise generate --format video_to_text --video ./input.mp4 --json
42
+ clipwise generate --format inventory_shoot --mode product_shoot --product-image ./product.png --reference-image POSE_ID --variations 4 --json
43
+ clipwise status gen_... --json
44
+ clipwise wait gen_... --timeout 1800 --poll-interval 5 --json
45
+ clipwise cancel gen_... --json
46
+ ```
47
+
48
+ Local file arguments are uploaded first when possible, then replaced with the
49
+ uploaded resource id in the generation request.
50
+
51
+ Standalone resource generation is available for `avatars` and
52
+ `background_images`. Background music generation remains tied to an existing
53
+ video job in the Clipwise workflow; use `generate` and let the backend auto-pick
54
+ or create music when that format supports it.
@@ -0,0 +1,2 @@
1
+ MIT
2
+
@@ -0,0 +1,35 @@
1
+ # LLM Usage Guide
2
+
3
+ 1. Run `clipwise formats list --json` before selecting a format unless the user
4
+ already provided an exact format slug or template name.
5
+ 2. Use `description`, `what_it_does`, `required_inputs`, `optional_inputs`,
6
+ and `resource_requirements` from the format response.
7
+ 3. For resource-backed inputs, list or upload the resource first:
8
+ `clipwise resources list RESOURCE_TYPE --page 1 --limit 20 --json` or
9
+ `clipwise resources upload RESOURCE_TYPE FILE --json`.
10
+ 4. For standalone generated assets, use
11
+ `clipwise resources generate avatars --prompt "..." --json` or
12
+ `clipwise resources generate background_images --prompt "..." --json`.
13
+ 5. Start video jobs with `clipwise generate ... --json`.
14
+ 6. Poll with `clipwise status JOB_ID --json` or `clipwise wait JOB_ID --json`.
15
+ 7. Do not call Clipwise frontend endpoints. This CLI uses only `/api/sdk/v1/*`.
16
+
17
+ Important resource mappings:
18
+
19
+ - `inventory_shoot --mode product_shoot` maps to `ugc_product_background`.
20
+ - `inventory_shoot --mode model_shoot` maps to `ugc_model_shoot`.
21
+ - `--reference-image` for inventory shoots maps to the backend `pose` field.
22
+ - Inventory shoot references can be selected from
23
+ `clipwise resources list poses --video-format ugc_product_background --page 1 --limit 20`, passed
24
+ as a pose id, or uploaded from a local file with `--reference-image ./pose.png`.
25
+ - Use `--page` and `--limit` for resource, upload, and product list commands;
26
+ SDK list responses include a `pagination` object when the backend can page.
27
+ - Product images can be uploaded with `resources upload product_images FILE`.
28
+ - Reference ad videos can be uploaded with `resources upload reference_ads FILE --type video`.
29
+ - Background music files can be uploaded with
30
+ `resources upload background_music FILE`.
31
+ - Overlay and underlay videos can be listed with `resources list overlays` and
32
+ uploaded with `resources upload overlays FILE --type overlay|underlay`.
33
+ - Standalone resource generation currently supports `avatars` and
34
+ `background_images`; background music generation is workflow-scoped and should
35
+ be left to the selected video format.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: clipwise
3
+ Version: 0.1.0b1
4
+ Summary: Python SDK and CLI for Clipwise generation APIs
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: typing-extensions>=4.8
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Clipwise Python SDK and CLI
12
+
13
+ Install in development:
14
+
15
+ ```bash
16
+ cd backend_clipwise/clipwise_cli
17
+ pip install -e .
18
+ ```
19
+
20
+ Configure:
21
+
22
+ ```bash
23
+ export CLIPWISE_API_KEY="cw_live_..."
24
+ export CLIPWISE_BASE_URL="https://app.clipwise.ai"
25
+ ```
26
+
27
+ Python:
28
+
29
+ ```python
30
+ from clipwise import Clipwise
31
+
32
+ client = Clipwise()
33
+ formats = client.formats.list(kind="all", platform="all")
34
+ job = client.generations.create(format="smart", prompt="Create a founder launch video")
35
+ result = client.generations.wait(job["id"])
36
+ print(result["generation"]["outputs"])
37
+ ```
38
+
39
+ CLI:
40
+
41
+ ```bash
42
+ clipwise formats list --json
43
+ clipwise resources list poses --video-format ugc_product_background --json
44
+ clipwise resources upload background_music ./track.mp3 --json
45
+ clipwise resources upload overlays ./overlay.webm --type underlay --json
46
+ clipwise resources generate avatars --prompt "Friendly skincare presenter" --video-format ugc_video --json
47
+ clipwise resources generate background_images --prompt "Luxury bathroom counter" --video-format ugc_product_background --json
48
+ clipwise generate --format smart --prompt "Create a 30 second founder launch video" --json
49
+ clipwise wait gen_abc123 --json
50
+ ```
51
+
52
+ The package only talks to `/api/sdk/v1/*` endpoints. It does not import Django
53
+ or use internal frontend endpoints.
@@ -0,0 +1,43 @@
1
+ # Clipwise Python SDK and CLI
2
+
3
+ Install in development:
4
+
5
+ ```bash
6
+ cd backend_clipwise/clipwise_cli
7
+ pip install -e .
8
+ ```
9
+
10
+ Configure:
11
+
12
+ ```bash
13
+ export CLIPWISE_API_KEY="cw_live_..."
14
+ export CLIPWISE_BASE_URL="https://app.clipwise.ai"
15
+ ```
16
+
17
+ Python:
18
+
19
+ ```python
20
+ from clipwise import Clipwise
21
+
22
+ client = Clipwise()
23
+ formats = client.formats.list(kind="all", platform="all")
24
+ job = client.generations.create(format="smart", prompt="Create a founder launch video")
25
+ result = client.generations.wait(job["id"])
26
+ print(result["generation"]["outputs"])
27
+ ```
28
+
29
+ CLI:
30
+
31
+ ```bash
32
+ clipwise formats list --json
33
+ clipwise resources list poses --video-format ugc_product_background --json
34
+ clipwise resources upload background_music ./track.mp3 --json
35
+ clipwise resources upload overlays ./overlay.webm --type underlay --json
36
+ clipwise resources generate avatars --prompt "Friendly skincare presenter" --video-format ugc_video --json
37
+ clipwise resources generate background_images --prompt "Luxury bathroom counter" --video-format ugc_product_background --json
38
+ clipwise generate --format smart --prompt "Create a 30 second founder launch video" --json
39
+ clipwise wait gen_abc123 --json
40
+ ```
41
+
42
+ The package only talks to `/api/sdk/v1/*` endpoints. It does not import Django
43
+ or use internal frontend endpoints.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "clipwise"
3
+ version = "0.1.0b1"
4
+ description = "Python SDK and CLI for Clipwise generation APIs"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ dependencies = [
8
+ "httpx>=0.27",
9
+ "typing-extensions>=4.8",
10
+ ]
11
+
12
+ [project.scripts]
13
+ clipwise = "clipwise.cli:main"
14
+
15
+ [build-system]
16
+ requires = ["hatchling"]
17
+ build-backend = "hatchling.build"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/clipwise"]
@@ -0,0 +1,5 @@
1
+ from .client import Clipwise
2
+ from .errors import ClipwiseError
3
+
4
+ __all__ = ["Clipwise", "ClipwiseError"]
5
+
@@ -0,0 +1,373 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+
5
+ from .client import Clipwise
6
+ from .config import clear_api_key, get_api_key, write_config
7
+ from .errors import ClipwiseError
8
+
9
+
10
+ def app():
11
+ return main()
12
+
13
+
14
+ def main(argv=None):
15
+ if argv is None:
16
+ argv = sys.argv[1:]
17
+ json_flag = "--json" in argv
18
+ if json_flag:
19
+ argv = [item for item in argv if item != "--json"]
20
+ parser = build_parser()
21
+ args = parser.parse_args(argv)
22
+ args.json = bool(args.json or json_flag)
23
+ try:
24
+ payload = dispatch(args)
25
+ if payload is not None:
26
+ print_payload(payload, as_json=getattr(args, "json", False))
27
+ return 0
28
+ except ClipwiseError as exc:
29
+ error = {
30
+ "success": False,
31
+ "error": {
32
+ "code": exc.code,
33
+ "message": str(exc),
34
+ "details": exc.details,
35
+ },
36
+ "request_id": exc.request_id,
37
+ }
38
+ if getattr(args, "json", False):
39
+ print(json.dumps(error, indent=2, default=str), file=sys.stderr)
40
+ else:
41
+ print(f"{exc.code}: {exc}", file=sys.stderr)
42
+ return 2
43
+
44
+
45
+ def build_parser():
46
+ parser = argparse.ArgumentParser(prog="clipwise")
47
+ parser.add_argument("--api-key", default=None)
48
+ parser.add_argument("--base-url", default=None)
49
+ parser.add_argument("--timeout", type=float, default=30)
50
+ parser.add_argument("--json", action="store_true", help="Output JSON")
51
+ subparsers = parser.add_subparsers(dest="command", required=True)
52
+
53
+ auth = subparsers.add_parser("auth")
54
+ auth_sub = auth.add_subparsers(dest="auth_command", required=True)
55
+ set_key = auth_sub.add_parser("set-key")
56
+ set_key.add_argument("key")
57
+ auth_sub.add_parser("show")
58
+ auth_sub.add_parser("clear")
59
+
60
+ formats = subparsers.add_parser("formats")
61
+ fmt_sub = formats.add_subparsers(dest="formats_command", required=True)
62
+ fmt_list = fmt_sub.add_parser("list")
63
+ fmt_list.add_argument("--kind", default="all", choices=["all", "format", "tool"])
64
+ fmt_list.add_argument("--platform", default="all", choices=["all", "clipwise", "video_company"])
65
+ fmt_list.add_argument("--include-inactive", action="store_true")
66
+ fmt_show = fmt_sub.add_parser("show")
67
+ fmt_show.add_argument("format")
68
+
69
+ resources = subparsers.add_parser("resources")
70
+ res_sub = resources.add_subparsers(dest="resources_command", required=True)
71
+ res_sub.add_parser("types")
72
+ res_list = res_sub.add_parser("list")
73
+ res_list.add_argument("resource_type")
74
+ add_resource_filters(res_list)
75
+ add_pagination_args(res_list)
76
+ res_show = res_sub.add_parser("show")
77
+ res_show.add_argument("resource_type")
78
+ res_show.add_argument("resource_id")
79
+ res_upload = res_sub.add_parser("upload")
80
+ res_upload.add_argument("resource_type")
81
+ res_upload.add_argument("file")
82
+ add_resource_filters(res_upload)
83
+ res_generate = res_sub.add_parser("generate")
84
+ res_generate.add_argument("resource_type")
85
+ res_generate.add_argument("--prompt", required=True)
86
+ add_resource_filters(res_generate)
87
+
88
+ uploads = subparsers.add_parser("uploads")
89
+ upload_sub = uploads.add_subparsers(dest="uploads_command", required=True)
90
+ upload_create = upload_sub.add_parser("create")
91
+ upload_create.add_argument("file")
92
+ upload_create.add_argument("--purpose", default="image")
93
+ upload_create.add_argument("--type", default=None)
94
+ upload_create.add_argument("--product-type", default=None)
95
+ upload_list = upload_sub.add_parser("list")
96
+ add_pagination_args(upload_list)
97
+
98
+ products = subparsers.add_parser("products")
99
+ product_sub = products.add_subparsers(dest="products_command", required=True)
100
+ product_list = product_sub.add_parser("list")
101
+ add_pagination_args(product_list)
102
+ product_create = product_sub.add_parser("create")
103
+ product_create.add_argument("--name")
104
+ product_create.add_argument("--image-id", action="append", dest="image_ids")
105
+ product_create.add_argument("--url")
106
+ product_create.add_argument("--product-type")
107
+ product_create.add_argument("--description", default="")
108
+ product_update = product_sub.add_parser("update")
109
+ product_update.add_argument("product_id")
110
+ product_update.add_argument("--name")
111
+ product_update.add_argument("--description")
112
+ product_update.add_argument("--product-type")
113
+ product_delete = product_sub.add_parser("delete")
114
+ product_delete.add_argument("product_id")
115
+
116
+ voices = subparsers.add_parser("voices")
117
+ voice_sub = voices.add_subparsers(dest="voices_command", required=True)
118
+ voice_sub.add_parser("list")
119
+ voice_clone = voice_sub.add_parser("clone")
120
+ voice_clone.add_argument("file")
121
+ voice_clone.add_argument("--name")
122
+ voice_clone.add_argument("--language")
123
+ voice_clone.add_argument("--description")
124
+
125
+ generate = subparsers.add_parser("generate")
126
+ add_generation_args(generate)
127
+
128
+ status = subparsers.add_parser("status")
129
+ status.add_argument("generation_id")
130
+
131
+ wait = subparsers.add_parser("wait")
132
+ wait.add_argument("generation_id")
133
+ wait.add_argument("--timeout", type=int, default=1800)
134
+ wait.add_argument("--poll-interval", type=int, default=5)
135
+
136
+ cancel = subparsers.add_parser("cancel")
137
+ cancel.add_argument("generation_id")
138
+
139
+ return parser
140
+
141
+
142
+ def add_resource_filters(parser):
143
+ parser.add_argument("--collection", default=None)
144
+ parser.add_argument("--video-format", default=None)
145
+ parser.add_argument("--product-type", default=None)
146
+ parser.add_argument("--type", default=None)
147
+ parser.add_argument("--file-type", default=None)
148
+
149
+
150
+ def add_pagination_args(parser):
151
+ parser.add_argument("--page", type=int, default=None)
152
+ parser.add_argument("--limit", type=int, default=None)
153
+
154
+
155
+ def add_generation_args(parser):
156
+ parser.add_argument("--format", default="smart")
157
+ parser.add_argument("--mode")
158
+ parser.add_argument("--prompt")
159
+ parser.add_argument("--script")
160
+ parser.add_argument("--language")
161
+ parser.add_argument("--aspect-ratio")
162
+ parser.add_argument("--duration")
163
+ parser.add_argument("--image")
164
+ parser.add_argument("--images")
165
+ parser.add_argument("--video")
166
+ parser.add_argument("--audio")
167
+ parser.add_argument("--avatar")
168
+ parser.add_argument("--voice")
169
+ parser.add_argument("--background-image")
170
+ parser.add_argument("--reference-image")
171
+ parser.add_argument("--reference-video")
172
+ parser.add_argument("--product-id")
173
+ parser.add_argument("--product-image")
174
+ parser.add_argument("--product-images")
175
+ parser.add_argument("--product-type")
176
+ parser.add_argument("--product-description")
177
+ parser.add_argument("--ad-format")
178
+ parser.add_argument("--variations", type=int)
179
+ parser.add_argument("--metadata")
180
+ parser.add_argument("--idempotency-key")
181
+ parser.add_argument("--wait", action="store_true")
182
+ parser.add_argument("--poll-interval", type=int, default=5)
183
+
184
+
185
+ def client_from_args(args):
186
+ return Clipwise(api_key=args.api_key, base_url=args.base_url, timeout=args.timeout)
187
+
188
+
189
+ def dispatch(args):
190
+ if args.command == "auth":
191
+ return dispatch_auth(args)
192
+ client = client_from_args(args)
193
+ if args.command == "formats":
194
+ if args.formats_command == "list":
195
+ return client.formats.list(kind=args.kind, platform=args.platform, include_inactive=args.include_inactive)
196
+ return client.formats.retrieve(args.format)
197
+ if args.command == "resources":
198
+ return dispatch_resources(client, args)
199
+ if args.command == "uploads":
200
+ if args.uploads_command == "create":
201
+ return client.uploads.create(args.file, purpose=args.purpose, type=args.type, product_type=args.product_type)
202
+ return client.uploads.list(**pagination_filters(args))
203
+ if args.command == "products":
204
+ return dispatch_products(client, args)
205
+ if args.command == "voices":
206
+ if args.voices_command == "list":
207
+ return client.voices.list()
208
+ return client.voices.clone(args.file, name=args.name, language=args.language, description=args.description)
209
+ if args.command == "generate":
210
+ payload = build_generation_payload(args)
211
+ created = client.generations.create(idempotency_key=args.idempotency_key, **payload)
212
+ if args.wait:
213
+ return client.generations.wait(created["id"], interval=args.poll_interval)
214
+ return created
215
+ if args.command == "status":
216
+ return client.generations.retrieve(args.generation_id)
217
+ if args.command == "wait":
218
+ return client.generations.wait(args.generation_id, timeout=args.timeout, interval=args.poll_interval)
219
+ if args.command == "cancel":
220
+ return client.generations.cancel(args.generation_id)
221
+ return None
222
+
223
+
224
+ def dispatch_auth(args):
225
+ if args.auth_command == "set-key":
226
+ write_config({"api_key": args.key})
227
+ return {"success": True, "message": "API key saved"}
228
+ if args.auth_command == "show":
229
+ key = get_api_key() or ""
230
+ return {"success": True, "configured": bool(key), "key_prefix": key[:18] if key else None}
231
+ if args.auth_command == "clear":
232
+ clear_api_key()
233
+ return {"success": True, "message": "API key cleared"}
234
+ return None
235
+
236
+
237
+ def dispatch_resources(client, args):
238
+ if args.resources_command == "types":
239
+ return client.resources.types()
240
+ if args.resources_command == "list":
241
+ return client.resources.list(args.resource_type, **resource_filters(args))
242
+ if args.resources_command == "show":
243
+ return client.resources.retrieve(args.resource_type, args.resource_id)
244
+ if args.resources_command == "upload":
245
+ return client.resources.upload(args.resource_type, args.file, **resource_filters(args))
246
+ if args.resources_command == "generate":
247
+ return client.resources.generate(args.resource_type, args.prompt, **resource_filters(args))
248
+ return None
249
+
250
+
251
+ def dispatch_products(client, args):
252
+ if args.products_command == "list":
253
+ return client.products.list(**pagination_filters(args))
254
+ if args.products_command == "create":
255
+ return client.products.create(
256
+ name=args.name,
257
+ image_ids=args.image_ids or [],
258
+ url=args.url,
259
+ product_type=args.product_type,
260
+ description=args.description,
261
+ )
262
+ if args.products_command == "update":
263
+ payload = compact({"name": args.name, "description": args.description, "product_type": args.product_type})
264
+ return client.products.update(args.product_id, **payload)
265
+ if args.products_command == "delete":
266
+ return client.products.delete(args.product_id)
267
+ return None
268
+
269
+
270
+ def resource_filters(args):
271
+ return compact(
272
+ {
273
+ "collection": getattr(args, "collection", None),
274
+ "video_format": getattr(args, "video_format", None),
275
+ "product_type": getattr(args, "product_type", None),
276
+ "type": getattr(args, "type", None),
277
+ "file_type": getattr(args, "file_type", None),
278
+ **pagination_filters(args),
279
+ }
280
+ )
281
+
282
+
283
+ def pagination_filters(args):
284
+ return compact({"page": getattr(args, "page", None), "limit": getattr(args, "limit", None)})
285
+
286
+
287
+ def build_generation_payload(args):
288
+ payload = compact(
289
+ {
290
+ "format": args.format,
291
+ "mode": args.mode,
292
+ "prompt": read_text_or_value(args.prompt),
293
+ "script": read_text_or_value(args.script),
294
+ "language": args.language,
295
+ "aspect_ratio": args.aspect_ratio,
296
+ "duration": args.duration,
297
+ "image": args.image,
298
+ "images": split_csv(args.images),
299
+ "video": args.video,
300
+ "audio": args.audio,
301
+ "avatar": args.avatar,
302
+ "voice": args.voice,
303
+ "background_image": args.background_image,
304
+ "reference_image": args.reference_image,
305
+ "reference_video": args.reference_video,
306
+ "product_id": args.product_id,
307
+ "product_image": args.product_image,
308
+ "product_images": split_csv(args.product_images),
309
+ "product_type": args.product_type,
310
+ "product_description": args.product_description,
311
+ "ad_format": args.ad_format,
312
+ "variations": args.variations,
313
+ }
314
+ )
315
+ if args.metadata:
316
+ payload["metadata"] = json.loads(read_text_or_value(args.metadata))
317
+ return payload
318
+
319
+
320
+ def read_text_or_value(value):
321
+ if not value:
322
+ return value
323
+ try:
324
+ from pathlib import Path
325
+
326
+ path = Path(value)
327
+ if path.exists() and path.is_file():
328
+ return path.read_text()
329
+ except Exception:
330
+ return value
331
+ return value
332
+
333
+
334
+ def split_csv(value):
335
+ if not value:
336
+ return None
337
+ return [item.strip() for item in str(value).split(",") if item.strip()]
338
+
339
+
340
+ def compact(payload):
341
+ return {key: value for key, value in payload.items() if value not in (None, "", [])}
342
+
343
+
344
+ def print_payload(payload, *, as_json=False):
345
+ if as_json:
346
+ print(json.dumps(payload, indent=2, default=str))
347
+ return
348
+ if isinstance(payload, dict):
349
+ generation = payload.get("generation")
350
+ if isinstance(generation, dict):
351
+ print(f"{generation.get('id')}: {generation.get('status')}")
352
+ outputs = generation.get("outputs") if isinstance(generation.get("outputs"), dict) else {}
353
+ if outputs.get("video_url"):
354
+ print(outputs["video_url"])
355
+ if outputs.get("transcript"):
356
+ print(outputs["transcript"])
357
+ return
358
+ if "formats" in payload:
359
+ for item in payload.get("formats") or []:
360
+ print(f"{item.get('slug') or item.get('template_name')}\t{item.get('display_name')}\t{item.get('description')}")
361
+ return
362
+ if "items" in payload:
363
+ for item in payload.get("items") or []:
364
+ print(f"{item.get('id')}\t{item.get('display_name')}\t{item.get('source_kind')}")
365
+ return
366
+ if "id" in payload and "status" in payload:
367
+ print(f"{payload['id']}: {payload['status']}")
368
+ return
369
+ print(json.dumps(payload, indent=2, default=str))
370
+
371
+
372
+ if __name__ == "__main__":
373
+ raise SystemExit(main())
@@ -0,0 +1,219 @@
1
+ import os
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+
8
+ from .config import get_api_key, get_base_url
9
+ from .errors import ClipwiseError
10
+ from .polling import wait_for_generation
11
+
12
+
13
+ class Clipwise:
14
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: float = 30):
15
+ self.api_key = api_key or get_api_key()
16
+ self.base_url = (base_url or get_base_url()).rstrip("/")
17
+ self.timeout = timeout
18
+ self.formats = FormatsResource(self)
19
+ self.resources = ResourcesResource(self)
20
+ self.uploads = UploadsResource(self)
21
+ self.products = ProductsResource(self)
22
+ self.voices = VoicesResource(self)
23
+ self.generations = GenerationsResource(self)
24
+
25
+ def request(self, method: str, path: str, *, params=None, json=None, files=None, data=None, headers=None):
26
+ if not self.api_key:
27
+ raise ClipwiseError("Missing API key. Set CLIPWISE_API_KEY or run clipwise auth set-key.", code="invalid_api_key")
28
+ url = f"{self.base_url}{path}"
29
+ request_headers = {"Authorization": f"Bearer {self.api_key}"}
30
+ if headers:
31
+ request_headers.update(headers)
32
+ with httpx.Client(timeout=self.timeout) as client:
33
+ response = client.request(
34
+ method,
35
+ url,
36
+ params=params,
37
+ json=json,
38
+ files=files,
39
+ data=data,
40
+ headers=request_headers,
41
+ )
42
+ try:
43
+ payload = response.json()
44
+ except Exception:
45
+ payload = {"error": response.text}
46
+ if response.status_code >= 400:
47
+ error = payload.get("error") if isinstance(payload, dict) else {}
48
+ if isinstance(error, dict):
49
+ raise ClipwiseError(
50
+ error.get("message") or "Clipwise API error",
51
+ code=error.get("code"),
52
+ status_code=response.status_code,
53
+ request_id=payload.get("request_id"),
54
+ details=error.get("details") or {},
55
+ )
56
+ raise ClipwiseError(str(error or "Clipwise API error"), status_code=response.status_code)
57
+ return payload
58
+
59
+
60
+ class FormatsResource:
61
+ def __init__(self, client: Clipwise):
62
+ self.client = client
63
+
64
+ def list(self, *, kind="all", platform="all", include_inactive=False):
65
+ return self.client.request(
66
+ "GET",
67
+ "/api/sdk/v1/formats/",
68
+ params={"kind": kind, "platform": platform, "include_inactive": str(bool(include_inactive)).lower()},
69
+ )
70
+
71
+ def retrieve(self, format_id_or_slug):
72
+ return self.client.request("GET", f"/api/sdk/v1/formats/{format_id_or_slug}/")
73
+
74
+
75
+ class ResourcesResource:
76
+ def __init__(self, client: Clipwise):
77
+ self.client = client
78
+
79
+ def types(self):
80
+ return self.client.request("GET", "/api/sdk/v1/resource-types/")
81
+
82
+ def list(self, resource_type, **filters):
83
+ return self.client.request("GET", f"/api/sdk/v1/resources/{resource_type}/", params=filters)
84
+
85
+ def retrieve(self, resource_type, resource_id):
86
+ return self.client.request("GET", f"/api/sdk/v1/resources/{resource_type}/{resource_id}/")
87
+
88
+ def upload(self, resource_type, file, **metadata):
89
+ return _multipart_upload(self.client, f"/api/sdk/v1/resources/{resource_type}/upload/", file, metadata)
90
+
91
+ def generate(self, resource_type, prompt, **metadata):
92
+ payload = {"prompt": prompt, **metadata}
93
+ return self.client.request("POST", f"/api/sdk/v1/resources/{resource_type}/generate/", json=payload)
94
+
95
+ def delete(self, resource_type, resource_id):
96
+ return self.client.request("DELETE", f"/api/sdk/v1/resources/{resource_type}/{resource_id}/")
97
+
98
+
99
+ class UploadsResource:
100
+ def __init__(self, client: Clipwise):
101
+ self.client = client
102
+
103
+ def create(self, file, *, purpose="image", **metadata):
104
+ return _multipart_upload(self.client, "/api/sdk/v1/uploads/", file, {"purpose": purpose, **metadata})
105
+
106
+ def list(self, **filters):
107
+ return self.client.request("GET", "/api/sdk/v1/uploads/", params=filters)
108
+
109
+ def delete(self, upload_id):
110
+ return self.client.request("DELETE", f"/api/sdk/v1/resources/generic_uploads/{upload_id}/")
111
+
112
+
113
+ class ProductsResource:
114
+ def __init__(self, client: Clipwise):
115
+ self.client = client
116
+
117
+ def list(self, **filters):
118
+ return self.client.request("GET", "/api/sdk/v1/products/", params=filters)
119
+
120
+ def create(self, *, name=None, image_ids=None, url=None, **metadata):
121
+ payload = {"name": name, "image_ids": image_ids or [], "url": url, **metadata}
122
+ return self.client.request("POST", "/api/sdk/v1/products/", json={k: v for k, v in payload.items() if v not in (None, [], "")})
123
+
124
+ def update(self, product_id, **metadata):
125
+ return self.client.request("PATCH", f"/api/sdk/v1/products/{product_id}/", json=metadata)
126
+
127
+ def delete(self, product_id):
128
+ return self.client.request("DELETE", f"/api/sdk/v1/products/{product_id}/")
129
+
130
+
131
+ class VoicesResource:
132
+ def __init__(self, client: Clipwise):
133
+ self.client = client
134
+
135
+ def list(self, **filters):
136
+ return self.client.resources.list("voices", **filters)
137
+
138
+ def clone(self, file, *, name=None, language=None, description=None):
139
+ return self.client.resources.upload(
140
+ "voices",
141
+ file,
142
+ name=name or "",
143
+ language=language or "",
144
+ description=description or "",
145
+ )
146
+
147
+
148
+ class GenerationsResource:
149
+ def __init__(self, client: Clipwise):
150
+ self.client = client
151
+
152
+ def create(self, *, idempotency_key=None, **params):
153
+ payload = self._upload_local_files(dict(params))
154
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
155
+ return self.client.request("POST", "/api/sdk/v1/generations/", json=payload, headers=headers)
156
+
157
+ def retrieve(self, generation_id):
158
+ return self.client.request("GET", f"/api/sdk/v1/generations/{generation_id}/")
159
+
160
+ def wait(self, generation_id, *, timeout=1800, interval=5):
161
+ return wait_for_generation(self.client, generation_id, timeout=timeout, interval=interval)
162
+
163
+ def cancel(self, generation_id):
164
+ return self.client.request("POST", f"/api/sdk/v1/generations/{generation_id}/cancel/")
165
+
166
+ def _upload_local_files(self, params: dict[str, Any]) -> dict[str, Any]:
167
+ single_mappings = {
168
+ "video": ("uploads", "video"),
169
+ "audio": ("uploads", "audio"),
170
+ "image": ("uploads", "image"),
171
+ "product_image": ("product_images", "product_image"),
172
+ "reference_image": ("poses", "pose"),
173
+ "background_image": ("background_images", "background_image"),
174
+ "reference_video": ("reference_ads", "video"),
175
+ }
176
+ for key, (resource_type, purpose) in single_mappings.items():
177
+ value = params.get(key)
178
+ if _is_local_file(value):
179
+ uploaded = (
180
+ self.client.uploads.create(value, purpose=purpose)
181
+ if resource_type == "uploads"
182
+ else self.client.resources.upload(resource_type, value, type=purpose)
183
+ )
184
+ params[key] = _extract_id(uploaded)
185
+ if isinstance(params.get("product_images"), list):
186
+ params["product_images"] = [
187
+ _extract_id(self.client.resources.upload("product_images", item))
188
+ if _is_local_file(item)
189
+ else item
190
+ for item in params["product_images"]
191
+ ]
192
+ return params
193
+
194
+
195
+ def _multipart_upload(client: Clipwise, path: str, file, metadata: dict):
196
+ file_path = Path(file)
197
+ if not file_path.exists():
198
+ raise ClipwiseError(f"File not found: {file}", code="invalid_upload")
199
+ data = {key: str(value) for key, value in metadata.items() if value is not None}
200
+ with file_path.open("rb") as handle:
201
+ files = {"file": (file_path.name, handle)}
202
+ return client.request("POST", path, files=files, data=data)
203
+
204
+
205
+ def _is_local_file(value) -> bool:
206
+ return isinstance(value, (str, os.PathLike)) and Path(value).exists()
207
+
208
+
209
+ def _extract_id(payload):
210
+ if not isinstance(payload, dict):
211
+ return payload
212
+ for key in ("upload", "resource", "product", "token"):
213
+ value = payload.get(key)
214
+ if isinstance(value, dict) and value.get("id"):
215
+ return value["id"]
216
+ generation = payload.get("generation")
217
+ if isinstance(generation, dict) and generation.get("id"):
218
+ return generation["id"]
219
+ return payload.get("id") or payload
@@ -0,0 +1,44 @@
1
+ import json
2
+ import os
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ DEFAULT_BASE_URL = "https://app.clipwise.ai"
8
+ CONFIG_PATH = Path(os.environ.get("CLIPWISE_CONFIG_PATH", Path.home() / ".clipwise" / "config.json"))
9
+
10
+
11
+ def get_api_key():
12
+ env_key = os.environ.get("CLIPWISE_API_KEY")
13
+ if env_key:
14
+ return env_key
15
+ config = read_config()
16
+ return config.get("api_key")
17
+
18
+
19
+ def get_base_url():
20
+ return os.environ.get("CLIPWISE_BASE_URL") or read_config().get("base_url") or DEFAULT_BASE_URL
21
+
22
+
23
+ def read_config():
24
+ if not CONFIG_PATH.exists():
25
+ return {}
26
+ try:
27
+ return json.loads(CONFIG_PATH.read_text())
28
+ except Exception:
29
+ return {}
30
+
31
+
32
+ def write_config(values):
33
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
34
+ current = read_config()
35
+ current.update(values)
36
+ CONFIG_PATH.write_text(json.dumps(current, indent=2, sort_keys=True))
37
+
38
+
39
+ def clear_api_key():
40
+ current = read_config()
41
+ current.pop("api_key", None)
42
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
43
+ CONFIG_PATH.write_text(json.dumps(current, indent=2, sort_keys=True))
44
+
@@ -0,0 +1,8 @@
1
+ class ClipwiseError(Exception):
2
+ def __init__(self, message, *, code=None, status_code=None, request_id=None, details=None):
3
+ super().__init__(message)
4
+ self.code = code or "clipwise_error"
5
+ self.status_code = status_code
6
+ self.request_id = request_id
7
+ self.details = details or {}
8
+
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class ResourceRef:
7
+ id: str
8
+ resource_type: str
9
+ source_kind: Optional[str] = None
@@ -0,0 +1,19 @@
1
+ import time
2
+
3
+
4
+ TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
5
+
6
+
7
+ def wait_for_generation(client, generation_id, *, timeout=1800, interval=5):
8
+ deadline = time.time() + timeout
9
+ while True:
10
+ payload = client.generations.retrieve(generation_id)
11
+ generation = payload.get("generation", payload)
12
+ if generation.get("status") in TERMINAL_STATUSES:
13
+ return payload
14
+ if time.time() >= deadline:
15
+ from .errors import ClipwiseError
16
+
17
+ raise ClipwiseError("Timed out waiting for generation", code="timeout")
18
+ time.sleep(interval)
19
+
@@ -0,0 +1,2 @@
1
+ """Resource modules are intentionally thin; use `clipwise.Clipwise`."""
2
+
@@ -0,0 +1,4 @@
1
+ from ..client import FormatsResource
2
+
3
+ __all__ = ["FormatsResource"]
4
+
@@ -0,0 +1,4 @@
1
+ from ..client import GenerationsResource
2
+
3
+ __all__ = ["GenerationsResource"]
4
+
@@ -0,0 +1,4 @@
1
+ from ..client import ProductsResource
2
+
3
+ __all__ = ["ProductsResource"]
4
+
@@ -0,0 +1,4 @@
1
+ from ..client import ResourcesResource
2
+
3
+ __all__ = ["ResourcesResource"]
4
+
@@ -0,0 +1,4 @@
1
+ from ..client import UploadsResource
2
+
3
+ __all__ = ["UploadsResource"]
4
+
@@ -0,0 +1,4 @@
1
+ from ..client import VoicesResource
2
+
3
+ __all__ = ["VoicesResource"]
4
+