Skip to content

Commit 2c32efc

Browse files
authored
Merge pull request #2 from ofspectrum/sync-sdk-v1.1.4
Sync SDK v1.1.4 from neo
2 parents 502e3a7 + 0a91124 commit 2c32efc

12 files changed

Lines changed: 339 additions & 146 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name: Publish to PyPI
33

44
on:
5+
workflow_dispatch:
56
release:
67
types: [published]
78
push:

‎.github/workflows/tag-release.yml‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Create a release tag after SDK changes land on main.
2+
name: Tag SDK Release
3+
4+
on:
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- 'pyproject.toml'
10+
workflow_dispatch:
11+
12+
permissions:
13+
actions: write
14+
contents: write
15+
16+
jobs:
17+
tag-release:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0
25+
26+
- name: Read SDK version
27+
id: version
28+
run: |
29+
VERSION=$(python - <<'PY'
30+
import tomllib
31+
32+
with open("pyproject.toml", "rb") as f:
33+
print(tomllib.load(f)["project"]["version"])
34+
PY
35+
)
36+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
37+
38+
- name: Create release tag and dispatch publish
39+
env:
40+
GH_TOKEN: ${{ github.token }}
41+
run: |
42+
VERSION="${{ steps.version.outputs.version }}"
43+
TAG="v$VERSION"
44+
45+
git fetch --tags
46+
if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
47+
echo "Tag $TAG already exists, skipping"
48+
exit 0
49+
fi
50+
51+
git config user.name "github-actions[bot]"
52+
git config user.email "github-actions[bot]@users.noreply.github.com"
53+
git tag -a "$TAG" -m "Release $TAG"
54+
git push origin "$TAG"
55+
56+
# Tags pushed with GITHUB_TOKEN do not trigger push workflows, so
57+
# dispatch the existing publish workflow explicitly at the new tag.
58+
gh workflow run publish.yml --ref "$TAG"

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Changelog
2+
3+
## 1.1.4
4+
5+
- Updated token documentation for Standard, Pro, and eligible Enterprise workflows.
6+
- Clarified the recommended encode save workflow using `result.save()`.
7+
- Clarified quota and notebook documentation with account-level wording.
8+
- Removed default verification-key examples and internal implementation wording from public docs.
9+
10+
## 1.1.3
11+
12+
- Previous public SDK release.

‎README.md‎

Lines changed: 82 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OfSpectrum Python SDK
22

3-
Official Python SDK for the OfSpectrum Audio Watermarking API.
3+
Official Python SDK for the OfSpectrum audio watermarking API.
44

55
## Installation
66

@@ -19,122 +19,154 @@ pip install -e /path/to/neo/sdk
1919
```python
2020
from ofspectrum import OfSpectrum
2121

22-
# Initialize client with your API key
2322
client = OfSpectrum(api_key="your_api_key")
2423

25-
# Create a watermark token (simple - uses "standard" type by default)
24+
# Create a Standard token.
2625
token = client.tokens.create(name="Production Token")
2726
print(f"Created token: {token.id}")
2827

29-
# Or specify a type (creator/enterprise require public_key)
30-
# token = client.tokens.create(
31-
# name="Creator Token",
32-
# token_type="creator",
33-
# public_key=12345
34-
# )
35-
36-
# Encode watermark into audio
28+
# Encode and save watermarked audio.
3729
result = client.audio.encode(
3830
audio="input.mp3",
3931
token_id=token.id,
40-
output_path="watermarked.mp3"
4132
)
33+
result.save("watermarked.mp3")
4234
print(f"Encoded {result.audio_duration}s of audio")
4335

44-
# Decode (detect) watermark from audio
36+
# Decode watermark from audio.
4537
decode = client.audio.decode("suspect.mp3")
4638
if decode.watermarked:
47-
print(f"Watermark detected! Token ID: {decode.token_id}")
39+
print(f"Watermark detected. Token ID: {decode.token_id}")
4840
else:
4941
print("No watermark detected")
5042

51-
# Check your quota
43+
# Check your quota.
5244
quota = client.quotas.get_encode_quota()
5345
print(f"Remaining encode quota: {quota.remaining}/{quota.quota_limit} seconds")
5446
```
5547

56-
## Features
48+
## Token Management
5749

58-
### Token Management
50+
Standard tokens are the simplest option. Pro tokens support workflow-specific verification-key configuration. Enterprise token configuration is available for eligible enterprise accounts or existing enterprise tokens.
5951

