verda-watermark 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- verda_watermark-0.1.0/PKG-INFO +116 -0
- verda_watermark-0.1.0/README.md +91 -0
- verda_watermark-0.1.0/pyproject.toml +37 -0
- verda_watermark-0.1.0/setup.cfg +4 -0
- verda_watermark-0.1.0/verda/__init__.py +5 -0
- verda_watermark-0.1.0/verda/client.py +278 -0
- verda_watermark-0.1.0/verda/models.py +86 -0
- verda_watermark-0.1.0/verda_watermark.egg-info/PKG-INFO +116 -0
- verda_watermark-0.1.0/verda_watermark.egg-info/SOURCES.txt +10 -0
- verda_watermark-0.1.0/verda_watermark.egg-info/dependency_links.txt +1 -0
- verda_watermark-0.1.0/verda_watermark.egg-info/requires.txt +1 -0
- verda_watermark-0.1.0/verda_watermark.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verda-watermark
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Verda Enterprise Watermarking API
|
|
5
|
+
Author-email: Verda AI <support@verda.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://verda.ai
|
|
8
|
+
Project-URL: Documentation, https://enterprise.verda.ai/api-docs
|
|
9
|
+
Project-URL: Repository, https://github.com/VerdaAI/sdk-python
|
|
10
|
+
Keywords: watermark,watermarking,content-protection,verda,ai
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: requests>=2.28.0
|
|
25
|
+
|
|
26
|
+
# Verda Python SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for the [Verda Enterprise Watermarking API](https://enterprise.verda.ai/api-docs).
|
|
29
|
+
|
|
30
|
+
Embed imperceptible watermarks into images, videos, and audio. Decode watermarks to trace content back to its source.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install verda
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from verda import VerdaClient
|
|
42
|
+
|
|
43
|
+
client = VerdaClient(api_key="vk_...")
|
|
44
|
+
|
|
45
|
+
# Encode a watermark into a local file
|
|
46
|
+
result = client.encode("photo.jpg")
|
|
47
|
+
print(result.watermark_id) # 40-bit watermark identifier
|
|
48
|
+
print(result.download_url) # Download the watermarked file
|
|
49
|
+
|
|
50
|
+
# Encode from a URL
|
|
51
|
+
result = client.encode(url="https://cdn.example.com/video.mp4")
|
|
52
|
+
|
|
53
|
+
# Decode / verify a watermark
|
|
54
|
+
result = client.decode("suspect.jpg")
|
|
55
|
+
print(result.match) # True if watermark found
|
|
56
|
+
print(result.watermark_id) # Original watermark ID
|
|
57
|
+
|
|
58
|
+
# Check credits
|
|
59
|
+
credits = client.credits()
|
|
60
|
+
print(f"${credits.balance_dollars:.2f} remaining")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = VerdaClient(
|
|
67
|
+
api_key="vk_...", # or set VERDA_API_KEY env var
|
|
68
|
+
base_url="https://api.verda.ai/api/v2/enterprise", # default
|
|
69
|
+
poll_interval=5, # seconds between job polls
|
|
70
|
+
poll_timeout=300, # max seconds to wait for a job
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async Jobs
|
|
75
|
+
|
|
76
|
+
By default, `encode()` and `decode()` wait for the job to complete. For long-running jobs (video), you can use `wait=False`:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Start encode without waiting
|
|
80
|
+
job_id = client.encode("long_video.mp4", wait=False)
|
|
81
|
+
|
|
82
|
+
# Check status later
|
|
83
|
+
job = client.get_job(job_id)
|
|
84
|
+
print(job.status) # QUEUED, PROCESSING, COMPLETED, or FAILED
|
|
85
|
+
|
|
86
|
+
# Or poll manually
|
|
87
|
+
import time
|
|
88
|
+
while True:
|
|
89
|
+
job = client.get_job(job_id)
|
|
90
|
+
if job.status in ("COMPLETED", "FAILED"):
|
|
91
|
+
break
|
|
92
|
+
time.sleep(10)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Supported Formats
|
|
96
|
+
|
|
97
|
+
| Type | Formats |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| Image | jpg, png, webp, bmp, gif, tiff, heic |
|
|
100
|
+
| Video | mp4, mov, avi, mkv, webm |
|
|
101
|
+
| Audio | mp3, wav, flac, aac, ogg, m4a |
|
|
102
|
+
|
|
103
|
+
## Pricing
|
|
104
|
+
|
|
105
|
+
| Operation | Price | Free Tier |
|
|
106
|
+
|-----------|-------|-----------|
|
|
107
|
+
| Image encode | $0.005/file | 100/month |
|
|
108
|
+
| Audio encode | $0.01/minute | 10 min/month |
|
|
109
|
+
| Video encode | $0.10/minute | 10 min/month |
|
|
110
|
+
| Image decode | $0.001/file | 1,000/month |
|
|
111
|
+
| Audio decode | $0.001/minute | 1,000/month |
|
|
112
|
+
| Video decode | $0.001/minute | 1,000/month |
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Verda Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Verda Enterprise Watermarking API](https://enterprise.verda.ai/api-docs).
|
|
4
|
+
|
|
5
|
+
Embed imperceptible watermarks into images, videos, and audio. Decode watermarks to trace content back to its source.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install verda
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from verda import VerdaClient
|
|
17
|
+
|
|
18
|
+
client = VerdaClient(api_key="vk_...")
|
|
19
|
+
|
|
20
|
+
# Encode a watermark into a local file
|
|
21
|
+
result = client.encode("photo.jpg")
|
|
22
|
+
print(result.watermark_id) # 40-bit watermark identifier
|
|
23
|
+
print(result.download_url) # Download the watermarked file
|
|
24
|
+
|
|
25
|
+
# Encode from a URL
|
|
26
|
+
result = client.encode(url="https://cdn.example.com/video.mp4")
|
|
27
|
+
|
|
28
|
+
# Decode / verify a watermark
|
|
29
|
+
result = client.decode("suspect.jpg")
|
|
30
|
+
print(result.match) # True if watermark found
|
|
31
|
+
print(result.watermark_id) # Original watermark ID
|
|
32
|
+
|
|
33
|
+
# Check credits
|
|
34
|
+
credits = client.credits()
|
|
35
|
+
print(f"${credits.balance_dollars:.2f} remaining")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
client = VerdaClient(
|
|
42
|
+
api_key="vk_...", # or set VERDA_API_KEY env var
|
|
43
|
+
base_url="https://api.verda.ai/api/v2/enterprise", # default
|
|
44
|
+
poll_interval=5, # seconds between job polls
|
|
45
|
+
poll_timeout=300, # max seconds to wait for a job
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Async Jobs
|
|
50
|
+
|
|
51
|
+
By default, `encode()` and `decode()` wait for the job to complete. For long-running jobs (video), you can use `wait=False`:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Start encode without waiting
|
|
55
|
+
job_id = client.encode("long_video.mp4", wait=False)
|
|
56
|
+
|
|
57
|
+
# Check status later
|
|
58
|
+
job = client.get_job(job_id)
|
|
59
|
+
print(job.status) # QUEUED, PROCESSING, COMPLETED, or FAILED
|
|
60
|
+
|
|
61
|
+
# Or poll manually
|
|
62
|
+
import time
|
|
63
|
+
while True:
|
|
64
|
+
job = client.get_job(job_id)
|
|
65
|
+
if job.status in ("COMPLETED", "FAILED"):
|
|
66
|
+
break
|
|
67
|
+
time.sleep(10)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Supported Formats
|
|
71
|
+
|
|
72
|
+
| Type | Formats |
|
|
73
|
+
|------|---------|
|
|
74
|
+
| Image | jpg, png, webp, bmp, gif, tiff, heic |
|
|
75
|
+
| Video | mp4, mov, avi, mkv, webm |
|
|
76
|
+
| Audio | mp3, wav, flac, aac, ogg, m4a |
|
|
77
|
+
|
|
78
|
+
## Pricing
|
|
79
|
+
|
|
80
|
+
| Operation | Price | Free Tier |
|
|
81
|
+
|-----------|-------|-----------|
|
|
82
|
+
| Image encode | $0.005/file | 100/month |
|
|
83
|
+
| Audio encode | $0.01/minute | 10 min/month |
|
|
84
|
+
| Video encode | $0.10/minute | 10 min/month |
|
|
85
|
+
| Image decode | $0.001/file | 1,000/month |
|
|
86
|
+
| Audio decode | $0.001/minute | 1,000/month |
|
|
87
|
+
| Video decode | $0.001/minute | 1,000/month |
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verda-watermark"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Verda Enterprise Watermarking API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{name = "Verda AI", email = "support@verda.ai"}]
|
|
13
|
+
keywords = ["watermark", "watermarking", "content-protection", "verda", "ai"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
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
|
+
"Topic :: Multimedia",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"requests>=2.28.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://verda.ai"
|
|
33
|
+
Documentation = "https://enterprise.verda.ai/api-docs"
|
|
34
|
+
Repository = "https://github.com/VerdaAI/sdk-python"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["verda*"]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import mimetypes
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from verda.models import EncodeResult, DecodeResult, Job, CreditBalance
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.verda.ai/api/v2/enterprise"
|
|
12
|
+
DEFAULT_TIMEOUT = 30
|
|
13
|
+
DEFAULT_POLL_INTERVAL = 5
|
|
14
|
+
DEFAULT_POLL_TIMEOUT = 300 # 5 minutes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerdaError(Exception):
|
|
18
|
+
def __init__(self, message: str, status_code: int = 0, error_code: str = ""):
|
|
19
|
+
self.message = message
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.error_code = error_code
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InsufficientCreditsError(VerdaError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VerdaClient:
|
|
30
|
+
"""Verda Enterprise API client for watermarking media.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
client = VerdaClient(api_key="vk_...")
|
|
34
|
+
|
|
35
|
+
# Encode a local file (uploads + encodes + waits for result)
|
|
36
|
+
result = client.encode("photo.jpg")
|
|
37
|
+
print(result.watermark_id)
|
|
38
|
+
print(result.download_url)
|
|
39
|
+
|
|
40
|
+
# Encode from a URL
|
|
41
|
+
result = client.encode(url="https://cdn.example.com/image.png")
|
|
42
|
+
|
|
43
|
+
# Decode / verify
|
|
44
|
+
result = client.decode("suspect.jpg")
|
|
45
|
+
print(result.match)
|
|
46
|
+
print(result.watermark_id)
|
|
47
|
+
|
|
48
|
+
# Check credits
|
|
49
|
+
credits = client.credits()
|
|
50
|
+
print(f"${credits.balance_dollars:.4f} remaining")
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: Optional[str] = None,
|
|
56
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
57
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
58
|
+
poll_interval: int = DEFAULT_POLL_INTERVAL,
|
|
59
|
+
poll_timeout: int = DEFAULT_POLL_TIMEOUT,
|
|
60
|
+
):
|
|
61
|
+
self.api_key = api_key or os.environ.get("VERDA_API_KEY", "")
|
|
62
|
+
if not self.api_key:
|
|
63
|
+
raise VerdaError("API key required. Pass api_key= or set VERDA_API_KEY env var.")
|
|
64
|
+
self.base_url = base_url.rstrip("/")
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
self.poll_interval = poll_interval
|
|
67
|
+
self.poll_timeout = poll_timeout
|
|
68
|
+
self._session = requests.Session()
|
|
69
|
+
self._session.headers.update({
|
|
70
|
+
"X-API-Key": self.api_key,
|
|
71
|
+
"User-Agent": "verda-python/0.1.0",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
75
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
76
|
+
resp = self._session.request(method, f"{self.base_url}{path}", **kwargs)
|
|
77
|
+
|
|
78
|
+
if resp.status_code == 402:
|
|
79
|
+
raise InsufficientCreditsError("Insufficient credits", status_code=402)
|
|
80
|
+
|
|
81
|
+
if resp.status_code >= 400:
|
|
82
|
+
body = {}
|
|
83
|
+
try:
|
|
84
|
+
body = resp.json()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
msg = body.get("message", body.get("error", f"HTTP {resp.status_code}"))
|
|
88
|
+
raise VerdaError(msg, status_code=resp.status_code, error_code=body.get("error_code", ""))
|
|
89
|
+
|
|
90
|
+
data = resp.json()
|
|
91
|
+
return data.get("data", data)
|
|
92
|
+
|
|
93
|
+
def _get(self, path: str, **kwargs) -> dict:
|
|
94
|
+
return self._request("GET", path, **kwargs)
|
|
95
|
+
|
|
96
|
+
def _post(self, path: str, **kwargs) -> dict:
|
|
97
|
+
return self._request("POST", path, **kwargs)
|
|
98
|
+
|
|
99
|
+
def _delete(self, path: str, **kwargs) -> dict:
|
|
100
|
+
return self._request("DELETE", path, **kwargs)
|
|
101
|
+
|
|
102
|
+
# --- Upload ---
|
|
103
|
+
|
|
104
|
+
def _upload_file(self, file_path: str) -> tuple[str, str]:
|
|
105
|
+
"""Upload a local file to Verda S3 and return (content_id, file_format)."""
|
|
106
|
+
p = Path(file_path)
|
|
107
|
+
if not p.exists():
|
|
108
|
+
raise VerdaError(f"File not found: {file_path}")
|
|
109
|
+
|
|
110
|
+
file_format = p.suffix.lstrip(".").lower()
|
|
111
|
+
content_type = _guess_content_type(file_format)
|
|
112
|
+
|
|
113
|
+
resp = self._get("/watermark/upload-url", params={
|
|
114
|
+
"filename": p.name,
|
|
115
|
+
"content_type": content_type,
|
|
116
|
+
"file_format": file_format,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
upload_url = resp.get("upload_url", resp.get("uploadUrl", ""))
|
|
120
|
+
content_id = resp.get("content_id", resp.get("contentId", ""))
|
|
121
|
+
|
|
122
|
+
if not upload_url or not content_id:
|
|
123
|
+
raise VerdaError("Failed to get upload URL")
|
|
124
|
+
|
|
125
|
+
mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
|
126
|
+
with open(file_path, "rb") as f:
|
|
127
|
+
put_resp = requests.put(upload_url, data=f, headers={"Content-Type": mime_type}, timeout=120)
|
|
128
|
+
if put_resp.status_code >= 400:
|
|
129
|
+
raise VerdaError(f"Upload failed: HTTP {put_resp.status_code}")
|
|
130
|
+
|
|
131
|
+
return content_id, file_format
|
|
132
|
+
|
|
133
|
+
# --- Jobs ---
|
|
134
|
+
|
|
135
|
+
def get_job(self, job_id: str) -> Job:
|
|
136
|
+
"""Get the status of a watermark job."""
|
|
137
|
+
resp = self._get(f"/watermark/jobs/{job_id}")
|
|
138
|
+
job_data = resp.get("job", resp)
|
|
139
|
+
return Job.from_dict(job_data)
|
|
140
|
+
|
|
141
|
+
def _wait_for_job(self, job_id: str) -> Job:
|
|
142
|
+
"""Poll a job until it completes or fails."""
|
|
143
|
+
deadline = time.time() + self.poll_timeout
|
|
144
|
+
while time.time() < deadline:
|
|
145
|
+
job = self.get_job(job_id)
|
|
146
|
+
if job.status in ("COMPLETED", "FAILED"):
|
|
147
|
+
return job
|
|
148
|
+
time.sleep(self.poll_interval)
|
|
149
|
+
raise VerdaError(f"Job {job_id} timed out after {self.poll_timeout}s")
|
|
150
|
+
|
|
151
|
+
# --- Encode ---
|
|
152
|
+
|
|
153
|
+
def encode(
|
|
154
|
+
self,
|
|
155
|
+
file: Optional[str] = None,
|
|
156
|
+
url: Optional[str] = None,
|
|
157
|
+
content_type: Optional[str] = None,
|
|
158
|
+
file_format: Optional[str] = None,
|
|
159
|
+
wait: bool = True,
|
|
160
|
+
) -> Union[EncodeResult, str]:
|
|
161
|
+
"""Encode a watermark into media.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
file: Path to a local file to watermark.
|
|
165
|
+
url: Public URL of media to watermark.
|
|
166
|
+
content_type: "image", "video", or "audio". Auto-detected from file extension if not provided.
|
|
167
|
+
file_format: File extension (e.g. "jpg", "mp4"). Auto-detected if not provided.
|
|
168
|
+
wait: If True (default), poll until the job completes and return the result.
|
|
169
|
+
If False, return the job_id immediately.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
EncodeResult if wait=True, job_id string if wait=False.
|
|
173
|
+
"""
|
|
174
|
+
body = {}
|
|
175
|
+
|
|
176
|
+
if file:
|
|
177
|
+
content_id, fmt = self._upload_file(file)
|
|
178
|
+
body["content_id"] = content_id
|
|
179
|
+
body["file_format"] = file_format or fmt
|
|
180
|
+
body["content_type"] = content_type or _guess_content_type(fmt)
|
|
181
|
+
elif url:
|
|
182
|
+
body["media_url"] = url
|
|
183
|
+
if file_format:
|
|
184
|
+
body["file_format"] = file_format
|
|
185
|
+
else:
|
|
186
|
+
body["file_format"] = _format_from_url(url)
|
|
187
|
+
body["content_type"] = content_type or _guess_content_type(body["file_format"])
|
|
188
|
+
else:
|
|
189
|
+
raise VerdaError("Provide either file= or url=")
|
|
190
|
+
|
|
191
|
+
resp = self._post("/watermark/encode", json=body)
|
|
192
|
+
job_id = resp.get("job_id", resp.get("jobId", ""))
|
|
193
|
+
|
|
194
|
+
if not wait:
|
|
195
|
+
return job_id
|
|
196
|
+
|
|
197
|
+
job = self._wait_for_job(job_id)
|
|
198
|
+
if job.status == "FAILED":
|
|
199
|
+
raise VerdaError(f"Encode failed: {job.error_message}", error_code="ENCODE_FAILED")
|
|
200
|
+
|
|
201
|
+
return EncodeResult.from_job(job.__dict__)
|
|
202
|
+
|
|
203
|
+
# --- Decode ---
|
|
204
|
+
|
|
205
|
+
def decode(
|
|
206
|
+
self,
|
|
207
|
+
file: Optional[str] = None,
|
|
208
|
+
url: Optional[str] = None,
|
|
209
|
+
content_type: Optional[str] = None,
|
|
210
|
+
file_format: Optional[str] = None,
|
|
211
|
+
wait: bool = True,
|
|
212
|
+
) -> Union[DecodeResult, str]:
|
|
213
|
+
"""Decode / verify a watermark from media.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
file: Path to a local file to check.
|
|
217
|
+
url: Public URL of media to check.
|
|
218
|
+
content_type: "image", "video", or "audio". Auto-detected if not provided.
|
|
219
|
+
file_format: File extension. Auto-detected if not provided.
|
|
220
|
+
wait: If True (default), poll until the job completes. If False, return job_id.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
DecodeResult if wait=True, job_id string if wait=False.
|
|
224
|
+
"""
|
|
225
|
+
body = {}
|
|
226
|
+
|
|
227
|
+
if file:
|
|
228
|
+
content_id, fmt = self._upload_file(file)
|
|
229
|
+
body["content_id"] = content_id
|
|
230
|
+
body["file_format"] = file_format or fmt
|
|
231
|
+
body["content_type"] = content_type or _guess_content_type(fmt)
|
|
232
|
+
elif url:
|
|
233
|
+
body["media_url"] = url
|
|
234
|
+
if file_format:
|
|
235
|
+
body["file_format"] = file_format
|
|
236
|
+
else:
|
|
237
|
+
body["file_format"] = _format_from_url(url)
|
|
238
|
+
body["content_type"] = content_type or _guess_content_type(body["file_format"])
|
|
239
|
+
else:
|
|
240
|
+
raise VerdaError("Provide either file= or url=")
|
|
241
|
+
|
|
242
|
+
resp = self._post("/watermark/decode", json=body)
|
|
243
|
+
job_id = resp.get("job_id", resp.get("jobId", ""))
|
|
244
|
+
|
|
245
|
+
if not wait:
|
|
246
|
+
return job_id
|
|
247
|
+
|
|
248
|
+
job = self._wait_for_job(job_id)
|
|
249
|
+
if job.status == "FAILED":
|
|
250
|
+
raise VerdaError(f"Decode failed: {job.error_message}", error_code="DECODE_FAILED")
|
|
251
|
+
|
|
252
|
+
return DecodeResult.from_job(job.__dict__)
|
|
253
|
+
|
|
254
|
+
# --- Credits ---
|
|
255
|
+
|
|
256
|
+
def credits(self) -> CreditBalance:
|
|
257
|
+
"""Check your credit balance."""
|
|
258
|
+
resp = self._get("/credits")
|
|
259
|
+
credit_data = resp.get("credits", resp)
|
|
260
|
+
return CreditBalance.from_dict(credit_data)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _guess_content_type(file_format: str) -> str:
|
|
264
|
+
fmt = file_format.lower().strip(".")
|
|
265
|
+
if fmt in ("jpg", "jpeg", "png", "webp", "bmp", "gif", "tiff", "heic"):
|
|
266
|
+
return "image"
|
|
267
|
+
elif fmt in ("mp4", "mov", "avi", "mkv", "webm", "ts", "m4v"):
|
|
268
|
+
return "video"
|
|
269
|
+
elif fmt in ("mp3", "wav", "flac", "aac", "ogg", "m4a", "wma"):
|
|
270
|
+
return "audio"
|
|
271
|
+
return "image"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _format_from_url(url: str) -> str:
|
|
275
|
+
path = url.split("?")[0].split("#")[0]
|
|
276
|
+
if "." in path:
|
|
277
|
+
return path.rsplit(".", 1)[-1].lower()
|
|
278
|
+
return "jpg"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class EncodeResult:
|
|
7
|
+
job_id: str
|
|
8
|
+
watermark_id: str
|
|
9
|
+
download_url: str
|
|
10
|
+
status: str
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_job(cls, job: dict) -> "EncodeResult":
|
|
14
|
+
return cls(
|
|
15
|
+
job_id=job.get("job_id", ""),
|
|
16
|
+
watermark_id=job.get("watermark_id", ""),
|
|
17
|
+
download_url=job.get("download_url", ""),
|
|
18
|
+
status=job.get("status", ""),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DecodeResult:
|
|
24
|
+
job_id: str
|
|
25
|
+
match: bool
|
|
26
|
+
watermark_id: Optional[str]
|
|
27
|
+
confidence: float
|
|
28
|
+
status: str
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_job(cls, job: dict) -> "DecodeResult":
|
|
32
|
+
return cls(
|
|
33
|
+
job_id=job.get("job_id", ""),
|
|
34
|
+
match=job.get("match", False),
|
|
35
|
+
watermark_id=job.get("watermark_id") or None,
|
|
36
|
+
confidence=job.get("confidence", 0.0),
|
|
37
|
+
status=job.get("status", ""),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Job:
|
|
43
|
+
job_id: str
|
|
44
|
+
job_type: str
|
|
45
|
+
status: str
|
|
46
|
+
watermark_id: Optional[str]
|
|
47
|
+
download_url: Optional[str]
|
|
48
|
+
match: bool
|
|
49
|
+
confidence: float
|
|
50
|
+
error_message: Optional[str]
|
|
51
|
+
created_at: int
|
|
52
|
+
completed_at: Optional[int]
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, d: dict) -> "Job":
|
|
56
|
+
status_map = {0: "UNSPECIFIED", 1: "QUEUED", 2: "PROCESSING", 3: "COMPLETED", 4: "FAILED"}
|
|
57
|
+
raw_status = d.get("status", 0)
|
|
58
|
+
status = status_map.get(raw_status, str(raw_status)) if isinstance(raw_status, int) else raw_status
|
|
59
|
+
return cls(
|
|
60
|
+
job_id=d.get("job_id", ""),
|
|
61
|
+
job_type=d.get("job_type", ""),
|
|
62
|
+
status=status,
|
|
63
|
+
watermark_id=d.get("watermark_id") or None,
|
|
64
|
+
download_url=d.get("download_url") or None,
|
|
65
|
+
match=d.get("match", False),
|
|
66
|
+
confidence=d.get("confidence", 0.0),
|
|
67
|
+
error_message=d.get("error_message") or None,
|
|
68
|
+
created_at=d.get("created_at", 0),
|
|
69
|
+
completed_at=d.get("completed_at") or None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class CreditBalance:
|
|
75
|
+
balance_microdollars: int
|
|
76
|
+
balance_dollars: float
|
|
77
|
+
free_tier_reset_at: int
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, d: dict) -> "CreditBalance":
|
|
81
|
+
micros = d.get("balance_microdollars", d.get("balanceMicrodollars", 0))
|
|
82
|
+
return cls(
|
|
83
|
+
balance_microdollars=micros,
|
|
84
|
+
balance_dollars=micros / 1_000_000,
|
|
85
|
+
free_tier_reset_at=d.get("free_tier_reset_at", d.get("freeTierResetAt", 0)),
|
|
86
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verda-watermark
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Verda Enterprise Watermarking API
|
|
5
|
+
Author-email: Verda AI <support@verda.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://verda.ai
|
|
8
|
+
Project-URL: Documentation, https://enterprise.verda.ai/api-docs
|
|
9
|
+
Project-URL: Repository, https://github.com/VerdaAI/sdk-python
|
|
10
|
+
Keywords: watermark,watermarking,content-protection,verda,ai
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: requests>=2.28.0
|
|
25
|
+
|
|
26
|
+
# Verda Python SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for the [Verda Enterprise Watermarking API](https://enterprise.verda.ai/api-docs).
|
|
29
|
+
|
|
30
|
+
Embed imperceptible watermarks into images, videos, and audio. Decode watermarks to trace content back to its source.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install verda
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from verda import VerdaClient
|
|
42
|
+
|
|
43
|
+
client = VerdaClient(api_key="vk_...")
|
|
44
|
+
|
|
45
|
+
# Encode a watermark into a local file
|
|
46
|
+
result = client.encode("photo.jpg")
|
|
47
|
+
print(result.watermark_id) # 40-bit watermark identifier
|
|
48
|
+
print(result.download_url) # Download the watermarked file
|
|
49
|
+
|
|
50
|
+
# Encode from a URL
|
|
51
|
+
result = client.encode(url="https://cdn.example.com/video.mp4")
|
|
52
|
+
|
|
53
|
+
# Decode / verify a watermark
|
|
54
|
+
result = client.decode("suspect.jpg")
|
|
55
|
+
print(result.match) # True if watermark found
|
|
56
|
+
print(result.watermark_id) # Original watermark ID
|
|
57
|
+
|
|
58
|
+
# Check credits
|
|
59
|
+
credits = client.credits()
|
|
60
|
+
print(f"${credits.balance_dollars:.2f} remaining")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = VerdaClient(
|
|
67
|
+
api_key="vk_...", # or set VERDA_API_KEY env var
|
|
68
|
+
base_url="https://api.verda.ai/api/v2/enterprise", # default
|
|
69
|
+
poll_interval=5, # seconds between job polls
|
|
70
|
+
poll_timeout=300, # max seconds to wait for a job
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async Jobs
|
|
75
|
+
|
|
76
|
+
By default, `encode()` and `decode()` wait for the job to complete. For long-running jobs (video), you can use `wait=False`:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Start encode without waiting
|
|
80
|
+
job_id = client.encode("long_video.mp4", wait=False)
|
|
81
|
+
|
|
82
|
+
# Check status later
|
|
83
|
+
job = client.get_job(job_id)
|
|
84
|
+
print(job.status) # QUEUED, PROCESSING, COMPLETED, or FAILED
|
|
85
|
+
|
|
86
|
+
# Or poll manually
|
|
87
|
+
import time
|
|
88
|
+
while True:
|
|
89
|
+
job = client.get_job(job_id)
|
|
90
|
+
if job.status in ("COMPLETED", "FAILED"):
|
|
91
|
+
break
|
|
92
|
+
time.sleep(10)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Supported Formats
|
|
96
|
+
|
|
97
|
+
| Type | Formats |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| Image | jpg, png, webp, bmp, gif, tiff, heic |
|
|
100
|
+
| Video | mp4, mov, avi, mkv, webm |
|
|
101
|
+
| Audio | mp3, wav, flac, aac, ogg, m4a |
|
|
102
|
+
|
|
103
|
+
## Pricing
|
|
104
|
+
|
|
105
|
+
| Operation | Price | Free Tier |
|
|
106
|
+
|-----------|-------|-----------|
|
|
107
|
+
| Image encode | $0.005/file | 100/month |
|
|
108
|
+
| Audio encode | $0.01/minute | 10 min/month |
|
|
109
|
+
| Video encode | $0.10/minute | 10 min/month |
|
|
110
|
+
| Image decode | $0.001/file | 1,000/month |
|
|
111
|
+
| Audio decode | $0.001/minute | 1,000/month |
|
|
112
|
+
| Video decode | $0.001/minute | 1,000/month |
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
verda/__init__.py
|
|
4
|
+
verda/client.py
|
|
5
|
+
verda/models.py
|
|
6
|
+
verda_watermark.egg-info/PKG-INFO
|
|
7
|
+
verda_watermark.egg-info/SOURCES.txt
|
|
8
|
+
verda_watermark.egg-info/dependency_links.txt
|
|
9
|
+
verda_watermark.egg-info/requires.txt
|
|
10
|
+
verda_watermark.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
verda
|