UNPKG

@sayna-ai/node-sdk

Version:

Node.js SDK for Sayna.ai server-side WebSocket connections

391 lines 15.2 kB
import type { STTConfig, TTSConfig, LiveKitConfig, STTResultMessage, ErrorMessage, SaynaMessage, Participant, VoicesResponse, HealthResponse, LiveKitTokenResponse, SipHook, SipHooksResponse } from "./types"; /** * Event handler for speech-to-text results. */ export type STTResultHandler = (result: STTResultMessage) => void | Promise<void>; /** * Event handler for text-to-speech audio data. */ export type TTSAudioHandler = (audio: ArrayBuffer) => void | Promise<void>; /** * Event handler for error messages. */ export type ErrorHandler = (error: ErrorMessage) => void | Promise<void>; /** * Event handler for participant messages. */ export type MessageHandler = (message: SaynaMessage) => void | Promise<void>; /** * Event handler for participant disconnections. */ export type ParticipantDisconnectedHandler = (participant: Participant) => void | Promise<void>; /** * Event handler for TTS playback completion. */ export type TTSPlaybackCompleteHandler = (timestamp: number) => void | Promise<void>; /** * Client for connecting to Sayna WebSocket server for real-time voice interactions. * * @example * ```typescript * const client = await saynaConnect( * "https://api.sayna.ai", * sttConfig, * ttsConfig * ); * * client.registerOnSttResult((result) => { * console.log("Transcription:", result.transcript); * }); * * await client.speak("Hello, world!"); * ``` */ export declare class SaynaClient { private url; private sttConfig?; private ttsConfig?; private livekitConfig?; private withoutAudio; private apiKey?; private websocket?; private isConnected; private isReady; private _livekitRoomName?; private _livekitUrl?; private _saynaParticipantIdentity?; private _saynaParticipantName?; private _streamId?; private inputStreamId?; private sttCallback?; private ttsCallback?; private errorCallback?; private messageCallback?; private participantDisconnectedCallback?; private ttsPlaybackCompleteCallback?; private readyPromiseResolve?; private readyPromiseReject?; /** * Creates a new SaynaClient instance. * * @param url - The Sayna server URL (e.g., "https://api.sayna.ai") * @param sttConfig - Speech-to-text configuration (required when withoutAudio=false) * @param ttsConfig - Text-to-speech configuration (required when withoutAudio=false) * @param livekitConfig - Optional LiveKit room configuration * @param withoutAudio - If true, disables audio streaming (default: false) * @param apiKey - Optional API key used to authorize HTTP and WebSocket calls (defaults to SAYNA_API_KEY env) * @param streamId - Optional session identifier for recording paths; server generates a UUID when omitted * * @throws {SaynaValidationError} If URL is invalid or if audio configs are missing when audio is enabled */ constructor(url: string, sttConfig?: STTConfig, ttsConfig?: TTSConfig, livekitConfig?: LiveKitConfig, withoutAudio?: boolean, apiKey?: string, streamId?: string); /** * Establishes connection to the Sayna WebSocket server. * * @throws {SaynaConnectionError} If connection fails * @returns Promise that resolves when the connection is ready */ connect(): Promise<void>; /** * Handles incoming JSON messages from the WebSocket. * @internal */ private handleJsonMessage; /** * Cleans up internal state. * @internal */ private cleanup; /** * Converts WebSocket URL to HTTP URL for REST API calls. * @internal */ private getHttpUrl; /** * Generic fetch helper for making REST API calls to Sayna server. * Handles URL construction, headers, error responses, and type conversion. * @internal * * @param endpoint - API endpoint path (e.g., "/voices", "/speak") * @param options - Fetch options including method, body, headers, etc. * @param responseType - Expected response type: "json" or "arrayBuffer" * @returns Promise resolving to the parsed response * @throws {SaynaConnectionError} If the network request fails * @throws {SaynaServerError} If the server returns an error response */ private fetchFromSayna; /** * Disconnects from the Sayna WebSocket server and cleans up resources. */ disconnect(): void; /** * Sends audio data to the server for speech recognition. * * @param audioData - Raw audio data as ArrayBuffer * @throws {SaynaNotConnectedError} If not connected * @throws {SaynaNotReadyError} If connection is not ready */ onAudioInput(audioData: ArrayBuffer): void; /** * Registers a callback for speech-to-text results. * * @param callback - Function to call when STT results are received */ registerOnSttResult(callback: STTResultHandler): void; /** * Registers a callback for text-to-speech audio data. * * @param callback - Function to call when TTS audio is received */ registerOnTtsAudio(callback: TTSAudioHandler): void; /** * Registers a callback for error messages. * * @param callback - Function to call when errors occur */ registerOnError(callback: ErrorHandler): void; /** * Registers a callback for participant messages. * * @param callback - Function to call when messages are received */ registerOnMessage(callback: MessageHandler): void; /** * Registers a callback for participant disconnection events. * * @param callback - Function to call when a participant disconnects */ registerOnParticipantDisconnected(callback: ParticipantDisconnectedHandler): void; /** * Registers a callback for TTS playback completion. * * @param callback - Function to call when TTS playback is complete */ registerOnTtsPlaybackComplete(callback: TTSPlaybackCompleteHandler): void; /** * Sends text to be synthesized as speech. * * @param text - Text to synthesize * @param flush - Whether to flush the TTS queue before speaking (default: true) * @param allowInterruption - Whether this speech can be interrupted (default: true) * @throws {SaynaNotConnectedError} If not connected * @throws {SaynaNotReadyError} If connection is not ready * @throws {SaynaValidationError} If text is not a string */ speak(text: string, flush?: boolean, allowInterruption?: boolean): void; /** * Clears the text-to-speech queue. * * @throws {SaynaNotConnectedError} If not connected * @throws {SaynaNotReadyError} If connection is not ready */ clear(): void; /** * Flushes the TTS queue by sending an empty speak command. * * @param allowInterruption - Whether the flush can be interrupted (default: true) * @throws {SaynaNotConnectedError} If not connected * @throws {SaynaNotReadyError} If connection is not ready */ ttsFlush(allowInterruption?: boolean): void; /** * Sends a message to the Sayna session. * * @param message - Message content * @param role - Message role (e.g., "user", "assistant") * @param topic - Optional topic identifier * @param debug - Optional debug metadata * @throws {SaynaNotConnectedError} If not connected * @throws {SaynaNotReadyError} If connection is not ready * @throws {SaynaValidationError} If parameters are invalid */ sendMessage(message: string, role: string, topic?: string, debug?: Record<string, unknown>): void; /** * Performs a health check on the Sayna server. * * @returns Promise that resolves with the health status * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error * * @example * ```typescript * const health = await client.health(); * console.log(health.status); // "OK" * ``` */ health(): Promise<HealthResponse>; /** * Retrieves the catalogue of text-to-speech voices grouped by provider. * * @returns Promise that resolves with voices organized by provider * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error * * @example * ```typescript * const voices = await client.getVoices(); * for (const [provider, voiceList] of Object.entries(voices)) { * console.log(`${provider}:`, voiceList.map(v => v.name)); * } * ``` */ getVoices(): Promise<VoicesResponse>; /** * Synthesizes text into audio using the REST API endpoint. * This is a standalone synthesis method that doesn't require an active WebSocket connection. * * @param text - Text to synthesize * @param ttsConfig - Text-to-speech configuration * @returns Promise that resolves with the audio data as ArrayBuffer * @throws {SaynaValidationError} If text is empty * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error * * @example * ```typescript * const audioBuffer = await client.speakRest("Hello, world!", { * provider: "elevenlabs", * voice_id: "21m00Tcm4TlvDq8ikWAM", * model: "eleven_turbo_v2", * speaking_rate: 1.0, * audio_format: "mp3", * sample_rate: 24000, * connection_timeout: 30, * request_timeout: 60, * pronunciations: [] * }); * ``` */ speakRest(text: string, ttsConfig: TTSConfig): Promise<ArrayBuffer>; /** * Issues a LiveKit access token for a participant. * * @param roomName - LiveKit room to join or create * @param participantName - Display name assigned to the participant * @param participantIdentity - Unique identifier for the participant * @returns Promise that resolves with the LiveKit token and connection details * @throws {SaynaValidationError} If any parameter is empty * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error * * @example * ```typescript * const tokenInfo = await client.getLiveKitToken( * "my-room", * "John Doe", * "user-123" * ); * console.log("Token:", tokenInfo.token); * console.log("LiveKit URL:", tokenInfo.livekit_url); * ``` */ getLiveKitToken(roomName: string, participantName: string, participantIdentity: string): Promise<LiveKitTokenResponse>; /** * Downloads the recorded audio file for a completed session. * * @param streamId - The session identifier (obtained from the `streamId` getter after connection) * @returns Promise that resolves with the audio data as ArrayBuffer (OGG format) * @throws {SaynaValidationError} If streamId is empty * @throws {SaynaConnectionError} If the network request fails * @throws {SaynaServerError} If the recording is not found or server returns an error * * @example * ```typescript * // After a session completes, download the recording * const audioBuffer = await client.getRecording(client.streamId!); * * // Save to file (Node.js) * import { writeFile } from "fs/promises"; * await writeFile("recording.ogg", Buffer.from(audioBuffer)); * ``` */ getRecording(streamId: string): Promise<ArrayBuffer>; /** * Retrieves all configured SIP webhook hooks from the runtime cache. * * @returns Promise that resolves with the list of configured SIP hooks * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error (e.g., 500 if reading cache fails) * * @example * ```typescript * const response = await client.getSipHooks(); * for (const hook of response.hooks) { * console.log(`Host: ${hook.host}, URL: ${hook.url}`); * } * ``` */ getSipHooks(): Promise<SipHooksResponse>; /** * Sets or updates SIP webhook hooks in the runtime cache. * * Hooks with matching hosts will be replaced; new hosts will be added. * The response contains the merged list of all hooks (existing + new). * * @param hooks - Array of SIP hook configurations to add or replace * @returns Promise that resolves with the merged list of all configured hooks * @throws {SaynaValidationError} If hooks array is empty or contains invalid entries * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error (e.g., 400 for duplicate hosts, 500 for cache errors) * * @example * ```typescript * const response = await client.setSipHooks([ * { host: "example.com", url: "https://webhook.example.com/events" }, * { host: "another.com", url: "https://webhook.another.com/events" } * ]); * console.log("Total hooks configured:", response.hooks.length); * ``` */ setSipHooks(hooks: SipHook[]): Promise<SipHooksResponse>; /** * Deletes SIP webhook hooks by host name from the runtime cache. * * If a deleted host exists in the original server configuration, * it will revert to its config value after deletion. * * @param hosts - Array of host names to remove (case-insensitive) * @returns Promise that resolves with the updated list of hooks after deletion * @throws {SaynaValidationError} If hosts array is empty or contains invalid entries * @throws {SaynaConnectionError} If the request fails * @throws {SaynaServerError} If server returns an error (e.g., 400 for empty hosts, 500 for cache errors) * * @example * ```typescript * const response = await client.deleteSipHooks(["example.com", "another.com"]); * console.log("Remaining hooks:", response.hooks.length); * ``` */ deleteSipHooks(hosts: string[]): Promise<SipHooksResponse>; /** * Whether the client is ready to send/receive data. */ get ready(): boolean; /** * Whether the client is connected to the WebSocket. */ get connected(): boolean; /** * LiveKit room name acknowledged by the server, if available. */ get livekitRoomName(): string | undefined; /** * LiveKit WebSocket URL configured on the server, if available. */ get livekitUrl(): string | undefined; /** * Identity assigned to the agent participant when LiveKit is enabled, if available. */ get saynaParticipantIdentity(): string | undefined; /** * Display name assigned to the agent participant when LiveKit is enabled, if available. */ get saynaParticipantName(): string | undefined; /** * Session identifier returned by the server. * This can be used to download recordings or correlate session data. * The value is available after the connection is ready. */ get streamId(): string | undefined; } //# sourceMappingURL=sayna-client.d.ts.map