Metadata-Version: 2.4
Name: sixtydb
Version: 2.2.0
Summary: Official 60db SDK for Python
Author: 60db
License: MIT
Keywords: 60db,tts,stt,voice,ai
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28.0
Requires-Dist: typing-extensions>=4.0.0

# 60db

Official 60db SDK for Python

## Installation

```bash
pip install sixtydb
```

## Quick Start

```python
from sixtydb import SixtyDBClient


def fetch_and_display_voices(client: SixtyDBClient) -> str:
    """
    Fetch all available voices and print their details.

    Returns:
        str: Selected voice_id
    """
    try:
        response = client.get_voices()
    except Exception as exc:
        print("Error fetching voices:", exc)
        raise

    if not response.get("success"):
        raise Exception(response.get("message"))

    voices = response["data"]["built_in_voices"]

    if not voices:
        raise Exception("No voices available.")

    print("\nAvailable Voices:\n" + "-" * 40)

    for voice in voices:
        print(
            f"Name: {voice.get('name')} | "
            f"Voice ID: {voice.get('voice_id')} | "
            f"Language: {voice.get('language_name')} | "
            f"Gender: {voice.get('gender')}"
        )

    return voices[0]["voice_id"]


def generate_speech(client: SixtyDBClient, voice_id: str) -> None:
    """
    Generate speech and save to file.
    """
    try:
        audio = client.text_to_speech(
            text="Hello, world!",
            voice_id=voice_id,
            speed=1.0
        )

        with open("output.mp3", "wb") as file:
            file.write(audio)

        print("\nAudio saved to output.mp3")

    except Exception as exc:
        print("Error generating speech:", exc)
        raise


def main() -> None:
    try:
        client = SixtyDBClient("your-api-key")
    except Exception as exc:
        print("Error creating client:", exc)
        return

    try:
        voice_id = fetch_and_display_voices(client)
        generate_speech(client, voice_id)
    except Exception as exc:
        print("Operation failed:", exc)


if __name__ == "__main__":
    main()
```

---

## API Methods

### Text-to-Speech
- `text_to_speech(text, voice_id=None, **params)` – Convert text to speech

### Speech-to-Text
- `speech_to_text(audio_file, language=None)` – Transcribe audio
- `get_languages()` – Get supported languages

### Voices
- `get_voices()` – List all voices

### Music
Free during beta (no credit check, nothing charged). Each `create()` call
makes **1 song**, and only **one generation runs at a time per workspace**
(a second `create()` while one is running returns 429). A song takes about
1.5-2.5 minutes.
- `music.create(prompt, lyrics_file=None, **params)` – Create a song (async)
- `music.list(**params)` – List songs (limit, offset, q, status, type, liked, sort)
- `music.get(song_id)` – Get a song (includes lyrics)
- `music.update(song_id, liked=None, played=None)` – Update flags
- `music.delete(song_id)` – Move a song to trash
- `music.download(song_id)` – Download the MP3 (bytes)
- `music.wait_for_completion(song_id, interval=4, timeout=600)` – Poll until succeeded/failed
- `music.list_voices()` – List catalog + saved voices
- `music.create_voice(name, file)` – Save a voice from an audio clip
- `music.health()` – Song service status

### Judge

Scores content against a rubric you define, and parses conversational turns.
A classifier, not a generator — answers are probability distributions over
your own options.

```python
run = client.judge.evaluate(
    state=call_transcript,
    questions={
        "tone": {"type": "choice",
                 "criteria": {"professional": "Calm", "dismissive": "Brushes off",
                              "unknown": "Not clear"}},        # always add an escape hatch
        "satisfaction": {"type": "score", "criteria": ["Angry", "Neutral", "Happy"]},
        "resolved": {"type": "noul"},
    },
)
run["answers"]["tone"]["choice"]           # 'dismissive'
run["answers"]["satisfaction"]["score"]    # 1.87 — weighted, not an index
run["answers"]["resolved"]["noul"]         # 0.28 — probability of true, not a confidence
run["min_confidence"]                      # under 0.85 → send to a human

rubric = client.judge.rubrics.create(name="Call QA", questions=questions)
client.judge.evaluate(state=transcript, rubric_id=rubric["id"])

turn = client.judge.extract(
    text="move my 10am appointment to friday",
    schema={"intents": {"booking": "Wants to change a booking"},
            "operations": {"reschedule": "Move it"},
            "entities": {"date": "A calendar date"}},
)
turn["intent"]["label"]   # 'booking'
turn["entities"]          # [{'label': 'date', 'text': 'friday', 'start': 28, 'end': 34, ...}]

client.judge.runs.list(needs_review=True)   # the low-confidence review queue
client.judge.models()                       # authoritative model list
client.judge.usage("current_month")         # spend and run counts
```

