felona-voice 0.1.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.
Files changed (126) hide show
  1. package/dist/agent.d.ts +105 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +287 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/analytics/index.d.ts +3 -0
  6. package/dist/analytics/index.d.ts.map +1 -0
  7. package/dist/analytics/index.js +2 -0
  8. package/dist/analytics/index.js.map +1 -0
  9. package/dist/analytics/logger.d.ts +92 -0
  10. package/dist/analytics/logger.d.ts.map +1 -0
  11. package/dist/analytics/logger.js +75 -0
  12. package/dist/analytics/logger.js.map +1 -0
  13. package/dist/index.d.ts +45 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +54 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/jev/action-space.d.ts +38 -0
  18. package/dist/jev/action-space.d.ts.map +1 -0
  19. package/dist/jev/action-space.js +92 -0
  20. package/dist/jev/action-space.js.map +1 -0
  21. package/dist/jev/embeddings.d.ts +23 -0
  22. package/dist/jev/embeddings.d.ts.map +1 -0
  23. package/dist/jev/embeddings.js +63 -0
  24. package/dist/jev/embeddings.js.map +1 -0
  25. package/dist/jev/engine.d.ts +101 -0
  26. package/dist/jev/engine.d.ts.map +1 -0
  27. package/dist/jev/engine.js +173 -0
  28. package/dist/jev/engine.js.map +1 -0
  29. package/dist/jev/index.d.ts +4 -0
  30. package/dist/jev/index.d.ts.map +1 -0
  31. package/dist/jev/index.js +4 -0
  32. package/dist/jev/index.js.map +1 -0
  33. package/dist/llm/index.d.ts +2 -0
  34. package/dist/llm/index.d.ts.map +1 -0
  35. package/dist/llm/index.js +2 -0
  36. package/dist/llm/index.js.map +1 -0
  37. package/dist/llm/openai.d.ts +22 -0
  38. package/dist/llm/openai.d.ts.map +1 -0
  39. package/dist/llm/openai.js +140 -0
  40. package/dist/llm/openai.js.map +1 -0
  41. package/dist/memory/context.d.ts +53 -0
  42. package/dist/memory/context.d.ts.map +1 -0
  43. package/dist/memory/context.js +95 -0
  44. package/dist/memory/context.js.map +1 -0
  45. package/dist/memory/index.d.ts +2 -0
  46. package/dist/memory/index.d.ts.map +1 -0
  47. package/dist/memory/index.js +2 -0
  48. package/dist/memory/index.js.map +1 -0
  49. package/dist/pipeline.d.ts +93 -0
  50. package/dist/pipeline.d.ts.map +1 -0
  51. package/dist/pipeline.js +255 -0
  52. package/dist/pipeline.js.map +1 -0
  53. package/dist/stt/deepgram.d.ts +27 -0
  54. package/dist/stt/deepgram.d.ts.map +1 -0
  55. package/dist/stt/deepgram.js +137 -0
  56. package/dist/stt/deepgram.js.map +1 -0
  57. package/dist/stt/index.d.ts +2 -0
  58. package/dist/stt/index.d.ts.map +1 -0
  59. package/dist/stt/index.js +2 -0
  60. package/dist/stt/index.js.map +1 -0
  61. package/dist/tools/index.d.ts +2 -0
  62. package/dist/tools/index.d.ts.map +1 -0
  63. package/dist/tools/index.js +2 -0
  64. package/dist/tools/index.js.map +1 -0
  65. package/dist/tools/registry.d.ts +38 -0
  66. package/dist/tools/registry.d.ts.map +1 -0
  67. package/dist/tools/registry.js +77 -0
  68. package/dist/tools/registry.js.map +1 -0
  69. package/dist/transport/index.d.ts +2 -0
  70. package/dist/transport/index.d.ts.map +1 -0
  71. package/dist/transport/index.js +2 -0
  72. package/dist/transport/index.js.map +1 -0
  73. package/dist/transport/websocket.d.ts +38 -0
  74. package/dist/transport/websocket.d.ts.map +1 -0
  75. package/dist/transport/websocket.js +180 -0
  76. package/dist/transport/websocket.js.map +1 -0
  77. package/dist/tts/eleven-labs.d.ts +27 -0
  78. package/dist/tts/eleven-labs.d.ts.map +1 -0
  79. package/dist/tts/eleven-labs.js +73 -0
  80. package/dist/tts/eleven-labs.js.map +1 -0
  81. package/dist/tts/index.d.ts +2 -0
  82. package/dist/tts/index.d.ts.map +1 -0
  83. package/dist/tts/index.js +2 -0
  84. package/dist/tts/index.js.map +1 -0
  85. package/dist/types.d.ts +370 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +13 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/vad/energy.d.ts +49 -0
  90. package/dist/vad/energy.d.ts.map +1 -0
  91. package/dist/vad/energy.js +108 -0
  92. package/dist/vad/energy.js.map +1 -0
  93. package/dist/vad/index.d.ts +2 -0
  94. package/dist/vad/index.d.ts.map +1 -0
  95. package/dist/vad/index.js +2 -0
  96. package/dist/vad/index.js.map +1 -0
  97. package/package.json +51 -0
  98. package/src/agent.ts +373 -0
  99. package/src/analytics/index.ts +7 -0
  100. package/src/analytics/logger.ts +152 -0
  101. package/src/index.ts +121 -0
  102. package/src/jev/action-space.ts +119 -0
  103. package/src/jev/embeddings.ts +81 -0
  104. package/src/jev/engine.ts +225 -0
  105. package/src/jev/index.ts +3 -0
  106. package/src/llm/index.ts +1 -0
  107. package/src/llm/openai.ts +178 -0
  108. package/src/memory/context.ts +124 -0
  109. package/src/memory/index.ts +1 -0
  110. package/src/pipeline.ts +331 -0
  111. package/src/stt/deepgram.ts +182 -0
  112. package/src/stt/index.ts +1 -0
  113. package/src/tools/index.ts +1 -0
  114. package/src/tools/registry.ts +99 -0
  115. package/src/transport/index.ts +1 -0
  116. package/src/transport/websocket.ts +227 -0
  117. package/src/tts/eleven-labs.ts +100 -0
  118. package/src/tts/index.ts +1 -0
  119. package/src/types.ts +429 -0
  120. package/src/vad/energy.ts +135 -0
  121. package/src/vad/index.ts +1 -0
  122. package/tests/jev.test.ts +198 -0
  123. package/tests/memory.test.ts +169 -0
  124. package/tests/vad.test.ts +94 -0
  125. package/tsconfig.json +9 -0
  126. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,182 @@
