Metadata-Version: 2.4
Name: sixtydb
Version: 1.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.

### 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)`

### Realtime (OpenAI-compatible)
The realtime socket (`wss://api.60db.ai/v1/realtime`) speaks the OpenAI
Realtime GA protocol, so the official `openai` package works unchanged —
just point it at 60db:

```python
from openai import AsyncOpenAI

oai = AsyncOpenAI(api_key="sk_...", base_url="https://api.60db.ai/v1")
async with oai.realtime.connect(model="60db-realtime-v1-mini") as conn:
    await conn.session.update(session={"type": "realtime", "output_modalities": ["text"]})
    await conn.conversation.item.create(item={"type": "message", "role": "user",
        "content": [{"type": "input_text", "text": "Hello"}]})
    await conn.response.create()
    async for event in conn:
        ...
```

For browsers, mint a short-lived `ek_` key server-side (never ship your `sk_` key):

```python
secret = client.realtime.create_client_secret(expires_in_seconds=600, session={...})
# secret["value"] -> "ek_...", secret["expires_at"]
```

- `realtime.create_client_secret(expires_in_seconds=None, session=None)` – 10-7200s, default 600
- `realtime.realtime_url(model="60db-realtime-v1-mini")` – WebSocket URL
- 60db session extensions: `memory` (`bool` or `{enabled, persist}`), `knowledge` (`{collections: [...]}`, max 5), `intent` (`{filler: True, hints: False}`), `audio.input.transcription.language` (str or list, default en+hi)

**Models/pricing:** `60db-realtime-v1-mini` — $0.02/connected minute + LLM tokens at $0.50/1M. `gpt-realtime*` aliases to it; any 60db voice id or OpenAI voice name works.

**Phone agents (beta; SIP calling not live yet):**
- `realtime.agents.list()` / `.get(id)` / `.create(name, **params)` / `.update(id, **params)` / `.delete(id)`
- `realtime.agents.set_numbers(id, ["+14155550100"])` / `.rotate_webhook_secret(id)`
- `create()` params: instructions, voice, languages, greeting, builtin_tools (`transferCall`/`endCall`/`sendDtmf`), tools, webhook_url, transfer_targets, max_duration_s (30-7200), recording. `webhook_secret` is returned only on create/rotate.

---

## 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