6052
```python
61-
# List all tokens
53+
import os
54+
55+
# List all tokens.
6256
tokens = client.tokens.list()
6357

64-
# Get a specific token
58+
# Get a specific token.
6559
token = client.tokens.get("token-uuid")
6660

67-
# Update a token
61+
# Create a Pro token when your workflow requires a configurable verification key.
62+
verification_key = int(os.environ["OFSPECTRUM_PUBLIC_KEY"])
63+
token = client.tokens.create(
64+
name="Pro Token",
65+
token_type="pro",
66+
public_key=verification_key,
67+
)
68+
69+
# Update a token name.
6870
token = client.tokens.update(
6971
token_id="token-uuid",
70-
name="New Name"
72+
name="New Name",
7173
)
7274

73-
# Note: Token deletion is not available via API.
74-
# Tokens are consumable resources.
75+
# Update a Pro or eligible Enterprise token verification key.
76+
token = client.tokens.update(
77+
token_id="token-uuid",
78+
public_key=verification_key,
79+
)
7580
```
7681

77-
### Audio Watermarking
82+
Token deletion is not available via API. Tokens are consumable resources.
83+
84+
## Audio Watermarking
7885

7986
```python
80-
# Encode with custom settings
8187
result = client.audio.encode(
8288
audio="input.mp3",
8389
token_id=token.id,
84-
strength=1.0, # 0.1-2.0
85-
normalize=True, # Smooth audio to reduce artifacts
86-
check_watermark=True, # Check for existing watermark first
87-
output_path="output.mp3"
90+
strength=1.0,
91+
smooth=True,
8892
)
93+
result.save("output.mp3")
8994

90-
# Decode watermark from audio file
9195
decode = client.audio.decode("suspect.mp3")
9296
if decode.watermarked:
9397
print(f"Token: {decode.token_id}")
9498
```
9599

96-
### Notebook (Notes) Management
100+
Use `decode(..., public_key=verification_key)` only when your workflow requires an explicit verification key.
101+
102+
## Streaming PCM Encode
103+
104+
Use `stream_encode_pcm()` when your application already works with raw PCM audio or needs low-latency streaming from a file-processing pipeline, microphone, call, meeting, or live stream.
105+
106+
The input must be raw PCM float32 little-endian bytes. 48 kHz mono is recommended. The SDK does not currently decode MP3/WAV/FLAC files, resample audio, or convert containers for this streaming method.
107+
108+
```python
109+
def chunk_pcm(pcm_bytes: bytes, chunk_seconds: float = 0.5):
110+
sample_rate = 48000
111+
channels = 1
112+
bytes_per_second = sample_rate * channels * 4
113+
chunk_size = int(bytes_per_second * chunk_seconds)
114+
for offset in range(0, len(pcm_bytes), chunk_size):
115+
yield pcm_bytes[offset:offset + chunk_size]
116+
117+
result = client.audio.stream_encode_pcm(
118+
pcm_chunks=chunk_pcm(pcm_f32le_bytes),
119+
token_id=token.id,
120+
sample_rate=48000,
121+
channels=1,
122+
smooth=True,
123+
)
124+
125+
encoded_pcm = result.encoded_pcm
126+
print(f"Encoded {result.audio_duration:.2f}s of PCM")
127+
```
128+
129+
`encoded_pcm` is raw PCM float32 little-endian, not WAV or MP3. Wrap it in a WAV container or encode it to your desired output format before playback or download.
130+
131+
## Notebook Management
132+
133+
Attach notes and media files to tokens. Private notebooks require a credential, and limits depend on your account and token configuration.
97134

98135
```python
99-
# Create a public notebook for a token
100136
notebook = client.notebooks.create(
101137
token_id=token.id,
102138
note_name="Release Notes",
103-
text_content="## Version 1.0\n\nInitial release.",
104-
is_public=True
139+
text_content="## Version 1.0\n\nRelease notes.",
140+
is_public=True,
105141
)
106142

107-
# Create a private notebook (default password: "123")
108-
private_nb = client.notebooks.create(
143+
private_notebook = client.notebooks.create(
109144
token_id=token.id,
110145
note_name="Private Notes",
111146
text_content="Confidential content",
112147
is_public=False,
113-
credential_val="123" # Optional, defaults to "123"
148+
credential_val="choose-a-secure-credential",
114149
)
115150

116-
# Upload media to notebook
117151
client.notebooks.upload_media(
118152
note_id=notebook.id,
119-
file="cover.jpg"
153+
file="cover.jpg",
120154
)
121155

122-
# List notebooks for a token
123156
notebooks = client.notebooks.list(token_id=token.id)
124157
```
125158

