sixtydb 1.1.0__py3-none-any.whl

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.
sixtydb/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ 60db Python SDK
3
+ Official Python client for the 60db API
4
+ """
5
+
6
+ from .client import SixtyDBClient
7
+ from .music import MusicResource
8
+ from .judge import JudgeResource, JudgeRubricsResource, JudgeRunsResource
9
+ from .dialer import (
10
+ DialerResource,
11
+ DialerNumbersResource,
12
+ DialerCallsResource,
13
+ DialerRecordingsResource,
14
+ DialerBillingResource,
15
+ DialerWebhooksResource,
16
+ )
17
+
18
+ __version__ = "1.1.0"
19
+ __all__ = [
20
+ "SixtyDBClient",
21
+ "MusicResource",
22
+ "JudgeResource",
23
+ "JudgeRubricsResource",
24
+ "JudgeRunsResource",
25
+ "DialerResource",
26
+ "DialerNumbersResource",
27
+ "DialerCallsResource",
28
+ "DialerRecordingsResource",
29
+ "DialerBillingResource",
30
+ "DialerWebhooksResource",
31
+ ]
sixtydb/client.py ADDED
@@ -0,0 +1,312 @@
1
+ """
2
+ 60db API Client
3
+ """
4
+
5
+ import requests
6
+ from typing import Optional, Dict, Any, List, Callable
7
+ import json
8
+ import base64
9
+
10
+ from .music import MusicResource
11
+ from .dialer import DialerResource
12
+ from .judge import JudgeResource
13
+
14
+
15
+ class SixtyDBClient:
16
+ """Official 60db API client"""
17
+
18
+ def __init__(self, api_key: str, base_url: str = "https://api.60db.ai"):
19
+ """
20
+ Initialize the 60db client
21
+
22
+ Args:
23
+ api_key: Your 60db API key
24
+ base_url: API base URL (default: https://api.60db.ai)
25
+ """
26
+ self.api_key = api_key
27
+ self.base_url = base_url.rstrip('/')
28
+ self.session = requests.Session()
29
+ self.session.headers.update({
30
+ 'Authorization': f'Bearer {api_key}',
31
+ 'Content-Type': 'application/json'
32
+ })
33
+ self.music = MusicResource(self)
34
+ self.dialer = DialerResource(self)
35
+ self.judge = JudgeResource(self)
36
+
37
+ def _request(self, method: str, endpoint: str, **kwargs) -> Any:
38
+ """Make HTTP request"""
39
+ url = f"{self.base_url}{endpoint}"
40
+ response = self.session.request(method, url, **kwargs)
41
+ response.raise_for_status()
42
+
43
+ if kwargs.get('stream'):
44
+ return response
45
+
46
+ content_type = response.headers.get('content-type', '')
47
+ if 'application/json' in content_type:
48
+ return response.json()
49
+ return response.content
50
+
51
+ # TTS Methods
52
+ def text_to_speech(self, text: str, voice_id: Optional[str] = None, enhance: Optional[bool] = True, speed: Optional[float] = 1.0, **params) -> bytes:
53
+ """
54
+ Convert text to speech
55
+
56
+ Args:
57
+ text: Text to convert
58
+ voice_id: Voice ID to use
59
+ enhance: Enable audio enhancement
60
+ speed: Speech speed (0.5 to 2.0)
61
+ **params: Additional parameters
62
+
63
+ Returns:
64
+ Audio data as bytes
65
+ """
66
+ data = {'text': text, 'enhance': enhance, 'speed': speed, **params}
67
+ if voice_id:
68
+ data['voice_id'] = voice_id
69
+
70
+ response = self._request('POST', '/tts-synthesize', json=data)
71
+
72
+ if isinstance(response, bytes):
73
+ return response
74
+
75
+ if isinstance(response, dict):
76
+ if response.get("success") and "backendResponse" in response:
77
+ backend = response["backendResponse"]
78
+
79
+ if isinstance(backend, dict) and backend.get("audio_base64"):
80
+ return base64.b64decode(backend["audio_base64"])
81
+
82
+ raise Exception(response.get("message", "Failed to generate audio"))
83
+
84
+ raise Exception("Unexpected TTS response format")
85
+
86
+ def text_to_speech_stream(
87
+ self,
88
+ text: str,
89
+ on_chunk: Callable[[bytes], None],
90
+ voice_id: Optional[str] = None,
91
+ on_complete: Optional[Callable[[], None]] = None,
92
+ on_error: Optional[Callable[[str], None]] = None,
93
+ **params
94
+ ) -> None:
95
+ """
96
+ Stream text to speech with callbacks
97
+
98
+ Args:
99
+ text: Text to convert
100
+ on_chunk: Callback for audio chunks
101
+ voice_id: Voice ID to use
102
+ on_complete: Callback when complete
103
+ on_error: Callback for errors
104
+ **params: Additional parameters
105
+ """
106
+ data = {'text': text, **params}
107
+ if voice_id:
108
+ data['voice_id'] = voice_id
109
+
110
+ try:
111
+ response = requests.post(
112
+ f"{self.base_url}/tts-stream",
113
+ headers={'Authorization': f'Bearer {self.api_key}', 'Content-Type': 'application/json'},
114
+ json=data,
115
+ stream=True
116
+ )
117
+ response.raise_for_status()
118
+
119
+ buffer = ""
120
+ for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
121
+ if chunk:
122
+ buffer += chunk
123
+ lines = buffer.split('\n')
124
+ buffer = lines.pop()
125
+
126
+ for line in lines:
127
+ if not line.strip():
128
+ continue
129
+ try:
130
+ parsed = json.loads(line)
131
+ if parsed.get('type') == 'error':
132
+ if on_error:
133
+ on_error(parsed.get('message', 'Unknown error'))
134
+ elif 'result' in parsed and 'audioContent' in parsed['result']:
135
+ audio_bytes = base64.b64decode(parsed['result']['audioContent'])
136
+ on_chunk(audio_bytes)
137
+ except json.JSONDecodeError:
138
+ pass
139
+
140
+ if on_complete:
141
+ on_complete()
142
+ except Exception as e:
143
+ if on_error:
144
+ on_error(str(e))
145
+ raise
146
+
147
+ # STT Methods
148
+ def speech_to_text(
149
+ self,
150
+ audio_file,
151
+ language: Optional[str] = None,
152
+ diarize: Optional[bool] = None,
153
+ context: Optional[str] = None,
154
+ ) -> Dict[str, Any]:
155
+ """
156
+ Transcribe audio to text.
157
+
158
+ Args:
159
+ audio_file: Path or file-like object for the audio to transcribe.
160
+ language: ISO 639-1 code, or None / "auto" to auto-detect.
161
+ diarize: Enable pyannote speaker diarization.
162
+ context: Free-form string describing the session / domain /
163
+ speakers / jargon. When supplied, the server runs a
164
+ background LLM pass and the response text is polished
165
+ (proper nouns, fillers removed, punctuation). Omit to skip
166
+ refinement.
167
+ """
168
+
169
+ # If string path passed, open it
170
+ if isinstance(audio_file, str):
171
+ file_obj = open(audio_file, "rb")
172
+ filename = audio_file.split("\\")[-1]
173
+ else:
174
+ file_obj = audio_file
175
+ filename = getattr(audio_file, "name", "audio.mp3")
176
+
177
+ files = {
178
+ "audio": (filename, file_obj, "audio/mpeg")
179
+ }
180
+
181
+ data = {}
182
+ if language:
183
+ data["language"] = language
184
+ if diarize:
185
+ data["diarize"] = "true"
186
+ if context and context.strip():
187
+ data["context"] = context.strip()
188
+
189
+ headers = {
190
+ "Authorization": f"Bearer {self.api_key}"
191
+ }
192
+
193
+ response = requests.post(
194
+ f"{self.base_url}/stt",
195
+ headers=headers,
196
+ files=files,
197
+ data=data
198
+ )
199
+
200
+ response.raise_for_status()
201
+ return response.json()
202
+
203
+
204
+ def get_languages(self) -> List[Dict[str, Any]]:
205
+ """Get supported languages"""
206
+ return self._request('GET', '/stt/languages')
207
+
208
+ # Voice Methods
209
+ def get_voices(self) -> List[Dict[str, Any]]:
210
+ """Get all voices"""
211
+ return self._request('GET', '/voices')
212
+
213
+ def get_voice(self, voice_id: str) -> Dict[str, Any]:
214
+ """Get specific voice"""
215
+ return self._request('GET', f'/voices/{voice_id}')
216
+
217
+ def create_voice(self, name: str, files: List, **params) -> Dict[str, Any]:
218
+ """Create custom voice"""
219
+ file_data = [('files', f) for f in files]
220
+ data = {'name': name, **params}
221
+
222
+ headers = {'Authorization': f'Bearer {self.api_key}'}
223
+ response = requests.post(
224
+ f"{self.base_url}/voices",
225
+ headers=headers,
226
+ files=file_data,
227
+ data=data
228
+ )
229
+ response.raise_for_status()
230
+ return response.json()
231
+
232
+ def update_voice(self, voice_id: str, **params) -> Dict[str, Any]:
233
+ """Update voice"""
234
+ return self._request('PUT', f'/voices/{voice_id}', json=params)
235
+
236
+ def delete_voice(self, voice_id: str) -> None:
237
+ """Delete voice"""
238
+ self._request('DELETE', f'/voices/{voice_id}')
239
+
240
+ # Auth Methods
241
+ def sign_up(self, email: str, password: str, **params) -> Dict[str, Any]:
242
+ """Register new user"""
243
+ data = {'email': email, 'password': password, **params}
244
+ return self._request('POST', '/auth/signup', json=data)
245
+
246
+ def sign_in(self, email: str, password: str) -> Dict[str, Any]:
247
+ """Login user"""
248
+ data = {'email': email, 'password': password}
249
+ return self._request('POST', '/auth/login', json=data)
250
+
251
+ def get_profile(self) -> Dict[str, Any]:
252
+ """Get user profile"""
253
+ return self._request('GET', '/auth/me')
254
+
255
+ def update_profile(self, **params) -> Dict[str, Any]:
256
+ """Update user profile"""
257
+ return self._request('PUT', '/auth/profile', json=params)
258
+
259
+ # Workspace Methods
260
+ def get_workspaces(self) -> List[Dict[str, Any]]:
261
+ """Get all workspaces"""
262
+ return self._request('GET', '/workspaces')
263
+
264
+ def create_workspace(self, name: str, **params) -> Dict[str, Any]:
265
+ """Create workspace"""
266
+ data = {'name': name, **params}
267
+ return self._request('POST', '/workspaces', json=data)
268
+
269
+ # Billing Methods
270
+ def get_plans(self) -> List[Dict[str, Any]]:
271
+ """Get available plans"""
272
+ return self._request('GET', '/plans')
273
+
274
+ def get_current_plan(self) -> Dict[str, Any]:
275
+ """Get current subscription"""
276
+ return self._request('GET', '/billing/current-plan')
277
+
278
+ def subscribe(self, plan_id: str) -> Dict[str, Any]:
279
+ """Subscribe to plan"""
280
+ return self._request('POST', '/billing/subscribe', json={'plan_id': plan_id})
281
+
282
+ # Analytics
283
+ def get_usage(self) -> Dict[str, Any]:
284
+ """Get usage statistics"""
285
+ return self._request('GET', '/analytics/usage')
286
+
287
+ # API Keys
288
+ def get_api_keys(self) -> List[Dict[str, Any]]:
289
+ """Get all API keys"""
290
+ return self._request('GET', '/developer/api')
291
+
292
+ def create_api_key(self, name: str) -> Dict[str, Any]:
293
+ """Create API key"""
294
+ return self._request('POST', '/developer/api', json={'name': name})
295
+
296
+ def delete_api_key(self, key_id: str) -> None:
297
+ """Delete API key"""
298
+ self._request('DELETE', f'/developer/api/{key_id}')
299
+
300
+ # Webhooks
301
+ def get_webhooks(self) -> List[Dict[str, Any]]:
302
+ """Get all webhooks"""
303
+ return self._request('GET', '/webhooks')
304
+
305
+ def create_webhook(self, url: str, events: List[str], **params) -> Dict[str, Any]:
306
+ """Create webhook"""
307
+ data = {'url': url, 'events': events, **params}
308
+ return self._request('POST', '/webhooks', json=data)
309
+
310
+ def delete_webhook(self, webhook_id: str) -> None:
311
+ """Delete webhook"""
312
+ self._request('DELETE', f'/webhooks/{webhook_id}')
sixtydb/dialer.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ Dialer resource for the 60db API.
3
+
4
+ Not free: a number costs $6.00/month, calls cost $0.003/min. Most calls
5
+ need a finished trunk setup and approved KYC, both done in the dashboard.
6
+
7
+ `calls.reserve()` does NOT dial. It reserves and authorizes a call id
8
+ before a SIP client (softphone/PBX) sends its SIP INVITE.
9
+ """
10
+
11
+ from typing import Any, Dict, Optional
12
+ from urllib.parse import quote
13
+
14
+
15
+ class DialerNumbersResource:
16
+ def __init__(self, client: "Any") -> None:
17
+ self._client = client
18
+
19
+ def search(self) -> Dict[str, Any]:
20
+ """Search available numbers in the shared pool (no filters)."""
21
+ return self._client._request("GET", "/dialer/pool", timeout=60)
22
+
23
+ def buy(self, number: Optional[str] = None, **params: Any) -> Dict[str, Any]:
24
+ """
25
+ Buy a number. Charges $6.00 now, then $6.00/month.
26
+
27
+ Requires approved dialer KYC, completed inside the 60db app (not
28
+ exposed by this SDK) — otherwise returns 403 DIALER_KYC_REQUIRED,
29
+ DIALER_KYC_PENDING or DIALER_KYC_REJECTED.
30
+
31
+ Args:
32
+ number: Specific E.164 number, or None for the next available one.
33
+ **params: assign_for ("sip"|"indian_number"), outbound_trunk_id,
34
+ inbound_trunk_id, channel_limit.
35
+ """
36
+ body: Dict[str, Any] = {**params}
37
+ if number is not None:
38
+ body["number"] = number
39
+ return self._client._request(
40
+ "POST", "/dialer/pool/allocate", json=body, timeout=60
41
+ )
42
+
43
+ def list(self) -> Dict[str, Any]:
44
+ """List numbers owned by the workspace."""
45
+ return self._client._request("GET", "/dialer/numbers", timeout=60)
46
+
47
+ def release(self, e164: str) -> Dict[str, Any]:
48
+ """Release a number, cancel its monthly rental."""
49
+ return self._client._request(
50
+ "DELETE", f"/dialer/numbers/{quote(e164, safe='')}", timeout=60
51
+ )
52
+
53
+ def set_caller_id(self, e164: str) -> Dict[str, Any]:
54
+ """Set the default caller id for outbound calls."""
55
+ return self._client._request(
56
+ "PUT", "/dialer/caller-id", json={"caller_id": e164}, timeout=60
57
+ )
58
+
59
+
60
+ class DialerCallsResource:
61
+ def __init__(self, client: "Any") -> None:
62
+ self._client = client
63
+
64
+ def list(
65
+ self, limit: Optional[int] = None, offset: Optional[int] = None
66
+ ) -> Dict[str, Any]:
67
+ """Call history (GET /dialer/logs). limit: 1-200, offset."""
68
+ params: Dict[str, Any] = {}
69
+ if limit is not None:
70
+ params["limit"] = limit
71
+ if offset is not None:
72
+ params["offset"] = offset
73
+ return self._client._request("GET", "/dialer/logs", params=params, timeout=60)
74
+
75
+ def get(self, call_id: str) -> Dict[str, Any]:
76
+ """Get one call by id."""
77
+ return self._client._request(
78
+ "GET", f"/dialer/calls/{quote(call_id, safe='')}", timeout=60
79
+ )
80
+
81
+ def live(self) -> Dict[str, Any]:
82
+ """In-flight calls and recent CDRs."""
83
+ return self._client._request("GET", "/dialer/live", timeout=60)
84
+
85
+ def line_status(self) -> Dict[str, Any]:
86
+ """Carrier line status."""
87
+ return self._client._request("GET", "/dialer/calls/line-status", timeout=60)
88
+
89
+ def reserve(
90
+ self,
91
+ to: str,
92
+ from_number: str,
93
+ sip_call_id: str,
94
+ direction: str = "outbound",
95
+ ) -> Dict[str, Any]:
96
+ """
97
+ Reserve and authorize a call id. Does NOT dial.
98
+
99
+ The call is placed by a SIP client (softphone/PBX) registered with
100
+ the workspace's SIP credentials from the dashboard; this only
101
+ pre-authorizes the call id before the client sends its SIP INVITE.
102
+ """
103
+ body = {
104
+ "to": to,
105
+ "from": from_number,
106
+ "sip_call_id": sip_call_id,
107
+ "direction": direction,
108
+ }
109
+ return self._client._request("POST", "/dialer/calls", json=body, timeout=60)
110
+
111
+
112
+ class DialerRecordingsResource:
113
+ def __init__(self, client: "Any") -> None:
114
+ self._client = client
115
+
116
+ def list(self, **params: Any) -> Dict[str, Any]:
117
+ """List recordings. direction, status, e164, limit (1-200), offset."""
118
+ return self._client._request(
119
+ "GET", "/dialer/recordings", params=params, timeout=60
120
+ )
121
+
122
+ def playback_url(self, recording_id: str) -> Dict[str, Any]:
123
+ """Get a short-lived (~5 min) playback URL."""
124
+ return self._client._request(
125
+ "POST",
126
+ f"/dialer/recordings/{quote(recording_id, safe='')}/playback-url",
127
+ timeout=60,
128
+ )
129
+
130
+ def transcript(self, recording_id: str) -> Dict[str, Any]:
131
+ """Get (or generate) a recording's transcript."""
132
+ return self._client._request(
133
+ "POST",
134
+ f"/dialer/recordings/{quote(recording_id, safe='')}/transcript",
135
+ timeout=60,
136
+ )
137
+
138
+ def delete(self, recording_id: str) -> Dict[str, Any]:
139
+ """Queue a recording for deletion."""
140
+ return self._client._request(
141
+ "DELETE", f"/dialer/recordings/{quote(recording_id, safe='')}", timeout=60
142
+ )
143
+
144
+ def set_enabled(self, enabled: bool) -> Dict[str, Any]:
145
+ """Turn call recording on/off for outbound calls."""
146
+ return self._client._request(
147
+ "PUT", "/dialer/recording", json={"enabled": enabled}, timeout=60
148
+ )
149
+
150
+
151
+ class DialerBillingResource:
152
+ def __init__(self, client: "Any") -> None:
153
+ self._client = client
154
+
155
+ def subscriptions(self) -> Dict[str, Any]:
156
+ """List per-number subscriptions."""
157
+ return self._client._request("GET", "/dialer/subscriptions", timeout=60)
158
+
159
+ def usage(self, limit: Optional[int] = None) -> Dict[str, Any]:
160
+ """Balance and recent dialer charges. limit: 1-200 (default 50)."""
161
+ params: Dict[str, Any] = {}
162
+ if limit is not None:
163
+ params["limit"] = limit
164
+ return self._client._request(
165
+ "GET", "/dialer/usage", params=params, timeout=60
166
+ )
167
+
168
+
169
+ class DialerWebhooksResource:
170
+ def __init__(self, client: "Any") -> None:
171
+ self._client = client
172
+
173
+ def list(self) -> Dict[str, Any]:
174
+ """List webhooks, health and available event types."""
175
+ return self._client._request("GET", "/dialer/webhooks", timeout=60)
176
+
177
+ def create(self, **params: Any) -> Dict[str, Any]:
178
+ """
179
+ Create a webhook.
180
+
181
+ Required: name, url (https), event_types (non-empty list).
182
+ Optional: description, status, timeout_ms, rate_limit_per_minute,
183
+ max_attempts, max_event_age_seconds.
184
+
185
+ The response includes `signing_secret` once; store it.
186
+ """
187
+ return self._client._request("POST", "/dialer/webhooks", json=params, timeout=60)
188
+
189
+ def update(self, webhook_id: str, **params: Any) -> Dict[str, Any]:
190
+ """Partially update a webhook."""
191
+ return self._client._request(
192
+ "PATCH",
193
+ f"/dialer/webhooks/{quote(webhook_id, safe='')}",
194
+ json=params,
195
+ timeout=60,
196
+ )
197
+
198
+ def delete(self, webhook_id: str) -> Dict[str, Any]:
199
+ """Disable a webhook endpoint."""
200
+ return self._client._request(
201
+ "DELETE", f"/dialer/webhooks/{quote(webhook_id, safe='')}", timeout=60
202
+ )
203
+
204
+ def rotate_secret(self, webhook_id: str) -> Dict[str, Any]:
205
+ """Rotate the signing secret. Returns the new secret once."""
206
+ return self._client._request(
207
+ "POST",
208
+ f"/dialer/webhooks/{quote(webhook_id, safe='')}/rotate-secret",
209
+ timeout=60,
210
+ )
211
+
212
+ def test(self, webhook_id: str) -> Dict[str, Any]:
213
+ """Send a test event to the webhook."""
214
+ return self._client._request(
215
+ "POST", f"/dialer/webhooks/{quote(webhook_id, safe='')}/test", timeout=60
216
+ )
217
+
218
+ def deliveries(self, webhook_id: str) -> Dict[str, Any]:
219
+ """List recent delivery attempts."""
220
+ return self._client._request(
221
+ "GET", f"/dialer/webhooks/{quote(webhook_id, safe='')}/deliveries", timeout=60
222
+ )
223
+
224
+
225
+ class DialerResource:
226
+ """Dialer endpoints, available at `client.dialer`."""
227
+
228
+ def __init__(self, client: "Any") -> None:
229
+ self._client = client
230
+ self.numbers = DialerNumbersResource(client)
231
+ self.calls = DialerCallsResource(client)
232
+ self.recordings = DialerRecordingsResource(client)
233
+ self.billing = DialerBillingResource(client)
234
+ self.webhooks = DialerWebhooksResource(client)
235
+
236
+ def status(self) -> Dict[str, Any]:
237
+ """Dialer provisioning status for the workspace."""
238
+ return self._client._request("GET", "/dialer/status", timeout=60)
sixtydb/judge.py ADDED
@@ -0,0 +1,214 @@
1
+ """
2
+ Judge resource for the 60db API.
3
+
4
+ Ask a pinned classifier a question about your content. It never generates
5
+ text: every answer is a probability distribution over an answer space you
6
+ defined, which is what makes results comparable and sortable in a way prose
7
+ never is.
8
+
9
+ Two shapes:
10
+ * evaluate() — a rubric of up to 32 questions asked about one shared
11
+ `state` (a transcript, a ticket, a generated answer).
12
+ * extract() — one conversational turn labelled with an intent and an
13
+ operation, plus entity spans as Unicode code-point offsets.
14
+
15
+ Billed per input token. The upstream re-encodes the shared state once per
16
+ question, so a 10-question rubric costs ten times the transcript — keep
17
+ rubrics tight on long content.
18
+ """
19
+
20
+ from typing import Any, Dict, Optional
21
+ from urllib.parse import quote
22
+
23
+
24
+ class JudgeRubricsResource:
25
+ """Saved rubrics — build once, re-run by id. `client.judge.rubrics`."""
26
+
27
+ def __init__(self, client: "Any") -> None:
28
+ self._client = client
29
+
30
+ def list(self) -> Any:
31
+ """Your own rubrics plus every rubric shared with the workspace."""
32
+ return self._client._request("GET", "/judge/rubrics")["data"]
33
+
34
+ def create(
35
+ self,
36
+ name: str,
37
+ questions: Dict[str, Any],
38
+ description: Optional[str] = None,
39
+ model: Optional[str] = None,
40
+ shared: bool = False,
41
+ ) -> Dict[str, Any]:
42
+ """
43
+ Save a rubric.
44
+
45
+ Args:
46
+ name: Unique per workspace.
47
+ questions: Map of answer key -> question. Each question is
48
+ {"type": "choice"|"score"|"noul", "instructions": ..., "criteria": ...}.
49
+ shared: Publish to the whole workspace. Owner/admin only.
50
+ """
51
+ body: Dict[str, Any] = {"name": name, "questions": questions, "shared": shared}
52
+ if description is not None:
53
+ body["description"] = description
54
+ if model is not None:
55
+ body["model"] = model
56
+ return self._client._request("POST", "/judge/rubrics", json=body)["data"]
57
+
58
+ def get(self, rubric_id: str) -> Dict[str, Any]:
59
+ return self._client._request("GET", f"/judge/rubrics/{quote(rubric_id)}")["data"]
60
+
61
+ def update(self, rubric_id: str, **params: Any) -> Dict[str, Any]:
62
+ """Patch only the fields you pass — the rest are left alone."""
63
+ return self._client._request("PATCH", f"/judge/rubrics/{quote(rubric_id)}", json=params)["data"]
64
+
65
+ def delete(self, rubric_id: str) -> None:
66
+ """Soft delete. Past runs keep their link, so history stays readable."""
67
+ self._client._request("DELETE", f"/judge/rubrics/{quote(rubric_id)}")
68
+
69
+
70
+ class JudgeRunsResource:
71
+ """Run history and the review queue. `client.judge.runs`."""
72
+
73
+ def __init__(self, client: "Any") -> None:
74
+ self._client = client
75
+
76
+ def list(
77
+ self,
78
+ limit: int = 25,
79
+ offset: int = 0,
80
+ kind: Optional[str] = None,
81
+ rubric_id: Optional[str] = None,
82
+ needs_review: bool = False,
83
+ ) -> Dict[str, Any]:
84
+ """
85
+ Saved runs, newest first.
86
+
87
+ Args:
88
+ kind: "evaluate" or "extract".
89
+ needs_review: Only runs the judge was under 85% sure about —
90
+ the upstream's own escalation line, and the queue worth a
91
+ person's attention.
92
+
93
+ Returns:
94
+ {"runs": [...], "pagination": {...}}
95
+ """
96
+ params: Dict[str, Any] = {"limit": limit, "offset": offset}
97
+ if kind:
98
+ params["kind"] = kind
99
+ if rubric_id:
100
+ params["rubric_id"] = rubric_id
101
+ if needs_review:
102
+ params["needs_review"] = "true"
103
+ body = self._client._request("GET", "/judge/evaluations", params=params)
104
+ return {"runs": body["data"], "pagination": body.get("pagination", {})}
105
+
106
+ def get(self, run_id: str) -> Dict[str, Any]:
107
+ """One run including exactly what was sent and what came back."""
108
+ return self._client._request("GET", f"/judge/evaluations/{quote(run_id)}")["data"]
109
+
110
+ def delete(self, run_id: str) -> None:
111
+ self._client._request("DELETE", f"/judge/evaluations/{quote(run_id)}")
112
+
113
+
114
+ class JudgeResource:
115
+ """Judge endpoints, available at `client.judge`."""
116
+
117
+ def __init__(self, client: "Any") -> None:
118
+ self._client = client
119
+ self.rubrics = JudgeRubricsResource(client)
120
+ self.runs = JudgeRunsResource(client)
121
+
122
+ def evaluate(
123
+ self,
124
+ state: Any,
125
+ questions: Optional[Dict[str, Any]] = None,
126
+ rubric_id: Optional[str] = None,
127
+ model: Optional[str] = None,
128
+ save: bool = True,
129
+ label: Optional[str] = None,
130
+ ) -> Dict[str, Any]:
131
+ """
132
+ Run a rubric over content.
133
+
134
+ Args:
135
+ state: What every question is asked about — text, or any JSON
136
+ structure. Each question sees this and nothing else.
137
+ questions: Inline rubric. Pass this OR rubric_id, never both.
138
+ rubric_id: Run a saved rubric instead.
139
+ save: False bills the run but keeps it out of history.
140
+
141
+ Returns:
142
+ {"answers": {...}, "summary": {...}, "min_confidence": float|None,
143
+ "usage": {...}, "credits_charged": float, ...}
144
+
145
+ Raises:
146
+ requests.HTTPError: 400 invalid rubric, 402 out of credits,
147
+ 429 judge busy, 503 unavailable. A failed run is refunded
148
+ automatically — nothing is charged for an answer you did
149
+ not get.
150
+ """
151
+ body: Dict[str, Any] = {"state": state}
152
+ if questions is not None:
153
+ body["questions"] = questions
154
+ if rubric_id is not None:
155
+ body["rubric_id"] = rubric_id
156
+ if model is not None:
157
+ body["model"] = model
158
+ if label is not None:
159
+ body["label"] = label
160
+ if not save:
161
+ body["save"] = False
162
+ return self._client._request("POST", "/judge/evaluate", json=body)["data"]
163
+
164
+ def extract(
165
+ self,
166
+ text: str,
167
+ schema: Dict[str, Any],
168
+ profile: Optional[str] = None,
169
+ budget_ms: Optional[int] = None,
170
+ save: bool = True,
171
+ label: Optional[str] = None,
172
+ ) -> Dict[str, Any]:
173
+ """
174
+ Classify one conversational turn and pull its entity spans.
175
+
176
+ Args:
177
+ text: The turn. Max 2,048 Unicode code points.
178
+ schema: {"intents": {...}, "operations": {...}, "entities": {...},
179
+ and optionally "responsePaths": {...}}. Each is a map of label
180
+ name -> description; the description is what the model reads.
181
+ Supplying responsePaths selects the v2 model.
182
+ profile: "generic" (default) or "medical".
183
+
184
+ Note:
185
+ Entity `start`/`end` are Unicode CODE POINT offsets, not Python
186
+ string indices for astral characters. Slice with
187
+ ``list(text)[start:end]`` if the turn may contain emoji.
188
+ """
189
+ body: Dict[str, Any] = {"text": text, "schema": schema}
190
+ if profile is not None:
191
+ body["profile"] = profile
192
+ if budget_ms is not None:
193
+ body["budget_ms"] = budget_ms
194
+ if label is not None:
195
+ body["label"] = label
196
+ if not save:
197
+ body["save"] = False
198
+ return self._client._request("POST", "/judge/extract", json=body)["data"]
199
+
200
+ def models(self) -> Dict[str, Any]:
201
+ """Model names evaluate() accepts. Authoritative — do not hardcode."""
202
+ return self._client._request("GET", "/judge/models")["data"]
203
+
204
+ def usage(self, period: str = "current_month") -> Dict[str, Any]:
205
+ """Net JUDGE_* spend and saved-run counts.
206
+
207
+ Args:
208
+ period: "current_month", "last_30_days" or "all_time".
209
+ """
210
+ return self._client._request("GET", "/judge/usage", params={"period": period})["data"]
211
+
212
+ def health(self) -> Dict[str, Any]:
213
+ """Upstream readiness, model revisions, and which upstream is in use."""
214
+ return self._client._request("GET", "/judge/health")["data"]
sixtydb/music.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ Music (Songs) resource for the 60db API.
3
+
4
+ Free during beta. Each create() call makes 1 song; only one generation
5
+ runs at a time per workspace (a second create while one is running
6
+ returns HTTP 429).
7
+ """
8
+
9
+ import os
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+ from urllib.parse import quote
13
+
14
+
15
+ class MusicResource:
16
+ """Music (song generation) endpoints, available at `client.music`."""
17
+
18
+ def __init__(self, client: "Any") -> None:
19
+ self._client = client
20
+
21
+ def create(
22
+ self,
23
+ prompt: str,
24
+ lyrics_file: Optional[Any] = None,
25
+ **params: Any,
26
+ ) -> Dict[str, Any]:
27
+ """
28
+ Create a song (async, returns immediately with a "submitted" song).
29
+
30
+ Args:
31
+ prompt: Style (advanced mode) or description (simple mode). Required.
32
+ lyrics_file: Optional path or file-like object for a .txt lyrics
33
+ upload (sent as multipart, replaces `lyrics`).
34
+ **params: mode, lyrics, lyrics_prompt, instrumental, vocal_gender,
35
+ voice_id, negative_tags, tags, target_duration, seed (string),
36
+ title.
37
+ """
38
+ data: Dict[str, Any] = {"prompt": prompt, **params}
39
+
40
+ if lyrics_file is not None:
41
+ opened = False
42
+ if isinstance(lyrics_file, str):
43
+ file_obj = open(lyrics_file, "rb")
44
+ filename = os.path.basename(lyrics_file)
45
+ opened = True
46
+ else:
47
+ file_obj = lyrics_file
48
+ filename = getattr(lyrics_file, "name", "lyrics.txt")
49
+
50
+ try:
51
+ files = {"lyrics_file": (filename, file_obj, "text/plain")}
52
+ # Multipart requests must not send the session's default
53
+ # JSON content-type header.
54
+ headers = {"Authorization": f"Bearer {self._client.api_key}"}
55
+ import requests
56
+
57
+ def _stringify(v: Any) -> str:
58
+ if isinstance(v, bool):
59
+ return "true" if v else "false"
60
+ return str(v)
61
+
62
+ response = requests.post(
63
+ f"{self._client.base_url}/songs",
64
+ headers=headers,
65
+ data={k: _stringify(v) for k, v in data.items() if v is not None},
66
+ files=files,
67
+ timeout=60,
68
+ )
69
+ response.raise_for_status()
70
+ return response.json()
71
+ finally:
72
+ if opened:
73
+ file_obj.close()
74
+
75
+ return self._client._request("POST", "/songs", json=data, timeout=60)
76
+
77
+ def list(self, **params: Any) -> Dict[str, Any]:
78
+ """List songs (paged, newest first). limit, offset, q, status, type, liked, sort."""
79
+ return self._client._request("GET", "/songs", params=params, timeout=60)
80
+
81
+ def get(self, song_id: str) -> Dict[str, Any]:
82
+ """Get one song (SongDetail, includes lyrics)."""
83
+ return self._client._request(
84
+ "GET", f"/songs/{quote(song_id, safe='')}", timeout=60
85
+ )
86
+
87
+ def update(
88
+ self,
89
+ song_id: str,
90
+ liked: Optional[bool] = None,
91
+ played: Optional[bool] = None,
92
+ ) -> Dict[str, Any]:
93
+ """Update flags on a song. At least one of liked/played is required."""
94
+ body: Dict[str, Any] = {}
95
+ if liked is not None:
96
+ body["liked"] = liked
97
+ if played is not None:
98
+ body["played"] = played
99
+ return self._client._request(
100
+ "PATCH", f"/songs/{quote(song_id, safe='')}", json=body, timeout=60
101
+ )
102
+
103
+ def delete(self, song_id: str) -> Dict[str, Any]:
104
+ """Move a song to trash (soft delete)."""
105
+ return self._client._request(
106
+ "DELETE", f"/songs/{quote(song_id, safe='')}", timeout=60
107
+ )
108
+
109
+ def download(self, song_id: str) -> bytes:
110
+ """Download the MP3 file for a song. Returns raw bytes."""
111
+ result = self._client._request(
112
+ "GET", f"/songs/{quote(song_id, safe='')}/download", timeout=120
113
+ )
114
+ if isinstance(result, bytes):
115
+ return result
116
+ raise Exception("Expected binary audio response for song download")
117
+
118
+ def wait_for_completion(
119
+ self, song_id: str, interval: float = 4, timeout: float = 600
120
+ ) -> Dict[str, Any]:
121
+ """
122
+ Poll GET /songs/{id} until status is "succeeded" or "failed".
123
+
124
+ Returns the unwrapped song object. Raises TimeoutError on timeout.
125
+ """
126
+ started_at = time.time()
127
+ while True:
128
+ response = self.get(song_id)
129
+ song = response.get("data", response) if isinstance(response, dict) else response
130
+ status = song.get("status") if isinstance(song, dict) else None
131
+ if status in ("succeeded", "failed"):
132
+ return song
133
+ if time.time() - started_at >= timeout:
134
+ raise TimeoutError(f"Timed out waiting for song {song_id} to complete")
135
+ time.sleep(interval)
136
+
137
+ def list_voices(self) -> Dict[str, Any]:
138
+ """List voices usable for song creation: `{catalog, mine}`."""
139
+ return self._client._request("GET", "/songs/voices", timeout=60)
140
+
141
+ def create_voice(self, name: str, file: Any) -> Dict[str, Any]:
142
+ """
143
+ Save a voice from an audio clip (multipart).
144
+
145
+ Args:
146
+ name: Voice name (<=200 chars).
147
+ file: Path or file-like object for the reference audio
148
+ (wav/flac/mp3/m4a/ogg, 3-30s, <=10MB).
149
+ """
150
+ opened = False
151
+ if isinstance(file, str):
152
+ file_obj = open(file, "rb")
153
+ filename = os.path.basename(file)
154
+ opened = True
155
+ else:
156
+ file_obj = file
157
+ filename = getattr(file, "name", "voice.wav")
158
+
159
+ try:
160
+ import requests
161
+
162
+ files = {"voice_reference": (filename, file_obj, "audio/wav")}
163
+ data = {"name": name, "consent": "true"}
164
+ headers = {"Authorization": f"Bearer {self._client.api_key}"}
165
+ response = requests.post(
166
+ f"{self._client.base_url}/songs/voices",
167
+ headers=headers,
168
+ data=data,
169
+ files=files,
170
+ timeout=60,
171
+ )
172
+ response.raise_for_status()
173
+ return response.json()
174
+ finally:
175
+ if opened:
176
+ file_obj.close()
177
+
178
+ def health(self) -> Dict[str, Any]:
179
+ """Song generation service status: `{ready, status}`."""
180
+ return self._client._request("GET", "/songs/health", timeout=60)
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: sixtydb
3
+ Version: 1.1.0
4
+ Summary: Official 60db SDK for Python
5
+ Author: 60db
6
+ License: MIT
7
+ Keywords: 60db,tts,stt,voice,ai
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.28.0
19
+ Requires-Dist: typing-extensions>=4.0.0
20
+
21
+ # 60db
22
+
23
+ Official 60db SDK for Python
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install sixtydb
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```python
34
+ from sixtydb import SixtyDBClient
35
+
36
+
37
+ def fetch_and_display_voices(client: SixtyDBClient) -> str:
38
+ """
39
+ Fetch all available voices and print their details.
40
+
41
+ Returns:
42
+ str: Selected voice_id
43
+ """
44
+ try:
45
+ response = client.get_voices()
46
+ except Exception as exc:
47
+ print("Error fetching voices:", exc)
48
+ raise
49
+
50
+ if not response.get("success"):
51
+ raise Exception(response.get("message"))
52
+
53
+ voices = response["data"]["built_in_voices"]
54
+
55
+ if not voices:
56
+ raise Exception("No voices available.")
57
+
58
+ print("\nAvailable Voices:\n" + "-" * 40)
59
+
60
+ for voice in voices:
61
+ print(
62
+ f"Name: {voice.get('name')} | "
63
+ f"Voice ID: {voice.get('voice_id')} | "
64
+ f"Language: {voice.get('language_name')} | "
65
+ f"Gender: {voice.get('gender')}"
66
+ )
67
+
68
+ return voices[0]["voice_id"]
69
+
70
+
71
+ def generate_speech(client: SixtyDBClient, voice_id: str) -> None:
72
+ """
73
+ Generate speech and save to file.
74
+ """
75
+ try:
76
+ audio = client.text_to_speech(
77
+ text="Hello, world!",
78
+ voice_id=voice_id,
79
+ speed=1.0
80
+ )
81
+
82
+ with open("output.mp3", "wb") as file:
83
+ file.write(audio)
84
+
85
+ print("\nAudio saved to output.mp3")
86
+
87
+ except Exception as exc:
88
+ print("Error generating speech:", exc)
89
+ raise
90
+
91
+
92
+ def main() -> None:
93
+ try:
94
+ client = SixtyDBClient("your-api-key")
95
+ except Exception as exc:
96
+ print("Error creating client:", exc)
97
+ return
98
+
99
+ try:
100
+ voice_id = fetch_and_display_voices(client)
101
+ generate_speech(client, voice_id)
102
+ except Exception as exc:
103
+ print("Operation failed:", exc)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## API Methods
113
+
114
+ ### Text-to-Speech
115
+ - `text_to_speech(text, voice_id=None, **params)` – Convert text to speech
116
+
117
+ ### Speech-to-Text
118
+ - `speech_to_text(audio_file, language=None)` – Transcribe audio
119
+ - `get_languages()` – Get supported languages
120
+
121
+ ### Voices
122
+ - `get_voices()` – List all voices
123
+
124
+ ### Music
125
+ Free during beta (no credit check, nothing charged). Each `create()` call
126
+ makes **1 song**, and only **one generation runs at a time per workspace**
127
+ (a second `create()` while one is running returns 429). A song takes about
128
+ 1.5-2.5 minutes.
129
+ - `music.create(prompt, lyrics_file=None, **params)` – Create a song (async)
130
+ - `music.list(**params)` – List songs (limit, offset, q, status, type, liked, sort)
131
+ - `music.get(song_id)` – Get a song (includes lyrics)
132
+ - `music.update(song_id, liked=None, played=None)` – Update flags
133
+ - `music.delete(song_id)` – Move a song to trash
134
+ - `music.download(song_id)` – Download the MP3 (bytes)
135
+ - `music.wait_for_completion(song_id, interval=4, timeout=600)` – Poll until succeeded/failed
136
+ - `music.list_voices()` – List catalog + saved voices
137
+ - `music.create_voice(name, file)` – Save a voice from an audio clip
138
+ - `music.health()` – Song service status
139
+
140
+ ### Judge
141
+
142
+ Scores content against a rubric you define, and parses conversational turns.
143
+ A classifier, not a generator — answers are probability distributions over
144
+ your own options.
145
+
146
+ ```python
147
+ run = client.judge.evaluate(
148
+ state=call_transcript,
149
+ questions={
150
+ "tone": {"type": "choice",
151
+ "criteria": {"professional": "Calm", "dismissive": "Brushes off",
152
+ "unknown": "Not clear"}}, # always add an escape hatch
153
+ "satisfaction": {"type": "score", "criteria": ["Angry", "Neutral", "Happy"]},
154
+ "resolved": {"type": "noul"},
155
+ },
156
+ )
157
+ run["answers"]["tone"]["choice"] # 'dismissive'
158
+ run["answers"]["satisfaction"]["score"] # 1.87 — weighted, not an index
159
+ run["answers"]["resolved"]["noul"] # 0.28 — probability of true, not a confidence
160
+ run["min_confidence"] # under 0.85 → send to a human
161
+
162
+ rubric = client.judge.rubrics.create(name="Call QA", questions=questions)
163
+ client.judge.evaluate(state=transcript, rubric_id=rubric["id"])
164
+
165
+ turn = client.judge.extract(
166
+ text="move my 10am appointment to friday",
167
+ schema={"intents": {"booking": "Wants to change a booking"},
168
+ "operations": {"reschedule": "Move it"},
169
+ "entities": {"date": "A calendar date"}},
170
+ )
171
+ turn["intent"]["label"] # 'booking'
172
+ turn["entities"] # [{'label': 'date', 'text': 'friday', 'start': 28, 'end': 34, ...}]
173
+
174
+ client.judge.runs.list(needs_review=True) # the low-confidence review queue
175
+ client.judge.models() # authoritative model list
176
+ client.judge.usage("current_month") # spend and run counts
177
+ ```
178
+
179
+ **Entity offsets are Unicode code points** — slice `list(text)[start:end]`,
180
+ not `text[start:end]`, or an emoji earlier in the turn shifts every span.
181
+
182
+ **Pricing:** per input token; the shared state is re-encoded once per
183
+ question. Context is 8K tokens **per question**. Failed runs are refunded.
184
+
185
+ ### Dialer
186
+ Not free: a number costs **$6.00/month**, calls cost **$0.003/min**. Most
187
+ calls need a finished trunk setup (dashboard) and approved dialer KYC
188
+ (also done in the dashboard, not exposed by this SDK). `calls.reserve()`
189
+ does **not** dial — it only pre-authorizes a call id for a SIP client
190
+ (softphone/PBX) to use.
191
+ - `dialer.status()` – Provisioning status
192
+ - `dialer.numbers.search()` / `.buy(number=None, **params)` / `.list()` / `.release(e164)` / `.set_caller_id(e164)` – `buy()` requires approved KYC done in the 60db app, otherwise 403 `DIALER_KYC_REQUIRED`/`DIALER_KYC_PENDING`/`DIALER_KYC_REJECTED`; charges $6.00 now, then $6.00/month
193
+ - `dialer.calls.list(limit=None, offset=None)` / `.get(call_id)` / `.live()` / `.line_status()` / `.reserve(to, from_number, sip_call_id, direction="outbound")` – `reserve()` does not dial
194
+ - `dialer.recordings.list(**params)` / `.playback_url(id)` / `.transcript(id)` / `.delete(id)` / `.set_enabled(enabled)`
195
+ - `dialer.billing.subscriptions()` / `.usage(limit=None)`
196
+ - `dialer.webhooks.list()` / `.create(**params)` / `.update(id, **params)` / `.delete(id)` / `.rotate_secret(id)` / `.test(id)` / `.deliveries(id)`
197
+
198
+ ---
199
+
200
+ ## Example: Speech to Text
201
+
202
+ ```python
203
+ from sixtydb import SixtyDBClient
204
+
205
+ client = SixtyDBClient("your-api-key")
206
+
207
+ try:
208
+ with open("output.mp3", "rb") as file:
209
+ result = client.speech_to_text(file, language="en")
210
+
211
+ if result.get("success"):
212
+ print("Transcription:", result["data"]["text"])
213
+ else:
214
+ print("STT Error:", result.get("message"))
215
+
216
+ except Exception as exc:
217
+ print("Error during transcription:", exc)
218
+ ```
219
+
220
+ ---
221
+
222
+ ## License
223
+
224
+ MIT
@@ -0,0 +1,9 @@
1
+ sixtydb/__init__.py,sha256=dxgyfVT4AiNQx2PFh7GOiLA7n9tO5sqmvUHqEolCf7g,709
2
+ sixtydb/client.py,sha256=YqyYQLDIjEHr3AjrDlexuDdcatAWhyG4eji6ClpIoe0,10780
3
+ sixtydb/dialer.py,sha256=Nadla0LijLUGIgqsjhcakNXpOq4_b9ig3JBDA0pYgwE,8643
4
+ sixtydb/judge.py,sha256=g9bJlEXX7aX0JQ8KOk0-KYC5lArvOqBrJNqjDNKixIA,8065
5
+ sixtydb/music.py,sha256=bjmJCtPl3JK74Y7jUHyGcRUy2lKyqQHPQIDFJ-3DerA,6604
6
+ sixtydb-1.1.0.dist-info/METADATA,sha256=67mJpEhMrO66auQoQve2g78zpapfeFnEHG4pTeZrbZ0,7356
7
+ sixtydb-1.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ sixtydb-1.1.0.dist-info/top_level.txt,sha256=oTqInjPZSaLsdxC7ZTMhDgJNYQmLpO8nyMbLglr7z4I,8
9
+ sixtydb-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sixtydb