**Entity offsets are Unicode code points** — slice `list(text)[start:end]`,
not `text[start:end]`, or an emoji earlier in the turn shifts every span.

**Pricing:** per input token; the shared state is re-encoded once per
question. Context is 8K tokens **per question**. Failed runs are refunded.

### Agents (phone voice agents)
Calls ring real phones and cost money: speech/LLM/knowledge-base use bills your
AI credits; line minutes on a 60db number bill your Dialer balance. Methods
return the API's `data` and raise `SixtyDBAgentsError` (`code`, `field`, `status`).
- `agents.create(name, script, voice_id, **fields)` – returns the agent plus `webhook_secret` (shown once)
  - call transfer: `transfer_targets=[{"name": "Sales", "numbers": ["+919800000001"]}]` (up to 10 targets of 1–10 E.164 numbers each, tried in turn; `[]` turns transfer off). Also accepted in `overrides` of `call()` / `stream_call()`
- `agents.list()` / `.get(id)` / `.update(id, **fields)` / `.delete(id)` / `.rotate_webhook_secret(id)` / `.tool_events(id)` (→ `{events}`)
- `agents.call(agent_id, to, phone_line_id, overrides=None, metadata=None)` – places a real call
- `agents.stream_call(agent_id, overrides=None, metadata=None)` – a call over a WebSocket, no phone line; returns `{call, url, expires_in}`
- `agents.calls.list(agent_id=None, status=None, ...)` / `.get(id)` / `.transcript(id)` / `.recording(id, mix=False)` / `.save_recording(id, path)` / `.delete_recording(id)` / `.wait_for_completion(id)`
- `agents.phone_lines.list()` / `.available_numbers()` / `.create_from_number(number, ...)` / `.create_byo(name, domain, username, password, caller_id, ...)` / `.update(id, **fields)` / `.delete(id)`
- `verify_webhook_signature(raw_body, signature, timestamp, secret)` – check `X-60db-Signature` on tool calls and events

```python
agent = client.agents.create(name="Front desk", script="You are the receptionist...", voice_id="YOUR_VOICE_ID",
                             listen_languages=["hi", "en"], call_defaults={"record": True})
free = [n for n in client.agents.phone_lines.available_numbers()["numbers"] if not n["in_use"]]
line = client.agents.phone_lines.create_from_number(free[0]["number"])
call = client.agents.call(agent["id"], to="+919876543210", phone_line_id=line["id"])
done = client.agents.calls.wait_for_completion(call["id"])
print(done["status"], done["duration_secs"])
```

**Stream calls (WebSocket, no phone line).** Connect call audio you already carry over a WebSocket (a PBX,
Twilio, Exotel, Klicko or your own app). Call `stream_call` from your server and hand the one-time `url`
(single use, 55 s, no API key in it) to whatever streams the audio. Send `{"event": "start", "format": "mulaw"}`
within 10 s, then 20 ms binary frames of 8 kHz audio; or send your usual Twilio Media Streams `connected` and
`start` with `mediaFormat` and base64 `media` events.

```python
out = client.agents.stream_call(agent["id"], metadata={"lead_id": "L-1042"})
print(out["url"], out["expires_in"])     # wss://api.60db.ai/ws/agents/stream/…?token=…, 55
# After the call: client.agents.calls.get(out["call"]["id"])["analytics"]
```

See `examples/agents_quickstart.py` and `examples/agents_tool_webhook_server.py`.

### Dialer
Not free: a number costs **$6.00/month**, calls cost **$0.003/min**. Most
calls need a finished trunk setup (dashboard) and approved dialer KYC
(also done in the dashboard, not exposed by this SDK). `calls.reserve()`
does **not** dial — it only pre-authorizes a call id for a SIP client
(softphone/PBX) to use.
- `dialer.status()` – Provisioning status
- `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
- `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
- `dialer.recordings.list(**params)` / `.playback_url(id)` / `.transcript(id)` / `.delete(id)` / `.set_enabled(enabled)`
- `dialer.billing.subscriptions()` / `.usage(limit=None)`
- `dialer.webhooks.list()` / `.create(**params)` / `.update(id, **params)` / `.delete(id)` / `.rotate_secret(id)` / `.test(id)` / `.deliveries(id)`

---

## Example: Speech to Text

```python
from sixtydb import SixtyDBClient

client = SixtyDBClient("your-api-key")

try:
    with open("output.mp3", "rb") as file:
        result = client.speech_to_text(file, language="en")

    if result.get("success"):
        print("Transcription:", result["data"]["text"])
    else:
        print("STT Error:", result.get("message"))

except Exception as exc:
    print("Error during transcription:", exc)
```

---

## License

MIT