126-
### Quota Checking
159+
## Quota Checking
127160

128161
```python
129-
# Get all quotas
130-
quotas = client.quotas.get_all()
131-
for quota in quotas:
132-
print(f"{quota.service_name}: {quota.remaining}/{quota.quota_limit}")
162+
quota = client.quotas.get_encode_quota()
163+
print(f"Remaining encode quota: {quota.remaining}")
164+
165+
decode_quota = client.quotas.get_decode_quota()
166+
print(f"Remaining decode quota: {decode_quota.remaining}")
133167

134-
# Check if encoding is available
135168
if client.quotas.check_encode_available(duration_seconds=300):
136-
# Proceed with encoding
137-
pass
169+
result = client.audio.encode(audio="input.mp3", token_id=token.id)
138170
```
139171

140172
## Error Handling
@@ -152,60 +184,20 @@ from ofspectrum import (
152184
try:
153185
result = client.audio.encode(audio="input.mp3", token_id="...")
154186
except RateLimitError as e:
155-
print(f"Rate limited! Retry after {e.retry_after} seconds")
187+
print(f"Rate limited. Retry after {e.retry_after} seconds")
156188
except QuotaExceededError as e:
157189
print(f"Quota exceeded for {e.service}")
158190
except WatermarkExistsError:
159-
print("Audio already has a watermark!")
191+
print("Audio already has a watermark")
160192
except AuthenticationError:
161193
print("Invalid API key")
162194
except OfSpectrumError as e:
163195
print(f"API error: {e.code} - {e.message}")
164196
```
165197

166-
## Retry Logic
167-
168-
```python
169-
from ofspectrum import OfSpectrum, RetryConfig, with_retry
170-
171-
# Use built-in retry decorator
172-
@with_retry(RetryConfig(max_retries=3))
173-
def encode_with_retry():
174-
return client.audio.encode(...)
175-
176-
# Or configure retry globally
177-
config = RetryConfig(
178-
max_retries=5,
179-
initial_delay=1.0,
180-
max_delay=60.0,
181-
exponential_base=2.0,
182-
jitter=True
183-
)
184-
```
185-
186198
## Context Manager
187199

188200
```python
189-
# Recommended: Use context manager for automatic cleanup
190201
with OfSpectrum(api_key="your_api_key") as client:
191202
tokens = client.tokens.list()
192203
```
193-
194-
## Configuration
195-
196-
```python
197-
client = OfSpectrum(
198-
api_key="your_api_key",
199-
base_url="https://api.ofspectrum.com/api/v1", # Optional: custom base URL
200-
timeout=120.0 # Optional: request timeout in seconds
201-
)
202-
```
203-
204-
## Requirements
205-
206-
- Python 3.8+
207-
- httpx >= 0.24.0
208-
209-
## License
210-
211-
MIT License

‎ofspectrum/__init__.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""
22
OfSpectrum Python SDK
33
4-
Audio watermarking and AI detection API client.
4+
Audio watermarking API client.
55
66
Example:
77
from ofspectrum import OfSpectrum
88
99
client = OfSpectrum(api_key="your_api_key")
1010
1111
# Create a token
12-
token = client.tokens.create(name="My Token", token_type="creator")
12+
token = client.tokens.create(name="My Token", token_type="pro")
1313
1414
# Encode watermark
1515
result = client.audio.encode(
@@ -29,7 +29,7 @@
2929
print(f"Remaining: {quota.remaining}/{quota.quota_limit}")
3030
"""
3131

32-
__version__ = "1.1.3"
32+
__version__ = "1.1.4"
3333
__author__ = "OfSpectrum"
3434

3535
from .client import OfSpectrum, AsyncOfSpectrum
@@ -54,6 +54,7 @@
5454
NotebookCreateParams,
5555
EncodeResult,
5656
DecodeResult,
57+
StreamingEncodeResult,
5758
Quota,
5859
QuotaList,
5960
)
@@ -83,6 +84,7 @@
8384
"NotebookCreateParams",
8485
"EncodeResult",
8586
"DecodeResult",
87+
"StreamingEncodeResult",
8688
"Quota",
8789
"QuotaList",
8890
# Utils

0 commit comments

Comments
 (0)