anva-sdk 0.1.0 → 0.3.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/LICENSE +21 -0
- package/README.md +29 -19
- package/index.d.ts +91 -54
- package/index.js +91 -78
- package/package.json +7 -3
- package/realtime.js +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Penguin Robotics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
# anva — JavaScript / TypeScript
|
|
1
|
+
# anva-sdk — JavaScript / TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
key server-side; hand only the `embed_url` to clients).
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install anva-sdk
|
|
9
|
-
```
|
|
3
|
+
Install with `npm install anva-sdk@0.3.0`. REST uses Node 18+'s fetch. Realtime needs
|
|
4
|
+
Node 22+'s WebSocket or an injected compatible constructor.
|
|
10
5
|
|
|
11
6
|
```js
|
|
12
|
-
import { Anva } from "anva-sdk";
|
|
13
|
-
|
|
7
|
+
import { Anva, AnvaError } from "anva-sdk";
|
|
14
8
|
const client = new Anva(process.env.ANVA_KEY);
|
|
15
|
-
|
|
16
|
-
const session = await client.createSession({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
const capabilities = await client.capabilities();
|
|
10
|
+
const session = await client.createSession({
|
|
11
|
+
presetId:"YOUR_PRESET_ID", serviceMode:"byo_llm"
|
|
12
|
+
});
|
|
13
|
+
// Attach session.embed_url in your browser.
|
|
14
|
+
const stream = await client.connect(session.session_id);
|
|
15
|
+
try {
|
|
16
|
+
for await (const event of stream) {
|
|
17
|
+
if (event.type === "turn.request") {
|
|
18
|
+
// Replace this immediate sample reply with your cancellable LLM worker.
|
|
19
|
+
await stream.turnDelta(event.payload.turn_id, "Hello from your application.");
|
|
20
|
+
await stream.turnDone(event.payload.turn_id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} finally {
|
|
24
|
+
stream.close();
|
|
25
|
+
await client.endSession(session.session_id);
|
|
23
26
|
}
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
Inject WebSocket on older Node: `client.connect(id, {WebSocketImpl})`. Keep keys
|
|
30
|
+
and sockets server-side. `AnvaError` exposes `status`, `code`, and `details`;
|
|
31
|
+
command errors arrive as structured events. Never log authenticated event URLs.
|
|
32
|
+
|
|
33
|
+
All five canonical modes, capability/billing discovery, REST interrupt,
|
|
34
|
+
presentation and PCM methods are typed in `index.d.ts`. Read the repository
|
|
35
|
+
README for mode availability and flow-control requirements. Preset updates use
|
|
36
|
+
the REST API's snake_case patch fields.
|
package/index.d.ts
CHANGED
|
@@ -1,81 +1,118 @@
|
|
|
1
|
-
/** Official
|
|
2
|
-
|
|
3
|
-
export interface AnvaOptions {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/** Official Anva SDK. All API keys and authenticated realtime sockets belong on your server. */
|
|
2
|
+
export type ServiceMode = 'avatar_only' | 'byo_llm' | 'anva_light' | 'anva_expressive' | 'elevenagents_max';
|
|
3
|
+
export interface AnvaOptions { baseUrl?: string; }
|
|
4
|
+
export interface PerformanceOptions {
|
|
5
|
+
conversation_provider?: string;
|
|
6
|
+
performance_mode?: string;
|
|
7
|
+
elevenlabs_agent_id?: string;
|
|
8
|
+
dynamic_expressions?: boolean;
|
|
9
|
+
[key: string]: unknown;
|
|
6
10
|
}
|
|
7
|
-
|
|
8
11
|
export interface CreateSessionParams {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
presetId?: string;
|
|
13
|
+
avatarId?: string;
|
|
14
|
+
systemPrompt?: string;
|
|
15
|
+
voiceId?: string;
|
|
16
|
+
languageCode?: string;
|
|
17
|
+
serviceMode?: ServiceMode;
|
|
18
|
+
/** Deprecated compatibility alias. Conflicts with serviceMode are rejected by the server. */
|
|
19
|
+
llmMode?: string;
|
|
20
|
+
performanceOptions?: PerformanceOptions;
|
|
21
|
+
conversationProvider?: string;
|
|
22
|
+
performanceMode?: string;
|
|
23
|
+
elevenlabsAgentId?: string;
|
|
24
|
+
dynamicExpressions?: boolean;
|
|
12
25
|
webhookUrl?: string;
|
|
13
26
|
webhookSecret?: string;
|
|
14
27
|
}
|
|
15
|
-
|
|
28
|
+
export interface ModeCapability { id: ServiceMode; name: string; available: boolean; reason?: string; input?: string[]; [key: string]: unknown; }
|
|
29
|
+
export interface Capabilities { api_version: string; modes: ModeCapability[]; audio_input?: Record<string, unknown>; [key: string]: unknown; }
|
|
30
|
+
export interface SessionBilling { unit: string; tokens_per_minute?: number; credits_per_minute?: number; rate_version: string; basis: string; [key: string]: unknown; }
|
|
16
31
|
export interface Session {
|
|
17
32
|
session_id: string;
|
|
18
33
|
session_token: string;
|
|
19
|
-
|
|
34
|
+
instance_id: string;
|
|
35
|
+
preset_id?: string;
|
|
36
|
+
avatar_id?: string;
|
|
37
|
+
service_mode?: ServiceMode;
|
|
20
38
|
llm_mode?: string;
|
|
39
|
+
billing?: SessionBilling;
|
|
21
40
|
expires_at: string;
|
|
22
|
-
/** Ready to drop into an <iframe allow="camera; microphone; autoplay">. */
|
|
23
41
|
embed_url: string;
|
|
24
42
|
events_ws_url: string;
|
|
25
43
|
[key: string]: unknown;
|
|
26
44
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
system_prompt: string;
|
|
33
|
-
voice_id: string;
|
|
34
|
-
language_code?: string;
|
|
35
|
-
disabled: boolean;
|
|
36
|
-
active: boolean;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SessionEvent {
|
|
41
|
-
type: string;
|
|
45
|
+
export interface Preset {
|
|
46
|
+
id: string; name: string; avatar_id: string;
|
|
47
|
+
/** Deprecated alias. */ visual_character_id?: string;
|
|
48
|
+
system_prompt: string; voice_id: string; language_code?: string;
|
|
49
|
+
disabled: boolean; active: boolean;
|
|
42
50
|
[key: string]: unknown;
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
export interface
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
export interface Instance { id: string; [key: string]: unknown; }
|
|
53
|
+
export interface SessionEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
|
|
54
|
+
export interface PresentationContext {
|
|
55
|
+
version: 1; demo: string; revision: number; page: string; highlight: string;
|
|
56
|
+
brief?: string; opening?: string;
|
|
57
|
+
pages: Array<{ id: string; title: string; items: Array<{id: string; text: string}> }>;
|
|
48
58
|
}
|
|
49
|
-
|
|
59
|
+
export interface PresentationStart { version: 1; demo: string; revision: number; }
|
|
60
|
+
export interface EventsOptions { WebSocketImpl?: new (url: string) => WebSocket; }
|
|
61
|
+
export type PCMBytes = Uint8Array | ArrayBuffer;
|
|
50
62
|
export declare class AnvaError extends Error {
|
|
51
|
-
status: number;
|
|
52
|
-
code: string;
|
|
63
|
+
constructor(status: number, code: string, message: string, details?: Record<string, unknown>);
|
|
64
|
+
status: number; code: string; details: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** One active event reader per connection. Breaking iteration closes the socket. */
|
|
67
|
+
export declare class RealtimeSession implements AsyncIterable<SessionEvent> {
|
|
68
|
+
constructor(socket: WebSocket);
|
|
69
|
+
ready: Promise<RealtimeSession>;
|
|
70
|
+
closed: boolean;
|
|
71
|
+
send(type: string, payload?: Record<string, unknown>): Promise<void>;
|
|
72
|
+
message(text: string): Promise<void>;
|
|
73
|
+
interrupt(): Promise<void>;
|
|
74
|
+
updateContext(context: PresentationContext): Promise<void>;
|
|
75
|
+
startPresentation(start: PresentationStart): Promise<void>;
|
|
76
|
+
turnDelta(turnId: string, text: string): Promise<void>;
|
|
77
|
+
turnDone(turnId: string): Promise<void>;
|
|
78
|
+
turnCancel(turnId: string, reason?: string): Promise<void>;
|
|
79
|
+
startSpeech(turnId: string, options?: {text?: string}): Promise<void>;
|
|
80
|
+
appendSpeech(turnId: string, seq: number, startSample: number, pcm: PCMBytes): Promise<void>;
|
|
81
|
+
finishSpeech(turnId: string, totalSamples: number): Promise<void>;
|
|
82
|
+
cancelSpeech(turnId: string): Promise<void>;
|
|
83
|
+
close(): void;
|
|
84
|
+
[Symbol.asyncIterator](): AsyncGenerator<SessionEvent, void, void>;
|
|
53
85
|
}
|
|
54
|
-
|
|
55
86
|
export declare class Anva {
|
|
56
87
|
constructor(apiKey: string, opts?: AnvaOptions);
|
|
57
|
-
apiKey: string;
|
|
58
|
-
baseUrl: string;
|
|
59
|
-
|
|
88
|
+
apiKey: string; baseUrl: string;
|
|
60
89
|
createSession(params: CreateSessionParams): Promise<Session>;
|
|
61
90
|
getSession(sessionId: string): Promise<Record<string, unknown>>;
|
|
62
91
|
endSession(sessionId: string): Promise<Record<string, unknown>>;
|
|
63
|
-
sendMessage(sessionId: string, text: string): Promise<{
|
|
64
|
-
interrupt(sessionId: string): Promise<{
|
|
65
|
-
triggerAction(sessionId: string, name: string): Promise<{
|
|
92
|
+
sendMessage(sessionId: string, text: string): Promise<{status: string}>;
|
|
93
|
+
interrupt(sessionId: string): Promise<{status: string}>;
|
|
94
|
+
triggerAction(sessionId: string, name: string): Promise<{status: string}>;
|
|
66
95
|
eventsUrl(sessionId: string): string;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
connect(sessionId: string, options?: EventsOptions): Promise<RealtimeSession>;
|
|
97
|
+
events(sessionId: string, options?: EventsOptions): AsyncGenerator<SessionEvent, void, void>;
|
|
98
|
+
capabilities(): Promise<Capabilities>;
|
|
99
|
+
billing(): Promise<Record<string, unknown>>;
|
|
100
|
+
listAvatars(): Promise<Record<string, unknown>>;
|
|
101
|
+
listVoices(): Promise<Record<string, unknown>>;
|
|
102
|
+
listLanguages(): Promise<Record<string, unknown>>;
|
|
103
|
+
updateContext(sessionId: string, context: PresentationContext): Promise<Record<string, unknown>>;
|
|
104
|
+
startPresentation(sessionId: string, start: PresentationStart): Promise<Record<string, unknown>>;
|
|
105
|
+
speech(sessionId: string, type: string, payload: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
106
|
+
startSpeech(sessionId: string, turnId: string, options?: {text?: string}): Promise<Record<string, unknown>>;
|
|
107
|
+
appendSpeech(sessionId: string, turnId: string, seq: number, startSample: number, pcm: PCMBytes): Promise<Record<string, unknown>>;
|
|
108
|
+
finishSpeech(sessionId: string, turnId: string, totalSamples: number): Promise<Record<string, unknown>>;
|
|
109
|
+
cancelSpeech(sessionId: string, turnId: string): Promise<Record<string, unknown>>;
|
|
110
|
+
listPresets(): Promise<{presets: Preset[]}>;
|
|
111
|
+
createPreset(params: {name: string; avatarId?: string; visualCharacterId?: string; systemPrompt?: string; voiceId?: string; languageCode?: string}): Promise<Preset>;
|
|
112
|
+
/** Patch uses the REST API's snake_case field names. */
|
|
113
|
+
updatePreset(presetId: string, patch: Record<string, unknown>): Promise<Preset>;
|
|
114
|
+
getPreset(presetId: string): Promise<Preset>;
|
|
115
|
+
deletePreset(presetId: string): Promise<{id: string; status: string}>;
|
|
116
|
+
listInstances(): Promise<{instances: Instance[]}>;
|
|
79
117
|
}
|
|
80
|
-
|
|
81
118
|
export default Anva;
|
package/index.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Official JavaScript/TypeScript SDK for Anva (https://anva.ai).
|
|
3
3
|
*
|
|
4
|
-
* import { Anva } from "anva";
|
|
4
|
+
* import { Anva } from "anva-sdk";
|
|
5
5
|
* const client = new Anva(process.env.ANVA_KEY);
|
|
6
|
-
* const session = await client.createSession({
|
|
6
|
+
* const session = await client.createSession({ presetId: "..." });
|
|
7
7
|
* // put session.embed_url in an <iframe allow="camera; microphone; autoplay">
|
|
8
8
|
*
|
|
9
|
-
* Works in Node 18+ (
|
|
9
|
+
* Works in Node 18+ (supply a WebSocket implementation before Node 22) and modern browsers — but keep
|
|
10
10
|
* your API key server-side; mint sessions on your backend and hand the
|
|
11
11
|
* embed_url to the client.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { RealtimeSession, speechStart, speechAppend } from "./realtime.js";
|
|
15
|
+
export { RealtimeSession } from "./realtime.js";
|
|
16
|
+
|
|
14
17
|
const DEFAULT_BASE_URL = "https://anva.ai";
|
|
15
18
|
|
|
16
19
|
export class AnvaError extends Error {
|
|
17
|
-
constructor(status, code, message) {
|
|
20
|
+
constructor(status, code, message, details = {}) {
|
|
18
21
|
super(`${code}: ${message} (HTTP ${status})`);
|
|
19
22
|
this.name = "AnvaError";
|
|
20
23
|
this.status = status;
|
|
21
24
|
this.code = code;
|
|
25
|
+
this.details = details;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -36,115 +40,124 @@ export class Anva {
|
|
|
36
40
|
// -- sessions -------------------------------------------------------------
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* Create a live session
|
|
40
|
-
*
|
|
43
|
+
* Create a live session, one of two ways:
|
|
44
|
+
* - Embed tier: pass `presetId` (a saved preset from the Playground).
|
|
45
|
+
* - Advanced tier: pass `avatarId` + the persona (`systemPrompt`, `voiceId`,
|
|
46
|
+
* `languageCode`) directly — nothing is stored server-side.
|
|
47
|
+
* Returns { session_id, session_token, embed_url, events_ws_url, expires_at,
|
|
48
|
+
* instance_id, preset_id, avatar_id, ... }.
|
|
49
|
+
*
|
|
50
|
+
* @param {{presetId?: string, avatarId?: string, systemPrompt?: string,
|
|
51
|
+
* voiceId?: string, languageCode?: string, llmMode?: string,
|
|
52
|
+
* webhookUrl?: string, webhookSecret?: string}} params
|
|
41
53
|
*/
|
|
42
|
-
createSession(
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
54
|
+
createSession(params = {}) {
|
|
55
|
+
const { presetId, avatarId, systemPrompt, voiceId, languageCode, llmMode, serviceMode, performanceOptions, conversationProvider, performanceMode, elevenlabsAgentId, dynamicExpressions, webhookUrl, webhookSecret } = params;
|
|
56
|
+
if ((!presetId && !avatarId) || (presetId && avatarId)) {
|
|
57
|
+
throw new Error("createSession requires exactly one of presetId or avatarId");
|
|
58
|
+
}
|
|
59
|
+
const body = {};
|
|
60
|
+
if (presetId) body.preset_id = presetId;
|
|
61
|
+
if (avatarId) body.avatar_id = avatarId;
|
|
62
|
+
if (systemPrompt) body.system_prompt = systemPrompt;
|
|
63
|
+
if (voiceId) body.voice_id = voiceId;
|
|
64
|
+
if (languageCode) body.language_code = languageCode;
|
|
65
|
+
if (llmMode !== undefined) body.llm_mode = llmMode;
|
|
66
|
+
if (serviceMode !== undefined) body.service_mode = serviceMode;
|
|
67
|
+
if (performanceOptions !== undefined) body.performance_options = performanceOptions;
|
|
68
|
+
if (conversationProvider !== undefined) body.conversation_provider = conversationProvider;
|
|
69
|
+
if (performanceMode !== undefined) body.performance_mode = performanceMode;
|
|
70
|
+
if (elevenlabsAgentId !== undefined) body.elevenlabs_agent_id = elevenlabsAgentId;
|
|
71
|
+
if (dynamicExpressions !== undefined) body.dynamic_expressions = dynamicExpressions;
|
|
45
72
|
if (webhookUrl) body.webhook_url = webhookUrl;
|
|
46
73
|
if (webhookSecret) body.webhook_secret = webhookSecret;
|
|
47
|
-
return this._request("POST", "/api/
|
|
74
|
+
return this._request("POST", "/api/v2/sessions", body);
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
getSession(sessionId) {
|
|
51
|
-
return this._request("GET", `/api/
|
|
78
|
+
return this._request("GET", `/api/v2/sessions/${enc(sessionId)}`);
|
|
52
79
|
}
|
|
53
80
|
|
|
54
81
|
endSession(sessionId) {
|
|
55
|
-
return this._request("DELETE", `/api/
|
|
82
|
+
return this._request("DELETE", `/api/v2/sessions/${enc(sessionId)}`);
|
|
56
83
|
}
|
|
57
84
|
|
|
58
|
-
/**
|
|
85
|
+
/** Send `text` as a user message; the avatar hears it and replies (it does
|
|
86
|
+
* NOT speak `text` verbatim). For external replies, use serviceMode:"byo_llm" and turnDelta/turnDone on a realtime connection. */
|
|
59
87
|
sendMessage(sessionId, text) {
|
|
60
|
-
return this._request("POST", `/api/
|
|
88
|
+
return this._request("POST", `/api/v2/sessions/${enc(sessionId)}/messages`, { text });
|
|
61
89
|
}
|
|
62
90
|
|
|
63
91
|
/** Stop the avatar mid-sentence. */
|
|
64
92
|
interrupt(sessionId) {
|
|
65
|
-
return this._request("POST", `/api/
|
|
93
|
+
return this._request("POST", `/api/v2/sessions/${enc(sessionId)}/interrupt`, {});
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
triggerAction(sessionId, name) {
|
|
69
|
-
return this._request("POST", `/api/
|
|
97
|
+
return this._request("POST", `/api/v2/sessions/${enc(sessionId)}/actions`, { name });
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
/** The authenticated WebSocket URL for the session's event stream. */
|
|
73
101
|
eventsUrl(sessionId) {
|
|
74
102
|
const ws = this.baseUrl.replace(/^http/, "ws");
|
|
75
|
-
return `${ws}/api/
|
|
103
|
+
return `${ws}/api/v2/sessions/${enc(sessionId)}/events?api_key=${encodeURIComponent(this.apiKey)}`;
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
* for await (const event of client.events(sessionId)) { ... }
|
|
82
|
-
*/
|
|
83
|
-
async *events(sessionId, { WebSocketImpl } = {}) {
|
|
106
|
+
/** Open a single socket for both events and turn/presentation/speech commands. */
|
|
107
|
+
async connect(sessionId, { WebSocketImpl } = {}) {
|
|
84
108
|
const WS = WebSocketImpl || globalThis.WebSocket;
|
|
85
|
-
if (!WS) throw new Error(
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
let notify = null;
|
|
89
|
-
let done = false;
|
|
90
|
-
let failure = null;
|
|
91
|
-
ws.onmessage = (m) => {
|
|
92
|
-
try {
|
|
93
|
-
queue.push(JSON.parse(m.data));
|
|
94
|
-
} catch {
|
|
95
|
-
/* non-JSON frame */
|
|
96
|
-
}
|
|
97
|
-
if (notify) notify();
|
|
98
|
-
};
|
|
99
|
-
ws.onclose = () => {
|
|
100
|
-
done = true;
|
|
101
|
-
if (notify) notify();
|
|
102
|
-
};
|
|
103
|
-
ws.onerror = () => {
|
|
104
|
-
failure = new Error("events socket error");
|
|
105
|
-
done = true;
|
|
106
|
-
if (notify) notify();
|
|
107
|
-
};
|
|
108
|
-
try {
|
|
109
|
-
while (!done || queue.length) {
|
|
110
|
-
if (!queue.length) {
|
|
111
|
-
await new Promise((resolve) => (notify = resolve));
|
|
112
|
-
notify = null;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
yield queue.shift();
|
|
116
|
-
}
|
|
117
|
-
if (failure) throw failure;
|
|
118
|
-
} finally {
|
|
119
|
-
try {
|
|
120
|
-
ws.close();
|
|
121
|
-
} catch {
|
|
122
|
-
/* already closed */
|
|
123
|
-
}
|
|
124
|
-
}
|
|
109
|
+
if (!WS) throw new Error('No WebSocket implementation; pass { WebSocketImpl }');
|
|
110
|
+
const stream = new RealtimeSession(new WS(this.eventsUrl(sessionId)));
|
|
111
|
+
try { await stream.ready; return stream; } catch (error) { stream.close(); throw error; }
|
|
125
112
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
async *events(sessionId, options = {}) {
|
|
114
|
+
const stream = await this.connect(sessionId, options);
|
|
115
|
+
try { yield* stream; } finally { stream.close(); }
|
|
116
|
+
}
|
|
117
|
+
capabilities() { return this._request('GET', '/api/v2/capabilities'); }
|
|
118
|
+
billing() { return this._request('GET', '/api/v2/billing'); }
|
|
119
|
+
listAvatars() { return this._request('GET', '/api/v2/avatars'); }
|
|
120
|
+
listVoices() { return this._request('GET', '/api/v2/voices'); }
|
|
121
|
+
listLanguages() { return this._request('GET', '/api/v2/languages'); }
|
|
122
|
+
updateContext(sessionId, context) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/context`, context); }
|
|
123
|
+
startPresentation(sessionId, start) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/presentation`, start); }
|
|
124
|
+
speech(sessionId, type, payload) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/speech`, { type, payload }); }
|
|
125
|
+
startSpeech(sessionId, turnId, { text } = {}) { return this.speech(sessionId, 'speech.start', speechStart(turnId, text)); }
|
|
126
|
+
appendSpeech(sessionId, turnId, seq, startSample, pcm) { return this.speech(sessionId, 'speech.append', speechAppend(turnId, seq, startSample, pcm)); }
|
|
127
|
+
finishSpeech(sessionId, turnId, totalSamples) { return this.speech(sessionId, 'speech.done', { turn_id: turnId, total_samples: totalSamples }); }
|
|
128
|
+
cancelSpeech(sessionId, turnId) { return this.speech(sessionId, 'speech.cancel', { turn_id: turnId }); }
|
|
129
|
+
|
|
130
|
+
// -- presets --------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
listPresets() {
|
|
133
|
+
return this._request("GET", "/api/v2/presets");
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
createPreset({ name, avatarId, visualCharacterId, systemPrompt, voiceId, languageCode }) {
|
|
134
137
|
const body = { name };
|
|
135
|
-
if (visualCharacterId) body.
|
|
138
|
+
if (avatarId || visualCharacterId) body.avatar_id = avatarId || visualCharacterId;
|
|
136
139
|
if (systemPrompt) body.system_prompt = systemPrompt;
|
|
137
140
|
if (voiceId) body.voice_id = voiceId;
|
|
138
141
|
if (languageCode) body.language_code = languageCode;
|
|
139
|
-
return this._request("POST", "/api/
|
|
142
|
+
return this._request("POST", "/api/v2/presets", body);
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
return this._request("GET", `/api/
|
|
145
|
+
getPreset(presetId) {
|
|
146
|
+
return this._request("GET", `/api/v2/presets/${enc(presetId)}`);
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
return this._request("
|
|
149
|
+
updatePreset(presetId, patch) {
|
|
150
|
+
return this._request("PATCH", `/api/v2/presets/${enc(presetId)}`, patch);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
deletePreset(presetId) {
|
|
154
|
+
return this._request("DELETE", `/api/v2/presets/${enc(presetId)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// -- instances ------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
listInstances() {
|
|
160
|
+
return this._request("GET", "/api/v2/instances");
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
// -- plumbing -------------------------------------------------------------
|
|
@@ -155,7 +168,7 @@ export class Anva {
|
|
|
155
168
|
headers: {
|
|
156
169
|
Authorization: `Bearer ${this.apiKey}`,
|
|
157
170
|
"Content-Type": "application/json",
|
|
158
|
-
"User-Agent": "anva-js/0.
|
|
171
|
+
"User-Agent": "anva-js/0.3.0",
|
|
159
172
|
},
|
|
160
173
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
161
174
|
});
|
|
@@ -167,8 +180,8 @@ export class Anva {
|
|
|
167
180
|
payload = { message: raw.slice(0, 300) };
|
|
168
181
|
}
|
|
169
182
|
if (!res.ok) {
|
|
170
|
-
const err = payload
|
|
171
|
-
throw new AnvaError(res.status, err.code || "request_failed", err.message || "request failed");
|
|
183
|
+
const err = payload?.error || payload || {};
|
|
184
|
+
throw new AnvaError(res.status, err.code || "request_failed", err.message || "request failed", err);
|
|
172
185
|
}
|
|
173
186
|
return payload;
|
|
174
187
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anva-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Official JavaScript/TypeScript SDK for Anva \u2014 live AI avatars for your product.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"avatar",
|
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"index.js",
|
|
32
32
|
"index.d.ts",
|
|
33
|
-
"README.md"
|
|
33
|
+
"README.md",
|
|
34
|
+
"realtime.js"
|
|
34
35
|
],
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.js"
|
|
37
41
|
}
|
|
38
|
-
}
|
|
42
|
+
}
|
package/realtime.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** A single bidirectional events connection. Keep the API key on your server. */
|
|
2
|
+
export class RealtimeSession {
|
|
3
|
+
constructor(socket) {
|
|
4
|
+
this.socket = socket;
|
|
5
|
+
this.queue = [];
|
|
6
|
+
this.waiters = [];
|
|
7
|
+
this.closed = false;
|
|
8
|
+
this.closeRequested = false;
|
|
9
|
+
this.failure = null;
|
|
10
|
+
this.ready = new Promise((resolve, reject) => {
|
|
11
|
+
this.resolveReady = resolve;
|
|
12
|
+
this.rejectReady = reject;
|
|
13
|
+
});
|
|
14
|
+
socket.onopen = () => this.resolveReady(this);
|
|
15
|
+
socket.onmessage = event => {
|
|
16
|
+
try { this.queue.push(JSON.parse(event.data)); } catch { return; }
|
|
17
|
+
this.wake();
|
|
18
|
+
};
|
|
19
|
+
socket.onclose = event => {
|
|
20
|
+
this.closed = true;
|
|
21
|
+
if (event.code !== 1000 && event.code !== undefined) this.failure ||= new Error(`Events socket closed (${event.code})`);
|
|
22
|
+
this.rejectReady(this.failure || new Error('Events socket closed before opening'));
|
|
23
|
+
this.wake();
|
|
24
|
+
};
|
|
25
|
+
socket.onerror = () => {
|
|
26
|
+
this.failure = new Error('Events socket error');
|
|
27
|
+
this.closed = true;
|
|
28
|
+
this.rejectReady(this.failure);
|
|
29
|
+
this.close();
|
|
30
|
+
this.wake();
|
|
31
|
+
};
|
|
32
|
+
if (socket.readyState === 1) this.resolveReady(this);
|
|
33
|
+
}
|
|
34
|
+
wake() { for (const resolve of this.waiters.splice(0)) resolve(); }
|
|
35
|
+
async send(type, payload = {}) {
|
|
36
|
+
await this.ready;
|
|
37
|
+
if (this.closed || this.socket.readyState !== 1) throw new Error('Events socket is not open');
|
|
38
|
+
this.socket.send(JSON.stringify({ type, payload }));
|
|
39
|
+
}
|
|
40
|
+
message(text) { return this.send('message', { text }); }
|
|
41
|
+
interrupt() { return this.send('interrupt'); }
|
|
42
|
+
updateContext(context) { return this.send('context.update', context); }
|
|
43
|
+
startPresentation(start) { return this.send('presentation.start', start); }
|
|
44
|
+
turnDelta(turnId, text) { return this.send('turn.delta', { turn_id: turnId, text }); }
|
|
45
|
+
turnDone(turnId) { return this.send('turn.done', { turn_id: turnId }); }
|
|
46
|
+
turnCancel(turnId, reason = '') { return this.send('turn.cancel', { turn_id: turnId, reason }); }
|
|
47
|
+
startSpeech(turnId, { text } = {}) { return this.send('speech.start', speechStart(turnId, text)); }
|
|
48
|
+
appendSpeech(turnId, seq, startSample, pcm) { return this.send('speech.append', speechAppend(turnId, seq, startSample, pcm)); }
|
|
49
|
+
finishSpeech(turnId, totalSamples) { return this.send('speech.done', { turn_id: turnId, total_samples: totalSamples }); }
|
|
50
|
+
cancelSpeech(turnId) { return this.send('speech.cancel', { turn_id: turnId }); }
|
|
51
|
+
close() {
|
|
52
|
+
if (this.closeRequested) return;
|
|
53
|
+
this.closeRequested = true;
|
|
54
|
+
this.closed = true;
|
|
55
|
+
this.rejectReady(new Error('Events socket closed before opening'));
|
|
56
|
+
try { this.socket.close(1000); } finally { this.wake(); }
|
|
57
|
+
}
|
|
58
|
+
async *[Symbol.asyncIterator]() {
|
|
59
|
+
await this.ready;
|
|
60
|
+
try {
|
|
61
|
+
while (!this.closed || this.queue.length) {
|
|
62
|
+
if (!this.queue.length) { await new Promise(resolve => this.waiters.push(resolve)); continue; }
|
|
63
|
+
yield this.queue.shift();
|
|
64
|
+
}
|
|
65
|
+
if (this.failure) throw this.failure;
|
|
66
|
+
} finally { this.close(); }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const speechStart = (turnId, text) => ({ turn_id: turnId, codec: 'pcm_s16le', sample_rate: 24000, channels: 1, ...(text === undefined ? {} : { text }) });
|
|
70
|
+
export function speechAppend(turnId, seq, startSample, pcm) {
|
|
71
|
+
const bytes = pcm instanceof Uint8Array ? pcm : pcm instanceof ArrayBuffer ? new Uint8Array(pcm) : null;
|
|
72
|
+
if (!bytes || bytes.length === 0 || bytes.length % 2 || bytes.length > 24000) throw new Error('PCM chunk must contain 1–12000 signed 16-bit samples');
|
|
73
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || !Number.isSafeInteger(startSample) || startSample < 0) throw new Error('seq and startSample must be nonnegative integers');
|
|
74
|
+
let binary = '';
|
|
75
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
76
|
+
const data = typeof globalThis.btoa === 'function' ? globalThis.btoa(binary) : globalThis.Buffer.from(bytes).toString('base64');
|
|
77
|
+
return { turn_id: turnId, seq, start_sample: startSample, data };
|
|
78
|
+
}
|