phonic 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,23 +50,10 @@ if (error === null) {
50
50
 
51
51
  ### Speech-to-speech via WebSocket
52
52
 
53
- Open a WebSocket connection:
53
+ To start a conversation, open a WebSocket connection:
54
54
 
55
55
  ```ts
56
- const { data, error } = await phonic.sts.websocket();
57
-
58
- if (error !== null) {
59
- throw new Error(error.message);
60
- }
61
-
62
- // Here we know that the WebSocket connection is open.
63
- const { phonicWebSocket } = data;
64
- ```
65
-
66
- Send config params for the conversation:
67
-
68
- ```ts
69
- phonicWebSocket.config({
56
+ const { data, error } = await phonic.sts.websocket({
70
57
  input_format: "mulaw_8000",
71
58
 
72
59
  // Optional fields
@@ -75,6 +62,12 @@ phonicWebSocket.config({
75
62
  voice_id: "meredith",
76
63
  output_format: "mulaw_8000"
77
64
  });
65
+
66
+ if (error !== null) {
67
+ throw new Error(`Failed to start conversation: ${error.message}`);
68
+ }
69
+
70
+ const { phonicWebSocket } = data;
78
71
  ```
79
72
 
80
73
  Stream input (user) audio chunks:
package/dist/index.d.mts CHANGED
@@ -26,6 +26,8 @@ type PhonicSTSConfig = {
26
26
  output_format?: "pcm_44100" | "mulaw_8000";
27
27
  };
28
28
  type PhonicSTSWebSocketResponseMessage = {
29
+ type: "ready_to_start_conversation";
30
+ } | {
29
31
  type: "input_text";
30
32
  text: string;
31
33
  } | {
@@ -62,7 +64,6 @@ declare class PhonicSTSWebSocket {
62
64
  onMessage(callback: OnMessageCallback): void;
63
65
  onClose(callback: OnCloseCallback): void;
64
66
  onError(callback: OnErrorCallback): void;
65
- config(message: PhonicSTSConfig): void;
66
67
  audioChunk(message: {
67
68
  audio: string;
68
69
  }): void;
@@ -72,7 +73,8 @@ declare class PhonicSTSWebSocket {
72
73
  declare class SpeechToSpeech {
73
74
  private readonly phonic;
74
75
  constructor(phonic: Phonic);
75
- websocket(): DataOrError<{
76
+ private connectToPhonicAPI;
77
+ websocket(config: PhonicSTSConfig): DataOrError<{
76
78
  phonicWebSocket: PhonicSTSWebSocket;
77
79
  }>;
78
80
  }
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ type PhonicSTSConfig = {
26
26
  output_format?: "pcm_44100" | "mulaw_8000";
27
27
  };
28
28
  type PhonicSTSWebSocketResponseMessage = {
29
+ type: "ready_to_start_conversation";
30
+ } | {
29
31
  type: "input_text";
30
32
  text: string;
31
33
  } | {
@@ -62,7 +64,6 @@ declare class PhonicSTSWebSocket {
62
64
  onMessage(callback: OnMessageCallback): void;
63
65
  onClose(callback: OnCloseCallback): void;
64
66
  onError(callback: OnErrorCallback): void;
65
- config(message: PhonicSTSConfig): void;
66
67
  audioChunk(message: {
67
68
  audio: string;
68
69
  }): void;
@@ -72,7 +73,8 @@ declare class PhonicSTSWebSocket {
72
73
  declare class SpeechToSpeech {
73
74
  private readonly phonic;
74
75
  constructor(phonic: Phonic);
75
- websocket(): DataOrError<{
76
+ private connectToPhonicAPI;
77
+ websocket(config: PhonicSTSConfig): DataOrError<{
76
78
  phonicWebSocket: PhonicSTSWebSocket;
77
79
  }>;
78
80
  }
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // package.json
38
- var version = "0.7.0";
38
+ var version = "0.8.1";
39
39
 
40
40
  // src/sts/index.ts
41
41
  var import_ws = __toESM(require("ws"));
@@ -71,7 +71,6 @@ var PhonicSTSWebSocket = class {
71
71
  this.onMessage = this.onMessage.bind(this);
72
72
  this.onClose = this.onClose.bind(this);
73
73
  this.onError = this.onError.bind(this);
74
- this.config = this.config.bind(this);
75
74
  this.audioChunk = this.audioChunk.bind(this);
76
75
  this.close = this.close.bind(this);
77
76
  }
@@ -87,14 +86,6 @@ var PhonicSTSWebSocket = class {
87
86
  onError(callback) {
88
87
  this.onErrorCallback = callback;
89
88
  }
90
- config(message) {
91
- this.ws.send(
92
- JSON.stringify({
93
- type: "config",
94
- ...message
95
- })
96
- );
97
- }
98
89
  audioChunk(message) {
99
90
  this.ws.send(
100
91
  JSON.stringify({
@@ -109,21 +100,43 @@ var PhonicSTSWebSocket = class {
109
100
  };
110
101
 
111
102
  // src/sts/index.ts
103
+ var phonicApiCloseCodes = {
104
+ insuffucientCapacityAvailable: 4004
105
+ };
112
106
  var SpeechToSpeech = class {
113
107
  constructor(phonic) {
114
108
  this.phonic = phonic;
115
109
  }
116
- async websocket() {
110
+ async connectToPhonicAPI(phonicApiWsUrl, config) {
117
111
  return new Promise((resolve) => {
118
- const wsBaseUrl = this.phonic.baseUrl.replace(/^http/, "ws");
119
- const ws = new import_ws.default(`${wsBaseUrl}/v1/sts/ws`, {
112
+ const ws = new import_ws.default(phonicApiWsUrl, {
120
113
  headers: {
121
114
  Authorization: `Bearer ${this.phonic.apiKey}`
122
115
  }
123
116
  });
124
117
  ws.onopen = () => {
125
- const phonicWebSocket = new PhonicSTSWebSocket(ws);
126
- resolve({ data: { phonicWebSocket }, error: null });
118
+ ws.send(
119
+ JSON.stringify({
120
+ type: "config",
121
+ ...config
122
+ })
123
+ );
124
+ };
125
+ ws.onmessage = (event) => {
126
+ if (typeof event.data !== "string") {
127
+ throw new Error("Received non-string message");
128
+ }
129
+ const dataObj = JSON.parse(
130
+ event.data
131
+ );
132
+ if (dataObj.type === "ready_to_start_conversation") {
133
+ resolve({
134
+ data: {
135
+ phonicWebSocket: new PhonicSTSWebSocket(ws)
136
+ },
137
+ error: null
138
+ });
139
+ }
127
140
  };
128
141
  ws.onerror = (error) => {
129
142
  resolve({
@@ -133,8 +146,48 @@ var SpeechToSpeech = class {
133
146
  }
134
147
  });
135
148
  };
149
+ ws.onclose = (event) => {
150
+ if (event.code === phonicApiCloseCodes.insuffucientCapacityAvailable) {
151
+ resolve({
152
+ data: null,
153
+ error: {
154
+ message: event.reason,
155
+ code: "insuffucient_capacity_available"
156
+ }
157
+ });
158
+ }
159
+ };
136
160
  });
137
161
  }
162
+ async websocket(config) {
163
+ const wsBaseUrl = this.phonic.baseUrl.replace(/^http/, "ws");
164
+ const phonicApiWsUrl = `${wsBaseUrl}/v1/sts/ws`;
165
+ let retryNumber = 0;
166
+ const maxRetries = 14;
167
+ const retryDelay = 15e3;
168
+ while (true) {
169
+ const connectResult = await this.connectToPhonicAPI(
170
+ phonicApiWsUrl,
171
+ config
172
+ );
173
+ if (connectResult.data !== null) {
174
+ return connectResult;
175
+ }
176
+ if (connectResult.error.code === "insuffucient_capacity_available") {
177
+ if (retryNumber >= maxRetries) {
178
+ return connectResult;
179
+ }
180
+ console.info(
181
+ `${connectResult.error.message}, will retry in ${retryDelay / 1e3}sec`
182
+ );
183
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
184
+ retryNumber += 1;
185
+ console.info(`Retrying... ${retryNumber}/${maxRetries}`);
186
+ continue;
187
+ }
188
+ return connectResult;
189
+ }
190
+ }
138
191
  };
139
192
 
140
193
  // src/voices/index.ts
@@ -172,7 +225,7 @@ var Phonic = class {
172
225
  'API key is missing. Pass it to the constructor: `new Phonic("ph_...")`'
173
226
  );
174
227
  }
175
- this.baseUrl = config?.baseUrl ?? defaultBaseUrl;
228
+ this.baseUrl = (config?.baseUrl ?? defaultBaseUrl).replace(/\/$/, "");
176
229
  this.headers = new Headers({
177
230
  Authorization: `Bearer ${this.apiKey}`,
178
231
  "User-Agent": process.env.PHONIC_USER_AGENT || defaultUserAgent,
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.7.0";
2
+ var version = "0.8.1";
3
3
 
4
4
  // src/sts/index.ts
5
5
  import WebSocket from "ws";
@@ -35,7 +35,6 @@ var PhonicSTSWebSocket = class {
35
35
  this.onMessage = this.onMessage.bind(this);
36
36
  this.onClose = this.onClose.bind(this);
37
37
  this.onError = this.onError.bind(this);
38
- this.config = this.config.bind(this);
39
38
  this.audioChunk = this.audioChunk.bind(this);
40
39
  this.close = this.close.bind(this);
41
40
  }
@@ -51,14 +50,6 @@ var PhonicSTSWebSocket = class {
51
50
  onError(callback) {
52
51
  this.onErrorCallback = callback;
53
52
  }
54
- config(message) {
55
- this.ws.send(
56
- JSON.stringify({
57
- type: "config",
58
- ...message
59
- })
60
- );
61
- }
62
53
  audioChunk(message) {
63
54
  this.ws.send(
64
55
  JSON.stringify({
@@ -73,21 +64,43 @@ var PhonicSTSWebSocket = class {
73
64
  };
74
65
 
75
66
  // src/sts/index.ts
67
+ var phonicApiCloseCodes = {
68
+ insuffucientCapacityAvailable: 4004
69
+ };
76
70
  var SpeechToSpeech = class {
77
71
  constructor(phonic) {
78
72
  this.phonic = phonic;
79
73
  }
80
- async websocket() {
74
+ async connectToPhonicAPI(phonicApiWsUrl, config) {
81
75
  return new Promise((resolve) => {
82
- const wsBaseUrl = this.phonic.baseUrl.replace(/^http/, "ws");
83
- const ws = new WebSocket(`${wsBaseUrl}/v1/sts/ws`, {
76
+ const ws = new WebSocket(phonicApiWsUrl, {
84
77
  headers: {
85
78
  Authorization: `Bearer ${this.phonic.apiKey}`
86
79
  }
87
80
  });
88
81
  ws.onopen = () => {
89
- const phonicWebSocket = new PhonicSTSWebSocket(ws);
90
- resolve({ data: { phonicWebSocket }, error: null });
82
+ ws.send(
83
+ JSON.stringify({
84
+ type: "config",
85
+ ...config
86
+ })
87
+ );
88
+ };
89
+ ws.onmessage = (event) => {
90
+ if (typeof event.data !== "string") {
91
+ throw new Error("Received non-string message");
92
+ }
93
+ const dataObj = JSON.parse(
94
+ event.data
95
+ );
96
+ if (dataObj.type === "ready_to_start_conversation") {
97
+ resolve({
98
+ data: {
99
+ phonicWebSocket: new PhonicSTSWebSocket(ws)
100
+ },
101
+ error: null
102
+ });
103
+ }
91
104
  };
92
105
  ws.onerror = (error) => {
93
106
  resolve({
@@ -97,8 +110,48 @@ var SpeechToSpeech = class {
97
110
  }
98
111
  });
99
112
  };
113
+ ws.onclose = (event) => {
114
+ if (event.code === phonicApiCloseCodes.insuffucientCapacityAvailable) {
115
+ resolve({
116
+ data: null,
117
+ error: {
118
+ message: event.reason,
119
+ code: "insuffucient_capacity_available"
120
+ }
121
+ });
122
+ }
123
+ };
100
124
  });
101
125
  }
126
+ async websocket(config) {
127
+ const wsBaseUrl = this.phonic.baseUrl.replace(/^http/, "ws");
128
+ const phonicApiWsUrl = `${wsBaseUrl}/v1/sts/ws`;
129
+ let retryNumber = 0;
130
+ const maxRetries = 14;
131
+ const retryDelay = 15e3;
132
+ while (true) {
133
+ const connectResult = await this.connectToPhonicAPI(
134
+ phonicApiWsUrl,
135
+ config
136
+ );
137
+ if (connectResult.data !== null) {
138
+ return connectResult;
139
+ }
140
+ if (connectResult.error.code === "insuffucient_capacity_available") {
141
+ if (retryNumber >= maxRetries) {
142
+ return connectResult;
143
+ }
144
+ console.info(
145
+ `${connectResult.error.message}, will retry in ${retryDelay / 1e3}sec`
146
+ );
147
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
148
+ retryNumber += 1;
149
+ console.info(`Retrying... ${retryNumber}/${maxRetries}`);
150
+ continue;
151
+ }
152
+ return connectResult;
153
+ }
154
+ }
102
155
  };
103
156
 
104
157
  // src/voices/index.ts
@@ -136,7 +189,7 @@ var Phonic = class {
136
189
  'API key is missing. Pass it to the constructor: `new Phonic("ph_...")`'
137
190
  );
138
191
  }
139
- this.baseUrl = config?.baseUrl ?? defaultBaseUrl;
192
+ this.baseUrl = (config?.baseUrl ?? defaultBaseUrl).replace(/\/$/, "");
140
193
  this.headers = new Headers({
141
194
  Authorization: `Bearer ${this.apiKey}`,
142
195
  "User-Agent": process.env.PHONIC_USER_AGENT || defaultUserAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phonic",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Phonic Node.js SDK",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -39,9 +39,9 @@
39
39
  "@biomejs/biome": "1.9.4",
40
40
  "@changesets/changelog-github": "0.5.1",
41
41
  "@changesets/cli": "2.28.1",
42
- "@types/bun": "1.2.3",
43
- "tsup": "8.3.6",
44
- "typescript": "5.7.3",
42
+ "@types/bun": "1.2.4",
43
+ "tsup": "8.4.0",
44
+ "typescript": "5.8.2",
45
45
  "zod": "3.24.2"
46
46
  },
47
47
  "files": ["dist/**"],