Tileward / Docs / Voice
Docs · Voice

A voice call is one WebSocket.

Tileward PersonaPlex 7B listens and talks at the same time. Open a socket with your API key, send your microphone, and hear it answer.

In the console

Hear it before you write a line.

The console's call page puts you on a call from the browser: pick a voice, say who it is, and talk. It is the same endpoint and the same meter as the API.

Open the call page

Connect

One address, your key in a header.

 Value
Addresswss://api.tileward.com/v1/voice
KeyAuthorization: Bearer tw_live_..., or the same key in x-api-key
voiceOne of 18 voices: NATF0 to NATF3, NATM0 to NATM3, VARF0 to VARF4, VARM0 to VARM4. Optional, NATF2 if you leave it out.
promptWho the voice is, up to 2,000 characters. Optional.

voice and prompt go in the query string. A browser cannot set a header on a WebSocket, so open the socket from your own server and keep the key there. A key holds one call at a time.

Get a key from the console under Settings → API keys.

# pip install "websockets>=14"
import asyncio, os
from urllib.parse import urlencode
import websockets

query = urlencode({
    "voice": "NATF2",
    "prompt": "You work for Acme Dental which is a dental clinic and your name is Sam.",
})
url = f"wss://api.tileward.com/v1/voice?{query}"
headers = {"Authorization": f"Bearer {os.environ['TILEWARD_API_KEY']}"}

async def talk(ws):
    async for page in microphone():  # yours: Ogg/Opus pages, 24 kHz mono
        await ws.send(b"\x01" + page)

async def main():
    async with websockets.connect(url, additional_headers=headers) as ws:
        sending = None
        try:
            async for frame in ws:
                if isinstance(frame, str):
                    print(frame)  # JSON: an error, or the bill when the call closes
                elif frame[0] == 0:
                    sending = asyncio.create_task(talk(ws))  # the call is live: start sending
                elif frame[0] == 1:
                    play(frame[1:])  # yours: the voice, Ogg/Opus, 24 kHz mono
                elif frame[0] == 2:
                    print(frame[1:].decode(), end="")  # the words it is saying
        except websockets.ConnectionClosed:
            pass
        if sending:
            sending.cancel()
        print("closed with code", ws.close_code)  # see the close codes below

asyncio.run(main())

microphone() and play() are yours to supply: an Opus encoder and decoder at 24 kHz mono. The model answers what it hears, so send your microphone from the moment the call is live: the call bills from then whether or not audio is flowing.

On the wire

Audio is binary. Everything else is JSON.

The first byte of a binary message says what it carries. Text messages are JSON from Tileward, so the two cannot be confused.

MessageDirectionWhat it is
Binary, first byte 0To youThe call is live. Billing starts here.
Binary, first byte 1Both waysOgg/Opus audio, 24 kHz mono, after the first byte: your microphone going up, the voice coming back.
Binary, first byte 2To youA piece of the text the model is saying.
{"type": "error", "code": "...", "message": "..."}To youWhy a call was refused or ended. The socket closes after it.
{"type": "closed", "reason": "...", "billed_seconds": 30.0, "cost_micros": "40000.0000"}To youThe bill. reason is client, host, balance or limit, and cost_micros is in millionths of a dollar.

The model reads the prompt before the call goes live. That takes a few seconds for a short prompt and longer for a long one, and it is not billed.

Close codes

Why a socket closed.

CodeMeaning
1000The call ended normally.
4400Bad request: an unknown voice, a prompt over 2,000 characters, or a seed, which a voice call does not take.
4401The key is missing or invalid.
4402The balance was empty at the start, or ran out during the call.
4403The key has a governance policy bound to it. A voice call takes a key without one.
4429Rate limit, or this key already has a call in progress.
4503No line is free right now. When the error carries retry_after_s, a line is starting: try again after that many seconds. Nothing was billed.
Billing

By the minute, from the moment it is live.

A call is billed by the minute of conversation from your account credit, counted in 80 ms steps from the moment it goes live, with a ten-second minimum. Silence bills the same as speech, and a call that never goes live bills nothing. Each voice model's rate is in the model library.

Tileward VoiceChat 11B · ready to serve on request

A call that looks things up.

Tileward VoiceChat 11B answers from your own Tileward Context store while the call is running.

It is ready to serve on request: talk to an engineer to start. A call then connects like this, with the same key, the same close codes and the same one call per key.

 Value
Addresswss://api.tileward.com/v1/voice/chat
promptWho the voice is, up to 2,000 characters. Optional.
contextThe store it answers from: knowledge for your uploaded documents, or the name of one of your Context conversations. Optional.
You sendBinary PCM audio: signed 16-bit little-endian, 16 kHz, mono.
You receiveBinary PCM audio: signed 16-bit little-endian, 22,050 Hz, mono, 80 ms a message.
Text messageWhat it is
{"type": "ready"}The call is live. Billing starts here.
{"type": "transcript.user", "delta": "..."}What the caller said.
{"type": "transcript.agent", "delta": "..."}What the voice is saying.
{"type": "lookup", "found": true}A question was looked up in your store, and whether it found an answer.
{"type": "closed", ...}The bill, as above, with lookups and found counts for the call.
{"type": "error", ...}Why a call was refused or ended.

To end the call, send {"type": "session.close"} or close the socket. The call is billed by the minute like the one above, plus the tokens its lookups use, and each lookup counts as one Context recall.

Where to go next

The models, and the rest of the API.

The voice models

The model library lists both voice models with their price and licence, and the write-up says how the PersonaPlex build was measured.

Chat, guard and Context

The endpoints say which address wants which credential, and Connect a client has a recipe for each client.