@cloudflare/voice-telnyx 0.0.0 → 0.0.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stt-DPden1_S.js","names":[],"sources":["../src/providers/stt.ts"],"sourcesContent":["/**\n * Telnyx STT provider for the Cloudflare Agents SDK.\n *\n * Implements the Transcriber interface from @cloudflare/voice,\n * streaming audio to the Telnyx WebSocket STT API.\n */\n\nimport type { Transcriber, TranscriberSession } from \"@cloudflare/voice\";\nimport { TelnyxClient, type TelnyxClientConfig } from \"../client.js\";\n\nconst DEFAULT_STT_URL = \"wss://api.telnyx.com/v2/speech-to-text/transcription\";\n\n/**\n * Build a 44-byte WAV header for streaming PCM16 audio.\n * The data-size field is set to 0x7FFFFFFF since the total length is unknown.\n */\nfunction wavHeader(sampleRate: number, channels: number): ArrayBuffer {\n const bitsPerSample = 16;\n const byteRate = sampleRate * channels * (bitsPerSample / 8);\n const blockAlign = channels * (bitsPerSample / 8);\n const buf = new ArrayBuffer(44);\n const v = new DataView(buf);\n\n // RIFF header\n v.setUint32(0, 0x52494646); // \"RIFF\"\n v.setUint32(4, 0x7fffffff, true); // file size (unknown, max)\n v.setUint32(8, 0x57415645); // \"WAVE\"\n\n // fmt chunk\n v.setUint32(12, 0x666d7420); // \"fmt \"\n v.setUint32(16, 16, true); // chunk size\n v.setUint16(20, 1, true); // PCM format\n v.setUint16(22, channels, true);\n v.setUint32(24, sampleRate, true);\n v.setUint32(28, byteRate, true);\n v.setUint16(32, blockAlign, true);\n v.setUint16(34, bitsPerSample, true);\n\n // data chunk\n v.setUint32(36, 0x64617461); // \"data\"\n v.setUint32(40, 0x7fffffff, true); // data size (unknown, max)\n\n return buf;\n}\n\nexport interface TelnyxSTTConfig extends TelnyxClientConfig {\n /** STT engine (default: \"Telnyx\") */\n engine?: string;\n /** Language code for transcription (default: \"en\") */\n language?: string;\n /**\n * Audio input format sent to the Telnyx API.\n * @default \"wav\"\n *\n * The Cloudflare voice pipeline delivers raw PCM16 chunks. The Telnyx STT\n * API does not accept raw PCM — it requires a container format. When set to\n * \"wav\" (the default), the session automatically prepends a WAV header so\n * the raw PCM stream is valid.\n */\n inputFormat?: string;\n /** Deepgram model when engine is \"Deepgram\" (e.g., \"nova-3\", \"flux\") */\n transcriptionModel?: string;\n /** Enable interim results (default: true) */\n interimResults?: boolean;\n /**\n * Override the WebSocket URL for the STT streaming endpoint.\n * @default \"wss://api.telnyx.com/v2/speech-to-text/transcription\"\n */\n sttWsUrl?: string;\n}\n\nexport interface TelnyxSTTSessionOptions {\n language?: string;\n onInterim?: (text: string) => void;\n onUtterance?: (transcript: string) => void;\n}\n\nexport class TelnyxSTT implements Transcriber {\n private client: TelnyxClient;\n private engine: string;\n private language: string;\n private inputFormat: string;\n private transcriptionModel?: string;\n private interimResults: boolean;\n private sttUrl: string;\n\n constructor(config: TelnyxSTTConfig) {\n this.client = new TelnyxClient(config);\n this.engine = config.engine ?? \"Telnyx\";\n this.language = config.language ?? \"en\";\n this.inputFormat = config.inputFormat ?? \"wav\";\n this.transcriptionModel = config.transcriptionModel;\n this.interimResults = config.interimResults ?? true;\n this.sttUrl = config.sttWsUrl ?? DEFAULT_STT_URL;\n }\n\n createSession(options?: TelnyxSTTSessionOptions): TelnyxSTTSession {\n const language = options?.language ?? this.language;\n return new TelnyxSTTSession({\n apiKey: this.client.apiKey,\n sttUrl: this.sttUrl,\n engine: this.engine,\n inputFormat: this.inputFormat,\n transcriptionModel: this.transcriptionModel,\n interimResults: this.interimResults,\n language,\n onInterim: options?.onInterim,\n onUtterance: options?.onUtterance\n });\n }\n}\n\ninterface SessionParams {\n apiKey: string;\n sttUrl: string;\n engine: string;\n inputFormat: string;\n transcriptionModel?: string;\n interimResults: boolean;\n language: string;\n onInterim?: (text: string) => void;\n onUtterance?: (transcript: string) => void;\n}\n\nexport class TelnyxSTTSession implements TranscriberSession {\n private ws: WebSocket | null = null;\n private pendingChunks: ArrayBuffer[] = [];\n private closed = false;\n private sentWavHeader = false;\n private inputFormat: string;\n private onInterim?: (text: string) => void;\n private onUtterance?: (transcript: string) => void;\n\n constructor(params: SessionParams) {\n this.onInterim = params.onInterim;\n this.onUtterance = params.onUtterance;\n this.inputFormat = params.inputFormat;\n\n const url = new URL(params.sttUrl);\n url.searchParams.set(\"transcription_engine\", params.engine);\n url.searchParams.set(\"input_format\", params.inputFormat);\n url.searchParams.set(\"language\", params.language);\n url.searchParams.set(\"interim_results\", String(params.interimResults));\n if (params.transcriptionModel) {\n url.searchParams.set(\"transcription_model\", params.transcriptionModel);\n }\n url.searchParams.set(\"token\", params.apiKey);\n\n this.connect(url.toString(), params.apiKey);\n }\n\n private async connect(wsUrl: string, apiKey: string): Promise<void> {\n try {\n // Use the Cloudflare Workers fetch-upgrade pattern.\n const resp = await fetch(wsUrl.replace(\"wss://\", \"https://\"), {\n headers: {\n Upgrade: \"websocket\",\n Authorization: `Bearer ${apiKey}`\n }\n });\n\n const ws = (resp as unknown as { webSocket?: WebSocket }).webSocket;\n if (!ws) {\n throw new Error(\n \"STT WebSocket requires the Cloudflare Workers runtime. \" +\n \"The fetch-upgrade did not return a WebSocket pair.\"\n );\n }\n\n if (this.closed) {\n try {\n (ws as unknown as { accept: () => void }).accept();\n ws.close();\n } catch {\n // ignore cleanup errors for a session that was closed while connecting\n }\n return;\n }\n\n // Register listeners BEFORE accepting the connection to avoid\n // a race where frames arrive between accept() and addEventListener().\n ws.addEventListener(\"message\", (event: MessageEvent) => {\n this.handleMessage(event);\n });\n\n ws.addEventListener(\"error\", (event: Event) => {\n console.error(\"[TelnyxSTT] WebSocket error:\", event);\n this.closed = true;\n });\n\n ws.addEventListener(\"close\", () => {\n this.closed = true;\n });\n\n // Now accept — listeners are already in place.\n (ws as unknown as { accept: () => void }).accept();\n\n if (this.closed) return;\n\n this.ws = ws;\n\n // When using wav format, send the WAV header before any PCM data\n // so the API knows the sample rate, bit depth, and channel count.\n if (this.inputFormat === \"wav\" && !this.sentWavHeader) {\n ws.send(wavHeader(16_000, 1));\n this.sentWavHeader = true;\n }\n\n // Flush any chunks buffered while the connection was being established.\n for (const chunk of this.pendingChunks) {\n ws.send(chunk);\n }\n this.pendingChunks = [];\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cloudflare Workers\")\n ) {\n console.error(`[TelnyxSTT] ${error.message}`);\n } else {\n console.error(\"[TelnyxSTT] WebSocket connection failed:\", error);\n }\n this.closed = true;\n }\n }\n\n feed(chunk: ArrayBuffer): void {\n if (this.closed) return;\n\n if (this.ws) {\n this.ws.send(chunk);\n } else {\n this.pendingChunks.push(chunk);\n }\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.pendingChunks = [];\n this.ws?.close();\n }\n\n private handleMessage(event: MessageEvent): void {\n if (this.closed) return;\n let data: { transcript?: string; is_final?: boolean };\n try {\n data = JSON.parse(event.data as string);\n } catch {\n return;\n }\n\n if (typeof data.transcript !== \"string\" || data.transcript === \"\") return;\n\n if (data.is_final) {\n this.onUtterance?.(data.transcript);\n } else {\n this.onInterim?.(data.transcript);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { n as TelnyxClientConfig } from "./client-tnkkrw_G.js";
2
+ import { Transcriber, TranscriberSession } from "@cloudflare/voice";
3
+
4
+ //#region src/providers/stt.d.ts
5
+ interface TelnyxSTTConfig extends TelnyxClientConfig {
6
+ /** STT engine (default: "Telnyx") */
7
+ engine?: string;
8
+ /** Language code for transcription (default: "en") */
9
+ language?: string;
10
+ /**
11
+ * Audio input format sent to the Telnyx API.
12
+ * @default "wav"
13
+ *
14
+ * The Cloudflare voice pipeline delivers raw PCM16 chunks. The Telnyx STT
15
+ * API does not accept raw PCM — it requires a container format. When set to
16
+ * "wav" (the default), the session automatically prepends a WAV header so
17
+ * the raw PCM stream is valid.
18
+ */
19
+ inputFormat?: string;
20
+ /** Deepgram model when engine is "Deepgram" (e.g., "nova-3", "flux") */
21
+ transcriptionModel?: string;
22
+ /** Enable interim results (default: true) */
23
+ interimResults?: boolean;
24
+ /**
25
+ * Override the WebSocket URL for the STT streaming endpoint.
26
+ * @default "wss://api.telnyx.com/v2/speech-to-text/transcription"
27
+ */
28
+ sttWsUrl?: string;
29
+ }
30
+ interface TelnyxSTTSessionOptions {
31
+ language?: string;
32
+ onInterim?: (text: string) => void;
33
+ onUtterance?: (transcript: string) => void;
34
+ }
35
+ declare class TelnyxSTT implements Transcriber {
36
+ private client;
37
+ private engine;
38
+ private language;
39
+ private inputFormat;
40
+ private transcriptionModel?;
41
+ private interimResults;
42
+ private sttUrl;
43
+ constructor(config: TelnyxSTTConfig);
44
+ createSession(options?: TelnyxSTTSessionOptions): TelnyxSTTSession;
45
+ }
46
+ interface SessionParams {
47
+ apiKey: string;
48
+ sttUrl: string;
49
+ engine: string;
50
+ inputFormat: string;
51
+ transcriptionModel?: string;
52
+ interimResults: boolean;
53
+ language: string;
54
+ onInterim?: (text: string) => void;
55
+ onUtterance?: (transcript: string) => void;
56
+ }
57
+ declare class TelnyxSTTSession implements TranscriberSession {
58
+ private ws;
59
+ private pendingChunks;
60
+ private closed;
61
+ private sentWavHeader;
62
+ private inputFormat;
63
+ private onInterim?;
64
+ private onUtterance?;
65
+ constructor(params: SessionParams);
66
+ private connect;
67
+ feed(chunk: ArrayBuffer): void;
68
+ close(): void;
69
+ private handleMessage;
70
+ }
71
+ //#endregion
72
+ export { TelnyxSTTConfig as n, TelnyxSTTSessionOptions as r, TelnyxSTT as t };
73
+ //# sourceMappingURL=stt-HkxzilR4.d.ts.map
package/dist/stt.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ n as TelnyxSTTConfig,
3
+ r as TelnyxSTTSessionOptions,
4
+ t as TelnyxSTT
5
+ } from "./stt-HkxzilR4.js";
6
+ export { TelnyxSTT, type TelnyxSTTConfig, type TelnyxSTTSessionOptions };
package/dist/stt.js ADDED
@@ -0,0 +1,2 @@
1
+ import { t as TelnyxSTT } from "./stt-DPden1_S.js";
2
+ export { TelnyxSTT };
@@ -0,0 +1,181 @@
1
+ import { t as TelnyxClient } from "./client-C14kiRwB.js";
2
+ //#region src/providers/tts.ts
3
+ /** Default voice when none is specified in config. */
4
+ const DEFAULT_VOICE = "Telnyx.NaturalHD.astra";
5
+ const TTS_PATH = "/text-to-speech/speech";
6
+ const DEFAULT_TTS_WS_URL = "wss://api.telnyx.com/v2/text-to-speech/speech";
7
+ var TelnyxTTS = class {
8
+ constructor(config) {
9
+ this.client = new TelnyxClient(config);
10
+ this.voice = config.voice ?? DEFAULT_VOICE;
11
+ this.backend = config.backend ?? "rest";
12
+ this.ttsWsUrl = config.ttsWsUrl ?? DEFAULT_TTS_WS_URL;
13
+ }
14
+ async synthesize(text, signal) {
15
+ if (!text?.trim()) return null;
16
+ try {
17
+ if (this.backend === "websocket") return await this.synthesizeViaWS(text, signal);
18
+ return await this.synthesizeViaREST(text, signal);
19
+ } catch (error) {
20
+ if (signal?.aborted) return null;
21
+ console.error("[TelnyxTTS] synthesize error:", error);
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Stream synthesized audio chunks.
27
+ *
28
+ * With the REST backend this yields a single buffered chunk (the complete
29
+ * audio); only the WebSocket backend provides true incremental streaming.
30
+ */
31
+ async *synthesizeStream(text, signal) {
32
+ if (!text?.trim()) return;
33
+ try {
34
+ if (this.backend === "websocket") yield* this.streamViaWS(text, signal);
35
+ else {
36
+ const audio = await this.synthesizeViaREST(text, signal);
37
+ if (audio) yield audio;
38
+ }
39
+ } catch (error) {
40
+ if (signal?.aborted) return;
41
+ console.error("[TelnyxTTS] synthesizeStream error:", error);
42
+ }
43
+ }
44
+ async synthesizeViaREST(text, signal) {
45
+ const response = await fetch(`${this.client.baseUrl}${TTS_PATH}`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ Authorization: `Bearer ${this.client.apiKey}`
50
+ },
51
+ body: JSON.stringify({
52
+ text,
53
+ voice: this.voice
54
+ }),
55
+ signal
56
+ });
57
+ if (!response.ok) {
58
+ const err = await response.text().catch(() => "unknown");
59
+ console.error(`[TelnyxTTS] REST error: ${response.status} — ${err}`);
60
+ return null;
61
+ }
62
+ return await response.arrayBuffer();
63
+ }
64
+ async synthesizeViaWS(text, signal) {
65
+ const chunks = [];
66
+ for await (const chunk of this.streamViaWS(text, signal)) chunks.push(chunk);
67
+ if (chunks.length === 0) return null;
68
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
69
+ const result = new Uint8Array(totalLength);
70
+ let offset = 0;
71
+ for (const chunk of chunks) {
72
+ result.set(new Uint8Array(chunk), offset);
73
+ offset += chunk.byteLength;
74
+ }
75
+ return result.buffer;
76
+ }
77
+ async *streamViaWS(text, signal) {
78
+ const url = `${this.ttsWsUrl}?voice=${encodeURIComponent(this.voice)}`;
79
+ let ws;
80
+ try {
81
+ const pair = (await fetch(url.replace("wss://", "https://"), { headers: {
82
+ Upgrade: "websocket",
83
+ Authorization: `Bearer ${this.client.apiKey}`
84
+ } })).webSocket;
85
+ if (!pair) throw new Error("WebSocket backend requires the Cloudflare Workers runtime. The fetch-upgrade did not return a WebSocket pair. Use backend: \"rest\" outside of Workers.");
86
+ ws = pair;
87
+ } catch (error) {
88
+ if (error instanceof Error && error.message.includes("Cloudflare Workers")) console.error(`[TelnyxTTS] ${error.message}`);
89
+ else console.error("[TelnyxTTS] WebSocket connection failed:", error);
90
+ return;
91
+ }
92
+ const queue = [];
93
+ let done = false;
94
+ let wsError = null;
95
+ let resolveWait = null;
96
+ const waitForData = () => new Promise((resolve) => {
97
+ if (queue.length > 0 || done) resolve();
98
+ else resolveWait = resolve;
99
+ });
100
+ const notify = () => {
101
+ if (resolveWait) {
102
+ const r = resolveWait;
103
+ resolveWait = null;
104
+ r();
105
+ }
106
+ };
107
+ ws.addEventListener("message", (event) => {
108
+ try {
109
+ const frame = typeof event.data === "string" ? JSON.parse(event.data) : null;
110
+ if (!frame) return;
111
+ if (frame.isFinal) {
112
+ done = true;
113
+ notify();
114
+ return;
115
+ }
116
+ if (frame.audio && frame.text === null) {
117
+ const binary = base64ToArrayBuffer(frame.audio);
118
+ if (binary.byteLength > 0) {
119
+ queue.push(binary);
120
+ notify();
121
+ }
122
+ }
123
+ } catch {}
124
+ });
125
+ ws.addEventListener("error", () => {
126
+ wsError = "WebSocket error";
127
+ done = true;
128
+ notify();
129
+ });
130
+ ws.addEventListener("close", () => {
131
+ done = true;
132
+ notify();
133
+ });
134
+ ws.accept();
135
+ const onAbort = () => {
136
+ done = true;
137
+ try {
138
+ ws.close();
139
+ } catch {}
140
+ notify();
141
+ };
142
+ if (signal) {
143
+ if (signal.aborted) {
144
+ try {
145
+ ws.close();
146
+ } catch {}
147
+ return;
148
+ }
149
+ signal.addEventListener("abort", onAbort, { once: true });
150
+ }
151
+ try {
152
+ ws.send(JSON.stringify({ text: " " }));
153
+ ws.send(JSON.stringify({ text }));
154
+ ws.send(JSON.stringify({ text: "" }));
155
+ while (!signal?.aborted) {
156
+ await waitForData();
157
+ while (queue.length > 0) {
158
+ const chunk = queue.shift();
159
+ if (!signal?.aborted) yield chunk;
160
+ }
161
+ if (done) break;
162
+ }
163
+ if (wsError) console.error(`[TelnyxTTS] ${wsError}`);
164
+ } finally {
165
+ signal?.removeEventListener("abort", onAbort);
166
+ try {
167
+ ws.close();
168
+ } catch {}
169
+ }
170
+ }
171
+ };
172
+ function base64ToArrayBuffer(base64) {
173
+ const binary = atob(base64);
174
+ const bytes = new Uint8Array(binary.length);
175
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
176
+ return bytes.buffer;
177
+ }
178
+ //#endregion
179
+ export { TelnyxTTS as t };
180
+
181
+ //# sourceMappingURL=tts-BNsM0WUr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tts-BNsM0WUr.js","names":[],"sources":["../src/providers/tts.ts"],"sourcesContent":["/**\n * Telnyx TTS provider for the Cloudflare Agents SDK.\n *\n * Implements TTSProvider and StreamingTTSProvider from @cloudflare/voice,\n * with two backend options:\n *\n * - **REST** (default): `POST /v2/text-to-speech/speech` — one HTTP request\n * per sentence, returns complete mp3 audio. Works with all voices including Ultra.\n *\n * - **WebSocket**: `wss://api.telnyx.com/v2/text-to-speech/speech` — streams\n * audio chunks as they're synthesized for lower time-to-first-audio.\n * **Requires the Cloudflare Workers runtime** (uses the fetch-upgrade pattern).\n * Does NOT support Telnyx Ultra voices.\n *\n * Audio format: MP3 by default — matches the Cloudflare voice pipeline's\n * default `audioFormat: \"mp3\"` setting. The Telnyx REST API returns MP3\n * regardless of format parameters.\n */\n\nimport type { TTSProvider, StreamingTTSProvider } from \"@cloudflare/voice\";\nimport { TelnyxClient, type TelnyxClientConfig } from \"../client.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface TelnyxTTSConfig extends TelnyxClientConfig {\n /**\n * Voice identifier.\n * @default \"Telnyx.NaturalHD.astra\"\n *\n * Examples: `Telnyx.NaturalHD.luna`, `Telnyx.Ultra.<id>`, `Azure.en-US-AvaMultilingualNeural`\n */\n voice?: string;\n /**\n * Backend to use.\n *\n * - `\"rest\"` (default): HTTP POST per sentence. Works everywhere, all voices.\n * - `\"websocket\"`: Streams audio chunks for lower time-to-first-audio.\n * Requires the Cloudflare Workers runtime.\n *\n * @default \"rest\"\n */\n backend?: \"rest\" | \"websocket\";\n /**\n * Override the WebSocket URL for the TTS streaming backend.\n * Only used when `backend` is `\"websocket\"`.\n * @default \"wss://api.telnyx.com/v2/text-to-speech/speech\"\n */\n ttsWsUrl?: string;\n}\n\n/** Frame received from the Telnyx TTS WebSocket. */\ninterface TelnyxWSFrame {\n audio?: string | null;\n text?: string | null;\n isFinal?: boolean;\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Default voice when none is specified in config. */\nconst DEFAULT_VOICE = \"Telnyx.NaturalHD.astra\";\nconst TTS_PATH = \"/text-to-speech/speech\";\nconst DEFAULT_TTS_WS_URL = \"wss://api.telnyx.com/v2/text-to-speech/speech\";\n\n// ─── Implementation ─────────────────────────────────────────────────────────\n\nexport class TelnyxTTS implements TTSProvider, StreamingTTSProvider {\n private client: TelnyxClient;\n private voice: string;\n private backend: \"rest\" | \"websocket\";\n private ttsWsUrl: string;\n\n constructor(config: TelnyxTTSConfig) {\n this.client = new TelnyxClient(config);\n this.voice = config.voice ?? DEFAULT_VOICE;\n this.backend = config.backend ?? \"rest\";\n this.ttsWsUrl = config.ttsWsUrl ?? DEFAULT_TTS_WS_URL;\n }\n\n // ─── TTSProvider ───────────────────────────────────────────────────────\n\n async synthesize(\n text: string,\n signal?: AbortSignal\n ): Promise<ArrayBuffer | null> {\n if (!text?.trim()) return null;\n\n try {\n if (this.backend === \"websocket\") {\n return await this.synthesizeViaWS(text, signal);\n }\n return await this.synthesizeViaREST(text, signal);\n } catch (error) {\n if (signal?.aborted) return null;\n console.error(\"[TelnyxTTS] synthesize error:\", error);\n return null;\n }\n }\n\n // ─── StreamingTTSProvider ──────────────────────────────────────────────\n\n /**\n * Stream synthesized audio chunks.\n *\n * With the REST backend this yields a single buffered chunk (the complete\n * audio); only the WebSocket backend provides true incremental streaming.\n */\n async *synthesizeStream(\n text: string,\n signal?: AbortSignal\n ): AsyncGenerator<ArrayBuffer> {\n if (!text?.trim()) return;\n\n try {\n if (this.backend === \"websocket\") {\n yield* this.streamViaWS(text, signal);\n } else {\n // REST cannot stream — fetch complete audio then yield as one chunk.\n const audio = await this.synthesizeViaREST(text, signal);\n if (audio) yield audio;\n }\n } catch (error) {\n if (signal?.aborted) return;\n console.error(\"[TelnyxTTS] synthesizeStream error:\", error);\n }\n }\n\n // ─── REST Backend ──────────────────────────────────────────────────────\n\n private async synthesizeViaREST(\n text: string,\n signal?: AbortSignal\n ): Promise<ArrayBuffer | null> {\n const response = await fetch(`${this.client.baseUrl}${TTS_PATH}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.client.apiKey}`\n },\n body: JSON.stringify({\n text,\n voice: this.voice\n }),\n signal\n });\n\n if (!response.ok) {\n const err = await response.text().catch(() => \"unknown\");\n console.error(`[TelnyxTTS] REST error: ${response.status} — ${err}`);\n return null;\n }\n\n return await response.arrayBuffer();\n }\n\n // ─── WebSocket Backend ─────────────────────────────────────────────────\n\n private async synthesizeViaWS(\n text: string,\n signal?: AbortSignal\n ): Promise<ArrayBuffer | null> {\n const chunks: ArrayBuffer[] = [];\n for await (const chunk of this.streamViaWS(text, signal)) {\n chunks.push(chunk);\n }\n if (chunks.length === 0) return null;\n\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(new Uint8Array(chunk), offset);\n offset += chunk.byteLength;\n }\n return result.buffer;\n }\n\n private async *streamViaWS(\n text: string,\n signal?: AbortSignal\n ): AsyncGenerator<ArrayBuffer> {\n const url = `${this.ttsWsUrl}?voice=${encodeURIComponent(this.voice)}`;\n\n let ws: WebSocket;\n\n try {\n // WebSocket backend requires Cloudflare Workers fetch-upgrade pattern.\n // The Telnyx WS endpoint authenticates via Authorization header, which\n // the standard browser/Node WebSocket API does not support.\n const resp = await fetch(url.replace(\"wss://\", \"https://\"), {\n headers: {\n Upgrade: \"websocket\",\n Authorization: `Bearer ${this.client.apiKey}`\n }\n });\n\n const pair = (resp as unknown as { webSocket?: WebSocket }).webSocket;\n if (!pair) {\n throw new Error(\n \"WebSocket backend requires the Cloudflare Workers runtime. \" +\n \"The fetch-upgrade did not return a WebSocket pair. \" +\n 'Use backend: \"rest\" outside of Workers.'\n );\n }\n\n ws = pair;\n } catch (error) {\n // Distinguish between Workers-missing and other failures\n if (\n error instanceof Error &&\n error.message.includes(\"Cloudflare Workers\")\n ) {\n console.error(`[TelnyxTTS] ${error.message}`);\n } else {\n console.error(\"[TelnyxTTS] WebSocket connection failed:\", error);\n }\n return;\n }\n\n // Register listeners BEFORE accepting the connection to avoid\n // a race where frames arrive between accept() and addEventListener().\n const queue: ArrayBuffer[] = [];\n let done = false;\n let wsError: string | null = null;\n let resolveWait: (() => void) | null = null;\n\n const waitForData = (): Promise<void> =>\n new Promise<void>((resolve) => {\n if (queue.length > 0 || done) resolve();\n else resolveWait = resolve;\n });\n\n const notify = () => {\n if (resolveWait) {\n const r = resolveWait;\n resolveWait = null;\n r();\n }\n };\n\n ws.addEventListener(\"message\", (event: MessageEvent) => {\n try {\n const frame: TelnyxWSFrame =\n typeof event.data === \"string\" ? JSON.parse(event.data) : null;\n if (!frame) return;\n\n if (frame.isFinal) {\n done = true;\n notify();\n return;\n }\n\n // Only yield streaming chunks (text === null with audio data).\n // Skip the blob frame (text === original text, no audio).\n if (frame.audio && frame.text === null) {\n const binary = base64ToArrayBuffer(frame.audio);\n if (binary.byteLength > 0) {\n queue.push(binary);\n notify();\n }\n }\n } catch {\n /* ignore malformed frames */\n }\n });\n\n ws.addEventListener(\"error\", () => {\n wsError = \"WebSocket error\";\n done = true;\n notify();\n });\n\n ws.addEventListener(\"close\", () => {\n done = true;\n notify();\n });\n\n // Now accept the connection — listeners are already in place.\n (ws as unknown as { accept: () => void }).accept();\n\n // Abort handling\n const onAbort = () => {\n done = true;\n try {\n ws.close();\n } catch {\n /* ignore */\n }\n notify();\n };\n\n if (signal) {\n if (signal.aborted) {\n try {\n ws.close();\n } catch {\n /* empty */\n }\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n try {\n // Telnyx TTS protocol: all three frames are sent back-to-back without\n // waiting for server ACKs. This is the correct protocol — the server\n // begins streaming audio after receiving the stop frame and does not\n // send any acknowledgment for init or content frames.\n // Verified against live API 2026-04-19.\n ws.send(JSON.stringify({ text: \" \" })); // 1. init\n ws.send(JSON.stringify({ text })); // 2. content\n ws.send(JSON.stringify({ text: \"\" })); // 3. stop (triggers synthesis)\n\n while (!signal?.aborted) {\n await waitForData();\n while (queue.length > 0) {\n const chunk = queue.shift()!;\n if (!signal?.aborted) yield chunk;\n }\n if (done) break;\n }\n\n if (wsError) console.error(`[TelnyxTTS] ${wsError}`);\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n try {\n ws.close();\n } catch {\n /* ignore */\n }\n }\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { n as TelnyxClientConfig } from "./client-tnkkrw_G.js";
2
+ import { StreamingTTSProvider, TTSProvider } from "@cloudflare/voice";
3
+
4
+ //#region src/providers/tts.d.ts
5
+ interface TelnyxTTSConfig extends TelnyxClientConfig {
6
+ /**
7
+ * Voice identifier.
8
+ * @default "Telnyx.NaturalHD.astra"
9
+ *
10
+ * Examples: `Telnyx.NaturalHD.luna`, `Telnyx.Ultra.<id>`, `Azure.en-US-AvaMultilingualNeural`
11
+ */
12
+ voice?: string;
13
+ /**
14
+ * Backend to use.
15
+ *
16
+ * - `"rest"` (default): HTTP POST per sentence. Works everywhere, all voices.
17
+ * - `"websocket"`: Streams audio chunks for lower time-to-first-audio.
18
+ * Requires the Cloudflare Workers runtime.
19
+ *
20
+ * @default "rest"
21
+ */
22
+ backend?: "rest" | "websocket";
23
+ /**
24
+ * Override the WebSocket URL for the TTS streaming backend.
25
+ * Only used when `backend` is `"websocket"`.
26
+ * @default "wss://api.telnyx.com/v2/text-to-speech/speech"
27
+ */
28
+ ttsWsUrl?: string;
29
+ }
30
+ declare class TelnyxTTS implements TTSProvider, StreamingTTSProvider {
31
+ private client;
32
+ private voice;
33
+ private backend;
34
+ private ttsWsUrl;
35
+ constructor(config: TelnyxTTSConfig);
36
+ synthesize(text: string, signal?: AbortSignal): Promise<ArrayBuffer | null>;
37
+ /**
38
+ * Stream synthesized audio chunks.
39
+ *
40
+ * With the REST backend this yields a single buffered chunk (the complete
41
+ * audio); only the WebSocket backend provides true incremental streaming.
42
+ */
43
+ synthesizeStream(
44
+ text: string,
45
+ signal?: AbortSignal
46
+ ): AsyncGenerator<ArrayBuffer>;
47
+ private synthesizeViaREST;
48
+ private synthesizeViaWS;
49
+ private streamViaWS;
50
+ }
51
+ //#endregion
52
+ export { TelnyxTTSConfig as n, TelnyxTTS as t };
53
+ //# sourceMappingURL=tts-D73UUcew.d.ts.map
package/dist/tts.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { n as TelnyxTTSConfig, t as TelnyxTTS } from "./tts-D73UUcew.js";
2
+ export { TelnyxTTS, type TelnyxTTSConfig };
package/dist/tts.js ADDED
@@ -0,0 +1,2 @@
1
+ import { t as TelnyxTTS } from "./tts-BNsM0WUr.js";
2
+ export { TelnyxTTS };
package/package.json CHANGED
@@ -1,13 +1,66 @@
1
1
  {
2
2
  "name": "@cloudflare/voice-telnyx",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
3
+ "version": "0.0.2",
4
+ "description": "Telnyx STT, TTS, and telephony providers for Cloudflare Agents voice pipeline",
5
+ "repository": {
6
+ "directory": "voice-providers/telnyx",
7
+ "type": "git",
8
+ "url": "git+https://github.com/cloudflare/agents.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/cloudflare/agents/issues"
12
+ },
13
+ "peerDependencies": {
14
+ "@cloudflare/voice": "*"
15
+ },
16
+ "dependencies": {
17
+ "@telnyx/webrtc": "^2.26.2"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.js"
24
+ },
25
+ "./stt": {
26
+ "types": "./dist/stt.d.ts",
27
+ "import": "./dist/stt.js",
28
+ "require": "./dist/stt.js"
29
+ },
30
+ "./tts": {
31
+ "types": "./dist/tts.d.ts",
32
+ "import": "./dist/tts.js",
33
+ "require": "./dist/tts.js"
34
+ },
35
+ "./browser": {
36
+ "types": "./dist/browser.d.ts",
37
+ "import": "./dist/browser.js",
38
+ "require": "./dist/browser.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md"
44
+ ],
6
45
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
46
+ "build": "tsx ./scripts/build.ts",
47
+ "test": "vitest run"
8
48
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
49
+ "keywords": [
50
+ "cloudflare",
51
+ "agents",
52
+ "voice",
53
+ "telnyx",
54
+ "stt",
55
+ "tts",
56
+ "telephony",
57
+ "webrtc",
58
+ "phone"
59
+ ],
60
+ "author": "Cloudflare Inc.",
61
+ "license": "MIT",
62
+ "type": "module",
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
13
66
  }