1
+ import { WebSocket } from "ws";
2
+ import { EventEmitter } from "node:events";
3
+ import type {
4
+ STTProvider,
5
+ STTStream,
6
+ STTStreamOptions,
7
+ STTResult,
8
+ AudioChunk,
9
+ } from "../types.js";
10
+
11
+ /**
12
+ * DeepgramSTT — Streaming Speech-to-Text using Deepgram's WebSocket API.
13
+ *
14
+ * Deepgram is the default STT provider because:
15
+ * - Sub-300ms latency for streaming recognition
16
+ * - Excellent accuracy with Nova-2 model
17
+ * - Native WebSocket API (no HTTP polling)
18
+ * - Supports interim results for real-time feedback
19
+ */
20
+ export class DeepgramSTT implements STTProvider {
21
+ readonly name = "deepgram";
22
+ private readonly apiKey: string;
23
+ private readonly model: string;
24
+ private readonly baseUrl: string;
25
+
26
+ constructor(options: {
27
+ apiKey: string;
28
+ model?: string;
29
+ baseUrl?: string;
30
+ }) {
31
+ this.apiKey = options.apiKey;
32
+ this.model = options.model ?? "nova-2";
33
+ this.baseUrl =
34
+ options.baseUrl ?? "wss://api.deepgram.com/v1/listen";
35
+ }
36
+
37
+ createStream(options?: STTStreamOptions): STTStream {
38
+ return new DeepgramSTTStream({
39
+ apiKey: this.apiKey,
40
+ model: this.model,
41
+ baseUrl: this.baseUrl,
42
+ language: options?.language ?? "en-US",
43
+ interimResults: options?.interimResults ?? true,
44
+ keywords: options?.keywords,
45
+ });
46
+ }
47
+ }
48
+
49
+ class DeepgramSTTStream extends EventEmitter implements STTStream {
50
+ private ws: WebSocket | null = null;
51
+ private resultHandler: ((result: STTResult) => void) | null = null;
52
+ private readonly config: {
53
+ apiKey: string;
54
+ model: string;
55
+ baseUrl: string;
56
+ language: string;
57
+ interimResults: boolean;
58
+ keywords?: string[];
59
+ };
60
+ private connected = false;
61
+ private connectPromise: Promise<void> | null = null;
62
+
63
+ constructor(config: {
64
+ apiKey: string;
65
+ model: string;
66
+ baseUrl: string;
67
+ language: string;
68
+ interimResults: boolean;
69
+ keywords?: string[];
70
+ }) {
71
+ super();
72
+ this.config = config;
73
+ this.connectPromise = this.connect();
74
+ }
75
+
76
+ private async connect(): Promise<void> {
77
+ const params = new URLSearchParams({
78
+ model: this.config.model,
79
+ language: this.config.language,
80
+ interim_results: String(this.config.interimResults),
81
+ punctuate: "true",
82
+ encoding: "linear16",
83
+ sample_rate: "16000",
84
+ channels: "1",
85
+ });
86
+
87
+ if (this.config.keywords?.length) {
88
+ for (const kw of this.config.keywords) {
89
+ params.append("keywords", kw);
90
+ }
91
+ }
92
+
93
+ const url = `${this.config.baseUrl}?${params.toString()}`;
94
+
95
+ return new Promise<void>((resolve, reject) => {
96
+ this.ws = new WebSocket(url, {
97
+ headers: {
98
+ Authorization: `Token ${this.config.apiKey}`,
99
+ },
100
+ });
101
+
102
+ this.ws.on("open", () => {
103
+ this.connected = true;
104
+ resolve();
105
+ });
106
+
107
+ this.ws.on("message", (data) => {
108
+ try {
109
+ const response = JSON.parse(data.toString());
110
+ this.handleResponse(response);
111
+ } catch {
112
+ // Ignore non-JSON messages
113
+ }
114
+ });
115
+
116
+ this.ws.on("error", (error) => {
117
+ console.error("[STT/Deepgram] WebSocket error:", error.message);
118
+ if (!this.connected) {
119
+ reject(error);
120
+ }
121
+ });
122
+
123
+ this.ws.on("close", () => {
124
+ this.connected = false;
125
+ });
126
+ });
127
+ }
128
+
129
+ write(chunk: AudioChunk): void {
130
+ if (this.ws && this.connected && this.ws.readyState === WebSocket.OPEN) {
131
+ this.ws.send(chunk.data);
132
+ }
133
+ }
134
+
135
+ onResult(handler: (result: STTResult) => void): void {
136
+ this.resultHandler = handler;
137
+ }
138
+
139
+ async close(): Promise<void> {
140
+ if (this.ws) {
141
+ // Send close message per Deepgram protocol
142
+ if (this.ws.readyState === WebSocket.OPEN) {
143
+ this.ws.send(JSON.stringify({ type: "CloseStream" }));
144
+ }
145
+ this.ws.close();
146
+ this.ws = null;
147
+ }
148
+ this.connected = false;
149
+ }
150
+
151
+ private handleResponse(response: Record<string, unknown>): void {
152
+ // Deepgram response format
153
+ if (response.type === "Results") {
154
+ const channel = (response as any).channel;
155
+ if (!channel?.alternatives?.length) return;
156
+
157
+ const alt = channel.alternatives[0];
158
+ const result: STTResult = {
159
+ text: alt.transcript ?? "",
160
+ isFinal: (response as any).is_final ?? false,
161
+ confidence: alt.confidence ?? 0,
162
+ words: alt.words?.map((w: any) => ({
163
+ word: w.word,
164
+ startMs: Math.round(w.start * 1000),
165
+ endMs: Math.round(w.end * 1000),
166
+ })),
167
+ };
168
+
169
+ // Only emit non-empty results
170
+ if (result.text.trim()) {
171
+ this.resultHandler?.(result);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ export function createDeepgramSTT(options: {
178
+ apiKey: string;
179
+ model?: string;
180
+ }): DeepgramSTT {
181
+ return new DeepgramSTT(options);
182
+ }
@@ -0,0 +1 @@
1
+ export { DeepgramSTT, createDeepgramSTT } from "./deepgram.js";
@@ -0,0 +1 @@
1
+ export { ToolRegistry, defineTool, createToolRegistry } from "./registry.js";
@@ -0,0 +1,99 @@
1
+ import type { AgentTool, ToolExecutor } from "../types.js";
2
+
3
+ /**
4
+ * ToolRegistry — Manages tool registration and execution.
5
+ *
6
+ * Tools are external capabilities the agent can invoke during a call
7
+ * (e.g., booking appointments, looking up orders, transferring calls).
8
+ */
9
+ export class ToolRegistry implements ToolExecutor {
10
+ private tools: Map<string, AgentTool> = new Map();
11
+
12
+ /** Register a tool */
13
+ register(tool: AgentTool): void {
14
+ if (this.tools.has(tool.name)) {
15
+ throw new Error(`Tool "${tool.name}" is already registered`);
16
+ }
17
+ this.tools.set(tool.name, tool);
18
+ }
19
+
20
+ /** Register multiple tools at once */
21
+ registerAll(tools: AgentTool[]): void {
22
+ for (const tool of tools) {
23
+ this.register(tool);
24
+ }
25
+ }
26
+
27
+ /** Call a registered tool by name */
28
+ async call(
29
+ toolName: string,
30
+ params?: Record<string, unknown>,
31
+ ): Promise<unknown> {
32
+ const tool = this.tools.get(toolName);
33
+ if (!tool) {
34
+ throw new Error(
35
+ `Tool "${toolName}" not found. Available: ${[...this.tools.keys()].join(", ")}`,
36
+ );
37
+ }
38
+
39
+ try {
40
+ const result = await tool.execute(params ?? {});
41
+ return result;
42
+ } catch (error) {
43
+ throw new Error(
44
+ `Tool "${toolName}" execution failed: ${error instanceof Error ? error.message : String(error)}`,
45
+ );
46
+ }
47
+ }
48
+
49
+ /** List all registered tools */
50
+ list(): AgentTool[] {
51
+ return [...this.tools.values()];
52
+ }
53
+
54
+ /** Check if a tool is registered */
55
+ has(toolName: string): boolean {
56
+ return this.tools.has(toolName);
57
+ }
58
+
59
+ /** Get a tool's schema (for LLM function calling) */
60
+ getSchema(
61
+ toolName: string,
62
+ ): { name: string; description: string; parameters: Record<string, unknown> } | undefined {
63
+ const tool = this.tools.get(toolName);
64
+ if (!tool) return undefined;
65
+ return {
66
+ name: tool.name,
67
+ description: tool.description,
68
+ parameters: tool.parameters,
69
+ };
70
+ }
71
+
72
+ /** Get all tool schemas (for LLM function calling) */
73
+ getAllSchemas(): Array<{
74
+ name: string;
75
+ description: string;
76
+ parameters: Record<string, unknown>;
77
+ }> {
78
+ return this.list().map((t) => ({
79
+ name: t.name,
80
+ description: t.description,
81
+ parameters: t.parameters,
82
+ }));
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Helper to define a tool with type safety.
88
+ */
89
+ export function defineTool(tool: AgentTool): AgentTool {
90
+ return tool;
91
+ }
92
+
93
+ export function createToolRegistry(tools?: AgentTool[]): ToolRegistry {
94
+ const registry = new ToolRegistry();
95
+ if (tools) {
96
+ registry.registerAll(tools);
97
+ }
98
+ return registry;
99
+ }
@@ -0,0 +1 @@
1
+ export { WebSocketTransport, createWebSocketTransport } from "./websocket.js";
@@ -0,0 +1,227 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { randomUUID } from "node:crypto";
3
+ import { WebSocketServer, WebSocket } from "ws";
4
+ import type {
5
+ Transport,
6
+ TransportOptions,
7
+ AudioChunk,
8
+ Session,
9
+ DEFAULT_AUDIO_FORMAT,
10
+ } from "../types.js";
11
+
12
+ /**
13
+ * WebSocketTransport — Handles real-time audio streaming over WebSocket.
14
+ *
15
+ * Protocol:
16
+ * - Client connects to ws://host:port
17
+ * - Binary frames = raw PCM audio data
18
+ * - JSON text frames = control messages (metadata, config, etc.)
19
+ *
20
+ * This is the default transport for Phase 1. WebRTC transport
21
+ * will be added in Phase 3 for lower-latency applications.
22
+ */
23
+ export class WebSocketTransport extends EventEmitter implements Transport {
24
+ private wss: WebSocketServer | null = null;
25
+ private sessions: Map<string, { ws: WebSocket; session: Session }> =
26
+ new Map();
27
+ private audioHandler:
28
+ | ((sessionId: string, chunk: AudioChunk) => void)
29
+ | null = null;
30
+ private connectHandler: ((session: Session) => void) | null = null;
31
+ private disconnectHandler: ((session: Session) => void) | null = null;
32
+
33
+ async start(options: TransportOptions): Promise<void> {
34
+ return new Promise((resolve, reject) => {
35
+ try {
36
+ this.wss = new WebSocketServer({
37
+ port: options.port,
38
+ host: options.host ?? "0.0.0.0",
39
+ });
40
+
41
+ this.wss.on("connection", (ws, req) => {
42
+ this.handleConnection(ws, req);
43
+ });
44
+
45
+ this.wss.on("listening", () => {
46
+ console.log(
47
+ `[Transport] WebSocket server listening on ${options.host ?? "0.0.0.0"}:${options.port}`,
48
+ );
49
+ resolve();
50
+ });
51
+
52
+ this.wss.on("error", (error) => {
53
+ reject(error);
54
+ });
55
+ } catch (error) {
56
+ reject(error);
57
+ }
58
+ });
59
+ }
60
+
61
+ async stop(): Promise<void> {
62
+ // Close all active sessions
63
+ for (const [sessionId, { ws, session }] of this.sessions) {
64
+ session.state = "ended";
65
+ ws.close();
66
+ this.disconnectHandler?.(session);
67
+ }
68
+ this.sessions.clear();
69
+
70
+ // Shut down the server
71
+ return new Promise((resolve) => {
72
+ if (this.wss) {
73
+ this.wss.close(() => {
74
+ console.log("[Transport] WebSocket server stopped");
75
+ resolve();
76
+ });
77
+ } else {
78
+ resolve();
79
+ }
80
+ });
81
+ }
82
+
83
+ onAudioChunk(
84
+ handler: (sessionId: string, chunk: AudioChunk) => void,
85
+ ): void {
86
+ this.audioHandler = handler;
87
+ }
88
+
89
+ onConnect(handler: (session: Session) => void): void {
90
+ this.connectHandler = handler;
91
+ }
92
+
93
+ onDisconnect(handler: (session: Session) => void): void {
94
+ this.disconnectHandler = handler;
95
+ }
96
+
97
+ async sendAudio(sessionId: string, chunk: AudioChunk): Promise<void> {
98
+ const entry = this.sessions.get(sessionId);
99
+ if (!entry) {
100
+ throw new Error(`Session "${sessionId}" not found`);
101
+ }
102
+
103
+ if (entry.ws.readyState === WebSocket.OPEN) {
104
+ entry.ws.send(chunk.data);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Send a JSON control message to a session.
110
+ */
111
+ async sendMessage(
112
+ sessionId: string,
113
+ message: Record<string, unknown>,
114
+ ): Promise<void> {
115
+ const entry = this.sessions.get(sessionId);
116
+ if (!entry) return;
117
+
118
+ if (entry.ws.readyState === WebSocket.OPEN) {
119
+ entry.ws.send(JSON.stringify(message));
120
+ }
121
+ }
122
+
123
+ /** Get active session count */
124
+ get activeSessionCount(): number {
125
+ return this.sessions.size;
126
+ }
127
+
128
+ /** Get a session by ID */
129
+ getSession(sessionId: string): Session | undefined {
130
+ return this.sessions.get(sessionId)?.session;
131
+ }
132
+
133
+ private handleConnection(
134
+ ws: WebSocket,
135
+ req: import("http").IncomingMessage,
136
+ ): void {
137
+ const sessionId = randomUUID();
138
+ const session: Session = {
139
+ id: sessionId,
140
+ startedAt: new Date(),
141
+ metadata: {
142
+ remoteAddress: req.socket.remoteAddress,
143
+ headers: req.headers,
144
+ },
145
+ state: "active",
146
+ };
147
+
148
+ this.sessions.set(sessionId, { ws, session });
149
+ console.log(`[Transport] New session: ${sessionId}`);
150
+
151
+ // Notify connection handler
152
+ this.connectHandler?.(session);
153
+
154
+ // Send session info to the client
155
+ ws.send(
156
+ JSON.stringify({
157
+ type: "session.created",
158
+ sessionId,
159
+ timestamp: Date.now(),
160
+ }),
161
+ );
162
+
163
+ const sessionStartTime = Date.now();
164
+
165
+ ws.on("message", (data, isBinary) => {
166
+ if (isBinary) {
167
+ // Binary = audio data
168
+ const chunk: AudioChunk = {
169
+ data: Buffer.from(data as Buffer),
170
+ sampleRate: 16000,
171
+ channels: 1,
172
+ bitDepth: 16,
173
+ timestampMs: Date.now() - sessionStartTime,
174
+ };
175
+ this.audioHandler?.(sessionId, chunk);
176
+ } else {
177
+ // Text = control message
178
+ try {
179
+ const message = JSON.parse(data.toString());
180
+ this.handleControlMessage(sessionId, message);
181
+ } catch {
182
+ console.warn(`[Transport] Invalid control message from ${sessionId}`);
183
+ }
184
+ }
185
+ });
186
+
187
+ ws.on("close", () => {
188
+ console.log(`[Transport] Session ended: ${sessionId}`);
189
+ session.state = "ended";
190
+ this.disconnectHandler?.(session);
191
+ this.sessions.delete(sessionId);
192
+ });
193
+
194
+ ws.on("error", (error) => {
195
+ console.error(`[Transport] Session error (${sessionId}):`, error.message);
196
+ });
197
+ }
198
+
199
+ private handleControlMessage(
200
+ sessionId: string,
201
+ message: Record<string, unknown>,
202
+ ): void {
203
+ const entry = this.sessions.get(sessionId);
204
+ if (!entry) return;
205
+
206
+ switch (message.type) {
207
+ case "session.update":
208
+ // Client can update session metadata
209
+ if (message.metadata && typeof message.metadata === "object") {
210
+ Object.assign(entry.session.metadata, message.metadata);
211
+ }
212
+ break;
213
+
214
+ case "audio.config":
215
+ // Client specifying audio format (future: handle format conversion)
216
+ this.emit("audioConfig", sessionId, message);
217
+ break;
218
+
219
+ default:
220
+ this.emit("controlMessage", sessionId, message);
221
+ }
222
+ }
223
+ }
224
+
225
+ export function createWebSocketTransport(): WebSocketTransport {
226
+ return new WebSocketTransport();
227
+ }
@@ -0,0 +1,100 @@
1
+ import type { TTSProvider, TTSOptions, AudioChunk } from "../types.js";
2
+
3
+ /**
4
+ * ElevenLabsTTS — Streaming Text-to-Speech using ElevenLabs' API.
5
+ *
6
+ * Uses the streaming endpoint for chunk-by-chunk audio delivery,
7
+ * so the agent can start speaking before the full response is generated.
8
+ */
9
+ export class ElevenLabsTTS implements TTSProvider {
10
+ readonly name = "elevenlabs";
11
+ private readonly apiKey: string;
12
+ private readonly defaultVoice: string;
13
+ private readonly baseUrl: string;
14
+ private readonly modelId: string;
15
+
16
+ constructor(options: {
17
+ apiKey: string;
18
+ voice?: string;
19
+ modelId?: string;
20
+ baseUrl?: string;
21
+ }) {
22
+ this.apiKey = options.apiKey;
23
+ this.defaultVoice = options.voice ?? "21m00Tcm4TlvDq8ikWAM"; // Rachel
24
+ this.modelId = options.modelId ?? "eleven_turbo_v2_5";
25
+ this.baseUrl = options.baseUrl ?? "https://api.elevenlabs.io/v1";
26
+ }
27
+
28
+ async *synthesize(
29
+ text: string,
30
+ options?: TTSOptions,
31
+ ): AsyncIterable<AudioChunk> {
32
+ const voiceId = options?.voice ?? this.defaultVoice;
33
+
34
+ const response = await fetch(
35
+ `${this.baseUrl}/text-to-speech/${voiceId}/stream`,
36
+ {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ "xi-api-key": this.apiKey,
41
+ },
42
+ body: JSON.stringify({
43
+ text,
44
+ model_id: this.modelId,
45
+ voice_settings: {
46
+ stability: 0.5,
47
+ similarity_boost: 0.75,
48
+ speed: options?.speed ?? 1.0,
49
+ },
50
+ output_format: "pcm_16000",
51
+ }),
52
+ },
53
+ );
54
+
55
+ if (!response.ok) {
56
+ const error = await response.text();
57
+ throw new Error(
58
+ `ElevenLabs TTS failed: ${response.status} ${error}`,
59
+ );
60
+ }
61
+
62
+ if (!response.body) {
63
+ throw new Error("ElevenLabs TTS returned no body");
64
+ }
65
+
66
+ const reader = response.body.getReader();
67
+ let timestampMs = 0;
68
+ const bytesPerSecond = 16000 * 2; // 16kHz * 16-bit = 32000 bytes/sec
69
+
70
+ try {
71
+ while (true) {
72
+ const { done, value } = await reader.read();
73
+ if (done) break;
74
+
75
+ const chunk: AudioChunk = {
76
+ data: Buffer.from(value),
77
+ sampleRate: 16000,
78
+ channels: 1,
79
+ bitDepth: 16,
80
+ timestampMs,
81
+ };
82
+
83
+ // Calculate timestamp based on audio duration
84
+ timestampMs += (value.length / bytesPerSecond) * 1000;
85
+
86
+ yield chunk;
87
+ }
88
+ } finally {
89
+ reader.releaseLock();
90
+ }
91
+ }
92
+ }
93
+
94
+ export function createElevenLabsTTS(options: {
95
+ apiKey: string;
96
+ voice?: string;
97
+ modelId?: string;
98
+ }): ElevenLabsTTS {
99
+ return new ElevenLabsTTS(options);
100
+ }
@@ -0,0 +1 @@
1
+ export { ElevenLabsTTS, createElevenLabsTTS } from "./eleven-labs.js";