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.
- clipwise-0.1.0b1/.gitignore +94 -0
- clipwise-0.1.0b1/CLI.md +54 -0
- clipwise-0.1.0b1/LICENSE +2 -0
- clipwise-0.1.0b1/LLM_USAGE.md +35 -0
- clipwise-0.1.0b1/PKG-INFO +53 -0
- clipwise-0.1.0b1/README.md +43 -0
- clipwise-0.1.0b1/pyproject.toml +20 -0
- clipwise-0.1.0b1/src/clipwise/__init__.py +5 -0
- clipwise-0.1.0b1/src/clipwise/cli.py +373 -0
- clipwise-0.1.0b1/src/clipwise/client.py +219 -0
- clipwise-0.1.0b1/src/clipwise/config.py +44 -0
- clipwise-0.1.0b1/src/clipwise/errors.py +8 -0
- clipwise-0.1.0b1/src/clipwise/models.py +9 -0
- clipwise-0.1.0b1/src/clipwise/polling.py +19 -0
- clipwise-0.1.0b1/src/clipwise/resources/__init__.py +2 -0
- clipwise-0.1.0b1/src/clipwise/resources/formats.py +4 -0
- clipwise-0.1.0b1/src/clipwise/resources/generations.py +4 -0
- clipwise-0.1.0b1/src/clipwise/resources/products.py +4 -0
- clipwise-0.1.0b1/src/clipwise/resources/resources.py +4 -0
- clipwise-0.1.0b1/src/clipwise/resources/uploads.py +4 -0
- clipwise-0.1.0b1/src/clipwise/resources/voices.py +4 -0
|
@@ -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/
|
clipwise-0.1.0b1/CLI.md
ADDED
|
@@ -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.
|
clipwise-0.1.0b1/LICENSE
ADDED
|
@@ -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,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,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
|
+
|