@pyai/sdk 0.1.1 → 0.2.0
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.
- package/README.md +132 -14
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +107 -2
- package/dist/index.d.ts +619 -5
- package/dist/index.js +368 -0
- package/package.json +1 -1
- package/src/cli.ts +115 -2
- package/src/index.ts +835 -5
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# @pyai/sdk
|
|
2
2
|
|
|
3
|
-
Official TypeScript/JavaScript SDK for [PyAI](https://pyai.com) — the
|
|
4
|
-
platform
|
|
5
|
-
|
|
3
|
+
Official TypeScript/JavaScript SDK for [PyAI](https://pyai.com) — the all-in-one
|
|
4
|
+
voice AI platform: lightning-fast speech-to-text, ultra-realistic text-to-speech,
|
|
5
|
+
end-to-end realtime voice agents, and automatic call compliance. Zero
|
|
6
|
+
dependencies; runs in the browser and Node 18+.
|
|
6
7
|
|
|
7
8
|
## PyAI products
|
|
8
9
|
|
|
9
|
-
- **[Hear](https://pyai.com/models/hear)** —
|
|
10
|
-
- **[Speak](https://pyai.com/models/speak)** —
|
|
11
|
-
- **[Omni](https://pyai.com/models/omni)** _(flagship)_ —
|
|
12
|
-
- **[Trace](https://pyai.com/models/trace)** _(flagship)_ —
|
|
13
|
-
- **[Cue](https://pyai.com/models/cue)** —
|
|
14
|
-
- **[Telephony](https://pyai.com/models/telephony)** —
|
|
10
|
+
- **[Hear](https://pyai.com/models/hear)** — Lightning-fast, telephony-native **speech-to-text**. Whisper-compatible transcription tuned for real phone-call audio, with live streaming partials so your app reacts mid-sentence, plus async batch transcription for big archives. `POST /v1/audio/transcriptions`
|
|
11
|
+
- **[Speak](https://pyai.com/models/speak)** — Ultra-realistic **text-to-speech** that starts speaking in tens of milliseconds. Stream lifelike, expressive voices, choose from 36 studio-quality presets, or clone any voice instantly — for free. `POST /v1/audio/speech`
|
|
12
|
+
- **[Omni](https://pyai.com/models/omni)** _(flagship)_ — One **API for a complete, end-to-end voice AI agent**. A single WebSocket where your agent listens, thinks, and speaks — grounded in your knowledge bases and tools, with human-like turn-taking and instant barge-in — no STT, LLM, or TTS to stitch together yourself. `wss://api.pyai.com/v1/omni`
|
|
13
|
+
- **[Trace](https://pyai.com/models/trace)** _(flagship)_ — The **compliance API that keeps your AI agents safe**. Trace automatically checks every call for HIPAA, TCPA, and PII risks (plus your own brand-voice rules), flags the exact rule broken, redacts sensitive data, and seals each call with a tamper-evident audit trail — so a risky conversation never slips through. `GET /v1/trace/interactions`
|
|
14
|
+
- **[Cue](https://pyai.com/models/cue)** — Realtime **turn detection + knowledge-grounded context** for your own stack. Bring your own LLM and voice; Cue nails the hard part — knowing the instant a speaker finishes and surfacing the right context. `wss://api.pyai.com/v1/audio/transcriptions/stream`
|
|
15
|
+
- **[Telephony](https://pyai.com/models/telephony)** — Instant **managed phone numbers** for your voice agents. Provision a US number and route live calls straight into an Omni agent — no carrier contracts, no telephony glue. `POST /v1/telephony/numbers`
|
|
15
16
|
|
|
16
17
|
The contract is `https://api.pyai.com/openapi.json`. This SDK wraps it
|
|
17
18
|
ergonomically with typed errors, automatic retries, and a realtime helper.
|
|
@@ -67,6 +68,118 @@ const proto = pyai.realtimeSubprotocol();
|
|
|
67
68
|
> targets it by default (`product: "omni"`); `product: "flow"` uses
|
|
68
69
|
> `/v1/realtime`. The older `/v2/omni/chat` URL is deprecated but still works.
|
|
69
70
|
|
|
71
|
+
## Streaming speech-to-text (Hear / Cue)
|
|
72
|
+
|
|
73
|
+
`transcriptions.stream()` hides the WebSocket frame protocol behind callbacks.
|
|
74
|
+
It opens `wss://api.pyai.com/v1/audio/transcriptions/stream` (key carried as the
|
|
75
|
+
WS subprotocol, so it works in the browser), routes the wire frames to
|
|
76
|
+
`onPartial`/`onFinal`/`onError`, and gives you `sendAudio`, `commit()`, and
|
|
77
|
+
`close()`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const hear = pyai.audio.transcriptions.stream({
|
|
81
|
+
sampleRate: 16000,
|
|
82
|
+
onPartial: (f) => console.log("…", f.text),
|
|
83
|
+
onFinal: (f) => console.log("✓", f.text, `(${f.audio_ms}ms)`),
|
|
84
|
+
onError: (e) => console.error(e),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
micChunks.on("data", (pcm16) => hear.sendAudio(pcm16)); // binary frames
|
|
88
|
+
vad.on("end", () => hear.commit()); // force-finalize an utterance
|
|
89
|
+
// hear.close() also flushes a final for any buffered audio
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Frame `type`s, WS close codes, and error `code`s are exported as named
|
|
93
|
+
constants so you never hardcode a magic string:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { HearFrameType, WSCloseCode, ErrorCode } from "@pyai/sdk";
|
|
97
|
+
HearFrameType.SpeechFinal; // "speech_final"
|
|
98
|
+
WSCloseCode.OverCapacity; // 4429
|
|
99
|
+
ErrorCode.CreditExhausted; // "credit_exhausted"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Set `grounding: true` to turn the stream into **Cue** (turn detection + KB
|
|
103
|
+
context): the SDK sends the grounding config on open and `final`/`speech_final`
|
|
104
|
+
frames then carry a `grounding` array of top KB passages.
|
|
105
|
+
|
|
106
|
+
In Node, pass a WebSocket implementation if there's no global one:
|
|
107
|
+
`transcriptions.stream({ webSocket: (await import("ws")).WebSocket })`.
|
|
108
|
+
|
|
109
|
+
## Speak audio formats (incl. telephony G.711)
|
|
110
|
+
|
|
111
|
+
`audio.speech` encodes server-side into any of eight formats via `response_format`
|
|
112
|
+
— the audio comes back already in the shape you need, so telephony callers can
|
|
113
|
+
drop the hand-rolled resampler + μ-law encoder entirely:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// Twilio/SIP-ready in one param: raw 8 kHz mono μ-law, no client-side DSP.
|
|
117
|
+
const ulaw = await pyai.audio.speech({
|
|
118
|
+
input: "Your appointment is confirmed.",
|
|
119
|
+
voice: "stock_sarah_style2",
|
|
120
|
+
response_format: "g711_ulaw", // -> audio/basic, forced 8 kHz
|
|
121
|
+
});
|
|
122
|
+
// base64-encode `ulaw` straight into a Twilio media frame.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| `response_format` | sample rates (Hz) | Content-Type |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `mp3` (default) | 8000 / 16000 / 24000 / 48000 | `audio/mpeg` |
|
|
128
|
+
| `wav` | 8000 / 16000 / 24000 / 48000 | `audio/wav` |
|
|
129
|
+
| `opus` | 8000 / 16000 / 24000 / 48000 | `audio/ogg` |
|
|
130
|
+
| `aac` | 8000 / 16000 / 24000 / 48000 | `audio/aac` |
|
|
131
|
+
| `flac` | 8000 / 16000 / 24000 / 48000 | `audio/flac` |
|
|
132
|
+
| `pcm` (raw int16 LE, no header) | 8000 / 16000 / 24000 / 48000 | `audio/pcm` |
|
|
133
|
+
| `g711_ulaw` | 8000 (forced) | `audio/basic` |
|
|
134
|
+
| `g711_alaw` | 8000 (forced) | `audio/basic` |
|
|
135
|
+
|
|
136
|
+
`sample_rate` is optional — omit it for the engine's native 24 kHz (`g711_*` is
|
|
137
|
+
always 8 kHz). The set is typed (`SpeechFormat`) and exported as `SPEECH_FORMATS`
|
|
138
|
+
/ `SPEECH_SAMPLE_RATES` for dropdowns and validation. Any other value is a
|
|
139
|
+
`400 unsupported_format`; omit `response_format` for the default `mp3`.
|
|
140
|
+
|
|
141
|
+
> See [`examples/speak-telephony-formats`](../../examples/speak-telephony-formats)
|
|
142
|
+
> for the full before/after: ~120 lines of resampler + μ-law replaced by one
|
|
143
|
+
> param, with Node (`@pyai/twilio`), Python, and raw-curl snippets.
|
|
144
|
+
|
|
145
|
+
## More APIs: clones, telephony, trace
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Voice clones (Speak)
|
|
149
|
+
const { data: clones } = await pyai.clones.list();
|
|
150
|
+
const clone = await pyai.clones.create({ name: "Brand VO", file: refAudioBlob });
|
|
151
|
+
await pyai.clones.delete(clone.id);
|
|
152
|
+
|
|
153
|
+
// Managed phone numbers (Telephony)
|
|
154
|
+
const { data: avail } = await pyai.telephony.numbers.available({ areaCode: "415" });
|
|
155
|
+
const num = await pyai.telephony.numbers.buy({ phone_number: avail[0]!.phone_number, agent_id: "agent_123" });
|
|
156
|
+
await pyai.telephony.numbers.assign(num.id, "agent_123");
|
|
157
|
+
await pyai.telephony.numbers.release(num.id);
|
|
158
|
+
|
|
159
|
+
// Compliance (Trace)
|
|
160
|
+
const { data: calls } = await pyai.trace.interactions.list({ verdict: "FAIL" });
|
|
161
|
+
const detail = await pyai.trace.interactions.get(calls[0]!.id);
|
|
162
|
+
await pyai.trace.config.set({ agent_id: "agent_123", enabled: true });
|
|
163
|
+
const exposure = await pyai.trace.exposure(30);
|
|
164
|
+
|
|
165
|
+
// Per-call eval scorecard (timeline + quality metrics). These are additive and
|
|
166
|
+
// forward-compatible — present once the engine emits them, so reading them is
|
|
167
|
+
// always safe (the timeline reader returns [] until then).
|
|
168
|
+
const timeline = await pyai.trace.callTimeline(detail.id); // TraceTimelineTurn[]
|
|
169
|
+
const quality = detail.quality_metrics; // { wer?, ttfb_ms?, turn_p95_ms?, vaqi?, … }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Reproducible runs (evals)
|
|
173
|
+
|
|
174
|
+
`audio.speech` and `audio.transcriptions.create` take optional `seed` and
|
|
175
|
+
`temperature` for deterministic eval runs. They're forward-compatible — honored
|
|
176
|
+
once the engine supports them and otherwise ignored — so it's always safe to send:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
await pyai.audio.speech({ input: "Hello", voice: "stock_sarah_style2", seed: 42, temperature: 0 });
|
|
180
|
+
await pyai.audio.transcriptions.create({ file: wavBlob, seed: 42 });
|
|
181
|
+
```
|
|
182
|
+
|
|
70
183
|
## Errors
|
|
71
184
|
|
|
72
185
|
Failures throw `PyAIError` with a stable `code` (branch on it, not the message):
|
|
@@ -90,16 +203,21 @@ Common codes: `unauthorized`, `forbidden`, `credit_exhausted`,
|
|
|
90
203
|
|
|
91
204
|
## CLI (`pyai`)
|
|
92
205
|
|
|
93
|
-
Installing the package also provides a `pyai` command
|
|
94
|
-
|
|
206
|
+
Installing the package also provides a `pyai` command. `pyai doctor` runs a
|
|
207
|
+
deeper diagnosis — it introspects your key/scopes via `GET /v1/me` (skipped
|
|
208
|
+
gracefully if the route isn't deployed yet), checks endpoint liveness, runs a
|
|
209
|
+
Speak→Hear round-trip, and prints remediation hints for any failure:
|
|
95
210
|
|
|
96
211
|
```bash
|
|
97
212
|
export PYAI_API_KEY=pyai_test_...
|
|
98
|
-
npx pyai
|
|
213
|
+
npx pyai doctor
|
|
214
|
+
# PASS key (/v1/me) — env=test; 3 scope(s): hear:transcribe, voice:synthesize, hear:stream
|
|
99
215
|
# PASS models.list — 12 models
|
|
100
216
|
# PASS voices.list — 38 voices
|
|
101
|
-
# PASS
|
|
102
|
-
#
|
|
217
|
+
# PASS speak→hear round-trip — synth 45210 bytes → "the quick brown fox…"
|
|
218
|
+
# Diagnosis: healthy. Key, endpoint, and a Speak→Hear round-trip all work.
|
|
219
|
+
|
|
220
|
+
npx pyai smoke # lighter: models + voices + speak
|
|
103
221
|
```
|
|
104
222
|
|
|
105
223
|
Other commands:
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* pyai — CLI
|
|
4
|
-
*
|
|
3
|
+
* pyai — CLI for the PyAI API: proves your key, the endpoint, and audio in one
|
|
4
|
+
* command (`smoke`) or runs a deeper diagnosis with remediation hints (`doctor`).
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
+
* pyai doctor diagnose key/scopes + endpoint + Speak→Hear round-trip
|
|
7
8
|
* pyai smoke run models+voices+speak and report PASS/FAIL
|
|
8
9
|
* pyai models list models
|
|
9
10
|
* pyai voices [--gender g --region r] list voices
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* pyai — CLI
|
|
4
|
-
*
|
|
3
|
+
* pyai — CLI for the PyAI API: proves your key, the endpoint, and audio in one
|
|
4
|
+
* command (`smoke`) or runs a deeper diagnosis with remediation hints (`doctor`).
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
+
* pyai doctor diagnose key/scopes + endpoint + Speak→Hear round-trip
|
|
7
8
|
* pyai smoke run models+voices+speak and report PASS/FAIL
|
|
8
9
|
* pyai models list models
|
|
9
10
|
* pyai voices [--gender g --region r] list voices
|
|
@@ -57,6 +58,7 @@ function fail(msg) {
|
|
|
57
58
|
const USAGE = `pyai — PyAI API CLI
|
|
58
59
|
|
|
59
60
|
Usage:
|
|
61
|
+
pyai doctor diagnose key/scopes + endpoint + Speak→Hear round-trip
|
|
60
62
|
pyai smoke run a key/endpoint/audio smoke test
|
|
61
63
|
pyai models list models
|
|
62
64
|
pyai voices [--gender g] [--region r] list voices
|
|
@@ -135,10 +137,113 @@ async function cmdSmoke(flags) {
|
|
|
135
137
|
if (!allOk)
|
|
136
138
|
process.exit(1);
|
|
137
139
|
}
|
|
140
|
+
/** Turn an error into an actionable, code-first remediation hint. */
|
|
141
|
+
function remediation(err) {
|
|
142
|
+
if (!(err instanceof PyAIError))
|
|
143
|
+
return err?.message ?? String(err);
|
|
144
|
+
switch (err.code) {
|
|
145
|
+
case "unauthorized":
|
|
146
|
+
return "Invalid or missing key — check PYAI_API_KEY (a pyai_test_ or pyai_live_ key).";
|
|
147
|
+
case "forbidden":
|
|
148
|
+
return "Key is missing a required scope — add it to the key in the console.";
|
|
149
|
+
case "origin_not_allowed":
|
|
150
|
+
return "Publishable token origin not allow-listed — fix the allowed origins.";
|
|
151
|
+
case "credit_exhausted":
|
|
152
|
+
return "Out of prepaid credit — add credit, or use a pyai_test_ sandbox key.";
|
|
153
|
+
case "key_budget_exceeded":
|
|
154
|
+
return "Per-key monthly budget hit — raise the budget in the console.";
|
|
155
|
+
case "insufficient_quota":
|
|
156
|
+
return "Plan quota exhausted — upgrade your plan.";
|
|
157
|
+
case "rate_limit_exceeded":
|
|
158
|
+
return "Rate limited — back off and retry (honor Retry-After).";
|
|
159
|
+
case "concurrency_limit_exceeded":
|
|
160
|
+
return "Too many concurrent sessions — retry shortly.";
|
|
161
|
+
case "daily_cap_exceeded":
|
|
162
|
+
return "Daily cap reached — wait until it resets.";
|
|
163
|
+
default:
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
switch (err.status) {
|
|
167
|
+
case 401:
|
|
168
|
+
return "Invalid or missing key — check PYAI_API_KEY.";
|
|
169
|
+
case 403:
|
|
170
|
+
return "Forbidden — the key likely lacks the required scope.";
|
|
171
|
+
case 404:
|
|
172
|
+
return "Not found — check PYAI_BASE_URL and the route.";
|
|
173
|
+
case 429:
|
|
174
|
+
return "Rate/concurrency limited — back off and retry.";
|
|
175
|
+
default:
|
|
176
|
+
return err.message;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function doctorCheck(checks, name, fn) {
|
|
180
|
+
try {
|
|
181
|
+
checks.push({ name, status: "PASS", detail: await fn() });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const detail = err instanceof PyAIError ? `${err.status} ${err.code ?? ""} ${err.message}`.trim() : err.message;
|
|
185
|
+
checks.push({ name, status: "FAIL", detail, hint: remediation(err) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Deeper than smoke: key/scopes, endpoint liveness, and a Speak→Hear round-trip. */
|
|
189
|
+
async function cmdDoctor(flags) {
|
|
190
|
+
const pyai = client(flags);
|
|
191
|
+
const checks = [];
|
|
192
|
+
// (a) Key validity + scopes via GET /v1/me. The route is new, so a 404 means
|
|
193
|
+
// "not deployed here yet" — skip it rather than failing the whole doctor.
|
|
194
|
+
try {
|
|
195
|
+
const me = await pyai.me();
|
|
196
|
+
const scopes = Array.isArray(me.scopes) ? me.scopes : [];
|
|
197
|
+
const env = me.environment ?? me.env ?? "unknown";
|
|
198
|
+
checks.push({
|
|
199
|
+
name: "key (/v1/me)",
|
|
200
|
+
status: "PASS",
|
|
201
|
+
detail: `env=${env}; ${scopes.length} scope(s)${scopes.length ? `: ${scopes.join(", ")}` : ""}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (err instanceof PyAIError && err.status === 404) {
|
|
206
|
+
checks.push({ name: "key (/v1/me)", status: "SKIP", detail: "introspection route not on this deployment" });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const detail = err instanceof PyAIError ? `${err.status} ${err.code ?? ""}`.trim() : err.message;
|
|
210
|
+
checks.push({ name: "key (/v1/me)", status: "FAIL", detail, hint: remediation(err) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// (b) Endpoint liveness.
|
|
214
|
+
await doctorCheck(checks, "models.list", async () => `${(await pyai.models.list()).data.length} models`);
|
|
215
|
+
await doctorCheck(checks, "voices.list", async () => `${(await pyai.voices.list()).data.length} voices`);
|
|
216
|
+
// (c) Speak -> Hear round-trip: synthesize a sentence, then transcribe it.
|
|
217
|
+
await doctorCheck(checks, "speak→hear round-trip", async () => {
|
|
218
|
+
const audio = await pyai.audio.speech({ input: "The quick brown fox jumps over the lazy dog." });
|
|
219
|
+
const bytes = Buffer.from(audio).byteLength;
|
|
220
|
+
const blob = new Blob([audio], { type: "audio/wav" });
|
|
221
|
+
const tr = await pyai.audio.transcriptions.create({ file: blob, filename: "doctor.wav" });
|
|
222
|
+
const text = (tr.text ?? "").trim();
|
|
223
|
+
if (!text)
|
|
224
|
+
throw new Error(`synthesized ${bytes} bytes but transcription came back empty`);
|
|
225
|
+
return `synth ${bytes} bytes → "${text.length > 60 ? `${text.slice(0, 60)}…` : text}"`;
|
|
226
|
+
});
|
|
227
|
+
for (const c of checks) {
|
|
228
|
+
out(`${c.status.padEnd(4)} ${c.name} — ${c.detail}`);
|
|
229
|
+
if (c.hint)
|
|
230
|
+
out(` ↳ ${c.hint}`);
|
|
231
|
+
}
|
|
232
|
+
const failed = checks.filter((c) => c.status === "FAIL");
|
|
233
|
+
if (failed.length === 0) {
|
|
234
|
+
out("\nDiagnosis: healthy. Key, endpoint, and a Speak→Hear round-trip all work.");
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
out(`\nDiagnosis: ${failed.length} check(s) failed — see the remediation hints above.`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
138
241
|
async function main() {
|
|
139
242
|
const flags = parseArgs(process.argv.slice(2));
|
|
140
243
|
const cmd = flags._[0];
|
|
141
244
|
switch (cmd) {
|
|
245
|
+
case "doctor":
|
|
246
|
+
return cmdDoctor(flags);
|
|
142
247
|
case "smoke":
|
|
143
248
|
return cmdSmoke(flags);
|
|
144
249
|
case "models":
|