@glydeunity/voice-sdk 1.2.1 → 1.2.3

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.
@@ -179,11 +179,11 @@ class g {
179
179
  headers: this.getAuthHeaders()
180
180
  });
181
181
  if (!i.ok) {
182
- const a = await i.json();
183
- throw new Error(a.error?.message || a.message || "Failed to fetch voice config");
182
+ const o = await i.json();
183
+ throw new Error(o.error?.message || o.message || "Failed to fetch voice config");
184
184
  }
185
- const { data: o } = await i.json();
186
- return o;
185
+ const { data: a } = await i.json();
186
+ return a;
187
187
  }
188
188
  /**
189
189
  * Initialize and start the voice session
@@ -207,15 +207,77 @@ class g {
207
207
  const s = await t.json();
208
208
  throw new Error(s.error?.message || s.message || "Failed to authenticate voice session");
209
209
  }
210
- const { data: i } = await t.json(), { token: o, agent_config: a, deepgram_config: r } = i, l = this.config.systemPrompt || a.instructions || this.serverConfig?.system_prompt || "You are a helpful AI assistant.";
210
+ const { data: i } = await t.json(), { token: a, agent_config: o, deepgram_config: r } = i, h = this.config.systemPrompt || o.instructions || this.serverConfig?.system_prompt || "You are a helpful AI assistant.";
211
211
  await this.initializeAudio();
212
- const c = "wss://agent.deepgram.com/v1/agent/converse";
213
- this.ws = new WebSocket(c, ["bearer", o]), this.ws.onopen = () => {
212
+ const p = "wss://agent.deepgram.com/v1/agent/converse";
213
+ this.ws = new WebSocket(p, ["bearer", a]), this.ws.onopen = () => {
214
214
  const s = this.config.deepgramConfig || r || this.serverConfig?.deepgram_config || {
215
- think: { provider: { type: "open_ai", model: "gpt-4o-mini" } },
215
+ think: {
216
+ provider: { type: "open_ai", model: "gpt-4.1-mini" },
217
+ functions: [
218
+ {
219
+ name: "end_conversation",
220
+ description: `You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
221
+
222
+ Here is a list of phrases to listen for but not restricted to:
223
+ -stop
224
+ -shut up
225
+ -go away
226
+ -turn off
227
+ -stop listening
228
+
229
+ Before ending the conversation, always say a brief, polite goodbye such as "Goodbye!", "Take care!", or "Have a great day!".
230
+
231
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos.
232
+
233
+ End the conversation immediately if:
234
+ 1. The user's input exactly matches any phrase in the list.
235
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
236
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,
237
+ parameters: {
238
+ type: "object",
239
+ properties: {
240
+ item: { type: "string", description: "The phrase or text that triggered the end of conversation" }
241
+ },
242
+ required: ["item"]
243
+ }
244
+ },
245
+ {
246
+ name: "other_opportunities",
247
+ description: `You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
248
+
249
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
250
+
251
+ Here is a list of phrases to listen for but not restricted to:
252
+ -other opportunities
253
+ -other jobs
254
+ -other roles
255
+ -other job opportunities
256
+ -other job roles
257
+ -other job opportunities
258
+
259
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
260
+
261
+ Suggest other opportunities if:
262
+ 1. The user's input exactly matches any phrase in the list.
263
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
264
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
265
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
266
+
267
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,
268
+ parameters: {
269
+ type: "object",
270
+ properties: {
271
+ item: { type: "string", description: "The phrase or text that triggered the suggestion of other opportunities" }
272
+ },
273
+ required: ["item"]
274
+ }
275
+ }
276
+ ]
277
+ },
216
278
  speak: { provider: { type: "deepgram", model: "aura-2-thalia-en" } },
217
- listen: { provider: { type: "deepgram", model: "nova-2", version: "latest" } }
218
- }, h = {
279
+ listen: { provider: { type: "deepgram", version: "v2", model: "flux-general-en" } }
280
+ }, l = {
219
281
  type: "Settings",
220
282
  audio: {
221
283
  input: {
@@ -237,36 +299,83 @@ class g {
237
299
  provider: { type: "deepgram", version: "v2", model: "flux-general-en" }
238
300
  },
239
301
  think: {
240
- provider: s.think?.provider || { type: "open_ai", model: "gpt-4o-mini" },
302
+ provider: s.think?.provider || { type: "open_ai", model: "gpt-4.1-mini" },
241
303
  functions: s.think?.functions || [
242
304
  {
243
305
  name: "end_conversation",
244
- description: "End the conversation when stop phrases are detected.",
306
+ description: `You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
307
+
308
+ Here is a list of phrases to listen for but not restricted to:
309
+ -stop
310
+ -shut up
311
+ -go away
312
+ -turn off
313
+ -stop listening
314
+
315
+ Before ending the conversation, always say a brief, polite goodbye such as "Goodbye!", "Take care!", or "Have a great day!".
316
+
317
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos.
318
+
319
+ End the conversation immediately if:
320
+ 1. The user's input exactly matches any phrase in the list.
321
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
322
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,
323
+ parameters: {
324
+ type: "object",
325
+ properties: {
326
+ item: { type: "string", description: "The phrase or text that triggered the end of conversation" }
327
+ },
328
+ required: ["item"]
329
+ }
330
+ },
331
+ {
332
+ name: "other_opportunities",
333
+ description: `You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
334
+
335
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
336
+
337
+ Here is a list of phrases to listen for but not restricted to:
338
+ -other opportunities
339
+ -other jobs
340
+ -other roles
341
+ -other job opportunities
342
+ -other job roles
343
+ -other job opportunities
344
+
345
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
346
+
347
+ Suggest other opportunities if:
348
+ 1. The user's input exactly matches any phrase in the list.
349
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
350
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
351
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
352
+
353
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,
245
354
  parameters: {
246
355
  type: "object",
247
356
  properties: {
248
- item: { type: "string", description: "The phrase that triggered end of conversation" }
357
+ item: { type: "string", description: "The phrase or text that triggered the suggestion of other opportunities" }
249
358
  },
250
359
  required: ["item"]
251
360
  }
252
361
  }
253
362
  ]
254
363
  },
255
- greeting: "Hi! I'm ready to speak with you. How can I help you today?"
364
+ greeting: "Hi! I'm excited you chose to speak with me. Are you ready to start?"
256
365
  }
257
366
  };
258
- this.ws.send(JSON.stringify(h)), this.emit({ type: "open", payload: { config: a, serverConfig: this.serverConfig } });
367
+ this.ws.send(JSON.stringify(l)), this.emit({ type: "open", payload: { config: o, serverConfig: this.serverConfig } });
259
368
  };
260
- const n = l;
369
+ const n = h;
261
370
  this.ws.onmessage = (s) => {
262
371
  if (typeof s.data == "string") {
263
372
  try {
264
373
  if (JSON.parse(s.data).type === "SettingsApplied") {
265
- const p = {
374
+ const c = {
266
375
  type: "UpdatePrompt",
267
376
  prompt: n
268
377
  };
269
- this.ws.send(JSON.stringify(p)), this.startMicrophone();
378
+ this.ws.send(JSON.stringify(c)), this.startMicrophone();
270
379
  }
271
380
  } catch {
272
381
  }
@@ -308,8 +417,8 @@ class g {
308
417
  URL.revokeObjectURL(e), URL.revokeObjectURL(t);
309
418
  }
310
419
  this.playbackWorkletNode = new AudioWorkletNode(this.audioContext, "audio-playback-processor"), this.playbackWorkletNode.connect(this.audioContext.destination), this.playbackWorkletNode.port.onmessage = (i) => {
311
- const { type: o } = i.data;
312
- (o === "cleared" || o === "bufferEmpty") && (this.isAgentSpeaking = !1, this.agentAudioDoneReceived = !1, this.emit({ type: "agent_speaking", payload: !1 }));
420
+ const { type: a } = i.data;
421
+ (a === "cleared" || a === "bufferEmpty") && (this.isAgentSpeaking = !1, this.agentAudioDoneReceived = !1, this.emit({ type: "agent_speaking", payload: !1 }));
313
422
  };
314
423
  }
315
424
  /**
@@ -368,28 +477,28 @@ class g {
368
477
  if (t === 0) return;
369
478
  const i = t - t % 2;
370
479
  if (i === 0) return;
371
- const o = i === t ? e : e.slice(0, i), a = new Int16Array(o), r = new Float32Array(a.length);
372
- for (let n = 0; n < a.length; n++)
373
- r[n] = a[n] / 32768;
374
- const l = this.resample24kTo48k(r);
480
+ const a = i === t ? e : e.slice(0, i), o = new Int16Array(a), r = new Float32Array(o.length);
481
+ for (let n = 0; n < o.length; n++)
482
+ r[n] = o[n] / 32768;
483
+ const h = this.resample24kTo48k(r);
375
484
  !this.isAgentSpeaking && !this.agentAudioDoneReceived && (this.isAgentSpeaking = !0, this.emit({ type: "agent_speaking", payload: !0 }));
376
- const c = new Float32Array(l);
485
+ const p = new Float32Array(h);
377
486
  this.playbackWorkletNode.port.postMessage({
378
487
  type: "audio",
379
- data: c
380
- }, [c.buffer]);
488
+ data: p
489
+ }, [p.buffer]);
381
490
  }
382
491
  /**
383
492
  * Resample audio from 24kHz to 48kHz using linear interpolation
384
493
  */
385
494
  resample24kTo48k(e) {
386
495
  const t = e.length * 2, i = new Float32Array(t);
387
- for (let a = 0; a < e.length - 1; a++) {
388
- const r = e[a], l = e[a + 1];
389
- i[a * 2] = r, i[a * 2 + 1] = (r + l) / 2;
496
+ for (let o = 0; o < e.length - 1; o++) {
497
+ const r = e[o], h = e[o + 1];
498
+ i[o * 2] = r, i[o * 2 + 1] = (r + h) / 2;
390
499
  }
391
- const o = e.length - 1;
392
- return i[o * 2] = e[o], i[o * 2 + 1] = e[o], i;
500
+ const a = e.length - 1;
501
+ return i[a * 2] = e[a], i[a * 2 + 1] = e[a], i;
393
502
  }
394
503
  /**
395
504
  * Clear the playback buffer (for interruption handling)
@@ -1,4 +1,4 @@
1
- (function(n,c){typeof exports=="object"&&typeof module<"u"?c(exports):typeof define=="function"&&define.amd?define(["exports"],c):(n=typeof globalThis<"u"?globalThis:n||self,c(n.GlydeVoice={}))})(this,(function(n){"use strict";const c=`
1
+ (function(n,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(n=typeof globalThis<"u"?globalThis:n||self,l(n.GlydeVoice={}))})(this,(function(n){"use strict";const l=`
2
2
  class AudioCaptureProcessor extends AudioWorkletProcessor {
3
3
  constructor() {
4
4
  super();
@@ -130,7 +130,79 @@ class AudioPlaybackProcessor extends AudioWorkletProcessor {
130
130
  }
131
131
 
132
132
  registerProcessor('audio-playback-processor', AudioPlaybackProcessor);
133
- `;class f{config;unityUrl;active=!1;serverConfig=null;ws=null;audioContext=null;mediaStream=null;captureWorkletNode=null;playbackWorkletNode=null;isMuted=!1;outputSampleRate=24e3;inputSampleRate=48e3;isAgentSpeaking=!1;agentAudioDoneReceived=!1;constructor(e){this.config=e,this.unityUrl=e.unityBaseUrl||"https://api.glydeunity.com",!e.publishableKey&&!e.apiKey&&!e.authToken&&console.warn("[GlydeVoice] No authentication method provided. One of publishableKey, apiKey, or authToken is required.")}getAuthHeaders(){const e={"Content-Type":"application/json"};return this.config.publishableKey&&(e["x-publishable-key"]=this.config.publishableKey),this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.authToken&&(e.Authorization=`Bearer ${this.config.authToken}`),e}async fetchConfig(){const e=`${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`,t=this.config.contextId?`${e}/${this.config.contextId}`:e,i=await fetch(t,{method:"GET",headers:this.getAuthHeaders()});if(!i.ok){const o=await i.json();throw new Error(o.error?.message||o.message||"Failed to fetch voice config")}const{data:a}=await i.json();return a}async start(){if(!this.active){this.active=!0;try{this.config.systemPrompt||(this.serverConfig=await this.fetchConfig(),console.log("[GlydeVoice] Fetched config:",this.serverConfig));const e={context_id:this.config.contextId,domain:typeof window<"u"?window.location.hostname:"localhost"};this.config.systemPrompt&&(e.system_prompt=this.config.systemPrompt),this.config.deepgramConfig&&(e.deepgram_config=this.config.deepgramConfig);const t=await fetch(`${this.unityUrl}/api/unity/voice/auth`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify(e)});if(!t.ok){const s=await t.json();throw new Error(s.error?.message||s.message||"Failed to authenticate voice session")}const{data:i}=await t.json(),{token:a,agent_config:o,deepgram_config:r}=i,h=this.config.systemPrompt||o.instructions||this.serverConfig?.system_prompt||"You are a helpful AI assistant.";await this.initializeAudio();const d="wss://agent.deepgram.com/v1/agent/converse";this.ws=new WebSocket(d,["bearer",a]),this.ws.onopen=()=>{const s=this.config.deepgramConfig||r||this.serverConfig?.deepgram_config||{think:{provider:{type:"open_ai",model:"gpt-4o-mini"}},speak:{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:{provider:{type:"deepgram",model:"nova-2",version:"latest"}}},p={type:"Settings",audio:{input:{encoding:"linear16",sample_rate:this.inputSampleRate},output:{encoding:"linear16",sample_rate:this.outputSampleRate,container:"none"}},agent:{language:"en",speak:s.speak||{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:s.listen||{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}},think:{provider:s.think?.provider||{type:"open_ai",model:"gpt-4o-mini"},functions:s.think?.functions||[{name:"end_conversation",description:"End the conversation when stop phrases are detected.",parameters:{type:"object",properties:{item:{type:"string",description:"The phrase that triggered end of conversation"}},required:["item"]}}]},greeting:"Hi! I'm ready to speak with you. How can I help you today?"}};this.ws.send(JSON.stringify(p)),this.emit({type:"open",payload:{config:o,serverConfig:this.serverConfig}})};const l=h;this.ws.onmessage=s=>{if(typeof s.data=="string"){try{if(JSON.parse(s.data).type==="SettingsApplied"){const g={type:"UpdatePrompt",prompt:l};this.ws.send(JSON.stringify(g)),this.startMicrophone()}}catch{}this.handleTextMessage(s.data)}else s.data instanceof Blob?this.handleAudioData(s.data):s.data instanceof ArrayBuffer&&this.handleAudioBuffer(s.data)},this.ws.onerror=s=>{console.error("[GlydeVoice] WebSocket error:",s),this.emit({type:"error",payload:s})},this.ws.onclose=()=>{this.cleanup(),this.emit({type:"close"})},this.renderUI()}catch(e){throw console.error("[GlydeVoice] Error starting session:",e),this.active=!1,this.emit({type:"error",payload:e}),e}}}createWorkletBlobUrl(e){const t=new Blob([e],{type:"application/javascript"});return URL.createObjectURL(t)}async initializeAudio(){this.audioContext=new AudioContext({sampleRate:this.inputSampleRate});const e=this.createWorkletBlobUrl(c),t=this.createWorkletBlobUrl(u);try{await Promise.all([this.audioContext.audioWorklet.addModule(e),this.audioContext.audioWorklet.addModule(t)])}finally{URL.revokeObjectURL(e),URL.revokeObjectURL(t)}this.playbackWorkletNode=new AudioWorkletNode(this.audioContext,"audio-playback-processor"),this.playbackWorkletNode.connect(this.audioContext.destination),this.playbackWorkletNode.port.onmessage=i=>{const{type:a}=i.data;(a==="cleared"||a==="bufferEmpty")&&(this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!1}))}}handleTextMessage(e){try{const t=JSON.parse(e);switch(t.type){case"Welcome":this.emit({type:"ready"});break;case"SettingsApplied":break;case"UserStartedSpeaking":this.emit({type:"user_speaking",payload:!0}),this.clearPlaybackBuffer(),this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1;break;case"UserStoppedSpeaking":this.emit({type:"user_speaking",payload:!1});break;case"ConversationText":if(t.content&&t.content.trim()){const i=t.role==="assistant"?"agent":"user";this.config.onTranscript&&this.config.onTranscript(t.content,i),this.emit({type:"transcript",payload:{text:t.content,role:i}}),this.saveTranscript(t.content,t.role)}break;case"AgentStartedSpeaking":this.isAgentSpeaking=!0,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!0});break;case"AgentAudioDone":this.agentAudioDoneReceived=!0;break;case"Error":console.error("[GlydeVoice] Agent error:",t),this.emit({type:"error",payload:t});break}}catch(t){console.error("[GlydeVoice] Failed to parse message:",t)}}async handleAudioData(e){const t=await e.arrayBuffer();this.handleAudioBuffer(t)}handleAudioBuffer(e){if(!this.playbackWorkletNode||!this.audioContext)return;this.audioContext.state==="suspended"&&this.audioContext.resume();const t=e.byteLength;if(t===0)return;const i=t-t%2;if(i===0)return;const a=i===t?e:e.slice(0,i),o=new Int16Array(a),r=new Float32Array(o.length);for(let l=0;l<o.length;l++)r[l]=o[l]/32768;const h=this.resample24kTo48k(r);!this.isAgentSpeaking&&!this.agentAudioDoneReceived&&(this.isAgentSpeaking=!0,this.emit({type:"agent_speaking",payload:!0}));const d=new Float32Array(h);this.playbackWorkletNode.port.postMessage({type:"audio",data:d},[d.buffer])}resample24kTo48k(e){const t=e.length*2,i=new Float32Array(t);for(let o=0;o<e.length-1;o++){const r=e[o],h=e[o+1];i[o*2]=r,i[o*2+1]=(r+h)/2}const a=e.length-1;return i[a*2]=e[a],i[a*2+1]=e[a],i}clearPlaybackBuffer(){this.playbackWorkletNode&&this.playbackWorkletNode.port.postMessage({type:"clear"})}async startMicrophone(){if(!this.audioContext)throw new Error("Audio context not initialized");try{this.mediaStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:this.inputSampleRate,echoCancellation:!0,noiseSuppression:!0}});const e=this.audioContext.createMediaStreamSource(this.mediaStream);this.captureWorkletNode=new AudioWorkletNode(this.audioContext,"audio-capture-processor"),this.captureWorkletNode.port.onmessage=t=>{!this.active||!this.ws||this.ws.readyState!==WebSocket.OPEN||this.isMuted||this.ws.send(t.data)},e.connect(this.captureWorkletNode),this.emit({type:"microphone_ready"})}catch(e){throw console.error("[GlydeVoice] Microphone error:",e),e}}async saveTranscript(e,t){if(!(!this.config.contextId||!e))try{await fetch(`${this.unityUrl}/api/unity/voice/transcript`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify({context_id:this.config.contextId,content:e,role:t==="assistant"?"assistant":"user"})})}catch{}}setMuted(e){this.isMuted=e}getMuted(){return this.isMuted}isActive(){return this.active}getServerConfig(){return this.serverConfig}stop(){this.active=!1,this.cleanup()}cleanup(){this.captureWorkletNode&&(this.captureWorkletNode.disconnect(),this.captureWorkletNode.port.close(),this.captureWorkletNode=null),this.playbackWorkletNode&&(this.playbackWorkletNode.disconnect(),this.playbackWorkletNode.port.close(),this.playbackWorkletNode=null),this.mediaStream&&(this.mediaStream.getTracks().forEach(e=>e.stop()),this.mediaStream=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.ws&&(this.ws.readyState===WebSocket.OPEN&&this.ws.close(),this.ws=null)}emit(e){this.config.onEvent&&this.config.onEvent(e)}renderUI(){if(!this.config.container)return;const e=typeof this.config.container=="string"?document.querySelector(this.config.container):this.config.container;e&&(e.innerHTML=`
133
+ `;class f{config;unityUrl;active=!1;serverConfig=null;ws=null;audioContext=null;mediaStream=null;captureWorkletNode=null;playbackWorkletNode=null;isMuted=!1;outputSampleRate=24e3;inputSampleRate=48e3;isAgentSpeaking=!1;agentAudioDoneReceived=!1;constructor(e){this.config=e,this.unityUrl=e.unityBaseUrl||"https://api.glydeunity.com",!e.publishableKey&&!e.apiKey&&!e.authToken&&console.warn("[GlydeVoice] No authentication method provided. One of publishableKey, apiKey, or authToken is required.")}getAuthHeaders(){const e={"Content-Type":"application/json"};return this.config.publishableKey&&(e["x-publishable-key"]=this.config.publishableKey),this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.authToken&&(e.Authorization=`Bearer ${this.config.authToken}`),e}async fetchConfig(){const e=`${this.unityUrl}/api/unity/voice/config/${this.config.contextType}`,t=this.config.contextId?`${e}/${this.config.contextId}`:e,i=await fetch(t,{method:"GET",headers:this.getAuthHeaders()});if(!i.ok){const o=await i.json();throw new Error(o.error?.message||o.message||"Failed to fetch voice config")}const{data:a}=await i.json();return a}async start(){if(!this.active){this.active=!0;try{this.config.systemPrompt||(this.serverConfig=await this.fetchConfig(),console.log("[GlydeVoice] Fetched config:",this.serverConfig));const e={context_id:this.config.contextId,domain:typeof window<"u"?window.location.hostname:"localhost"};this.config.systemPrompt&&(e.system_prompt=this.config.systemPrompt),this.config.deepgramConfig&&(e.deepgram_config=this.config.deepgramConfig);const t=await fetch(`${this.unityUrl}/api/unity/voice/auth`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify(e)});if(!t.ok){const s=await t.json();throw new Error(s.error?.message||s.message||"Failed to authenticate voice session")}const{data:i}=await t.json(),{token:a,agent_config:o,deepgram_config:r}=i,p=this.config.systemPrompt||o.instructions||this.serverConfig?.system_prompt||"You are a helpful AI assistant.";await this.initializeAudio();const c="wss://agent.deepgram.com/v1/agent/converse";this.ws=new WebSocket(c,["bearer",a]),this.ws.onopen=()=>{const s=this.config.deepgramConfig||r||this.serverConfig?.deepgram_config||{think:{provider:{type:"open_ai",model:"gpt-4.1-mini"},functions:[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
134
+
135
+ Here is a list of phrases to listen for but not restricted to:
136
+ -stop
137
+ -shut up
138
+ -go away
139
+ -turn off
140
+ -stop listening
141
+
142
+ Before ending the conversation, always say a brief, polite goodbye such as "Goodbye!", "Take care!", or "Have a great day!".
143
+
144
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos.
145
+
146
+ End the conversation immediately if:
147
+ 1. The user's input exactly matches any phrase in the list.
148
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
149
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}},{name:"other_opportunities",description:`You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
150
+
151
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
152
+
153
+ Here is a list of phrases to listen for but not restricted to:
154
+ -other opportunities
155
+ -other jobs
156
+ -other roles
157
+ -other job opportunities
158
+ -other job roles
159
+ -other job opportunities
160
+
161
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
162
+
163
+ Suggest other opportunities if:
164
+ 1. The user's input exactly matches any phrase in the list.
165
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
166
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
167
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
168
+
169
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the suggestion of other opportunities"}},required:["item"]}}]},speak:{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}}},d={type:"Settings",audio:{input:{encoding:"linear16",sample_rate:this.inputSampleRate},output:{encoding:"linear16",sample_rate:this.outputSampleRate,container:"none"}},agent:{language:"en",speak:s.speak||{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:s.listen||{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}},think:{provider:s.think?.provider||{type:"open_ai",model:"gpt-4.1-mini"},functions:s.think?.functions||[{name:"end_conversation",description:`You are an AI assistant that monitors conversations and ends them when specific stop phrases are detected.
170
+
171
+ Here is a list of phrases to listen for but not restricted to:
172
+ -stop
173
+ -shut up
174
+ -go away
175
+ -turn off
176
+ -stop listening
177
+
178
+ Before ending the conversation, always say a brief, polite goodbye such as "Goodbye!", "Take care!", or "Have a great day!".
179
+
180
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos.
181
+
182
+ End the conversation immediately if:
183
+ 1. The user's input exactly matches any phrase in the list.
184
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please shut up" instead of "shut up").
185
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the end of conversation"}},required:["item"]}},{name:"other_opportunities",description:`You are an AI assistant that monitors conversations to identify whether the candidate should be informed about other job opportunities.
186
+
187
+ If the candidate appears to be a poor fit for any of the must have requirements, gently suggest that there are other job opportunities with the company and you could inform about other roles if they are interested. If they are not interested, you should continue with the conversation about the current role.
188
+
189
+ Here is a list of phrases to listen for but not restricted to:
190
+ -other opportunities
191
+ -other jobs
192
+ -other roles
193
+ -other job opportunities
194
+ -other job roles
195
+ -other job opportunities
196
+
197
+ When monitoring the conversation, pay close attention to any input that matches or closely resembles the phrases listed above. The matching should be case-insensitive and allow for minor variations or typos. Additionally monitor for input that suggests the candidate does not meet the criteria for the current role or if they'd like to know about urgent or immediate opportunities.
198
+
199
+ Suggest other opportunities if:
200
+ 1. The user's input exactly matches any phrase in the list.
201
+ 2. The user's input is a close variation of any phrase in the list (e.g., "please other opportunities" instead of "other opportunities").
202
+ 3. The user's input clearly expresses a desire to end the conversation, even if it doesn't use the exact phrases listed.
203
+ 4. The user's input clearly expresses a desire to know about urgent or immediate opportunities.
204
+
205
+ If the candidate is interested in other opportunities, you should call a GLYDE Unity MCP tool to identify other job openings.`,parameters:{type:"object",properties:{item:{type:"string",description:"The phrase or text that triggered the suggestion of other opportunities"}},required:["item"]}}]},greeting:"Hi! I'm excited you chose to speak with me. Are you ready to start?"}};this.ws.send(JSON.stringify(d)),this.emit({type:"open",payload:{config:o,serverConfig:this.serverConfig}})};const h=p;this.ws.onmessage=s=>{if(typeof s.data=="string"){try{if(JSON.parse(s.data).type==="SettingsApplied"){const y={type:"UpdatePrompt",prompt:h};this.ws.send(JSON.stringify(y)),this.startMicrophone()}}catch{}this.handleTextMessage(s.data)}else s.data instanceof Blob?this.handleAudioData(s.data):s.data instanceof ArrayBuffer&&this.handleAudioBuffer(s.data)},this.ws.onerror=s=>{console.error("[GlydeVoice] WebSocket error:",s),this.emit({type:"error",payload:s})},this.ws.onclose=()=>{this.cleanup(),this.emit({type:"close"})},this.renderUI()}catch(e){throw console.error("[GlydeVoice] Error starting session:",e),this.active=!1,this.emit({type:"error",payload:e}),e}}}createWorkletBlobUrl(e){const t=new Blob([e],{type:"application/javascript"});return URL.createObjectURL(t)}async initializeAudio(){this.audioContext=new AudioContext({sampleRate:this.inputSampleRate});const e=this.createWorkletBlobUrl(l),t=this.createWorkletBlobUrl(u);try{await Promise.all([this.audioContext.audioWorklet.addModule(e),this.audioContext.audioWorklet.addModule(t)])}finally{URL.revokeObjectURL(e),URL.revokeObjectURL(t)}this.playbackWorkletNode=new AudioWorkletNode(this.audioContext,"audio-playback-processor"),this.playbackWorkletNode.connect(this.audioContext.destination),this.playbackWorkletNode.port.onmessage=i=>{const{type:a}=i.data;(a==="cleared"||a==="bufferEmpty")&&(this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!1}))}}handleTextMessage(e){try{const t=JSON.parse(e);switch(t.type){case"Welcome":this.emit({type:"ready"});break;case"SettingsApplied":break;case"UserStartedSpeaking":this.emit({type:"user_speaking",payload:!0}),this.clearPlaybackBuffer(),this.isAgentSpeaking=!1,this.agentAudioDoneReceived=!1;break;case"UserStoppedSpeaking":this.emit({type:"user_speaking",payload:!1});break;case"ConversationText":if(t.content&&t.content.trim()){const i=t.role==="assistant"?"agent":"user";this.config.onTranscript&&this.config.onTranscript(t.content,i),this.emit({type:"transcript",payload:{text:t.content,role:i}}),this.saveTranscript(t.content,t.role)}break;case"AgentStartedSpeaking":this.isAgentSpeaking=!0,this.agentAudioDoneReceived=!1,this.emit({type:"agent_speaking",payload:!0});break;case"AgentAudioDone":this.agentAudioDoneReceived=!0;break;case"Error":console.error("[GlydeVoice] Agent error:",t),this.emit({type:"error",payload:t});break}}catch(t){console.error("[GlydeVoice] Failed to parse message:",t)}}async handleAudioData(e){const t=await e.arrayBuffer();this.handleAudioBuffer(t)}handleAudioBuffer(e){if(!this.playbackWorkletNode||!this.audioContext)return;this.audioContext.state==="suspended"&&this.audioContext.resume();const t=e.byteLength;if(t===0)return;const i=t-t%2;if(i===0)return;const a=i===t?e:e.slice(0,i),o=new Int16Array(a),r=new Float32Array(o.length);for(let h=0;h<o.length;h++)r[h]=o[h]/32768;const p=this.resample24kTo48k(r);!this.isAgentSpeaking&&!this.agentAudioDoneReceived&&(this.isAgentSpeaking=!0,this.emit({type:"agent_speaking",payload:!0}));const c=new Float32Array(p);this.playbackWorkletNode.port.postMessage({type:"audio",data:c},[c.buffer])}resample24kTo48k(e){const t=e.length*2,i=new Float32Array(t);for(let o=0;o<e.length-1;o++){const r=e[o],p=e[o+1];i[o*2]=r,i[o*2+1]=(r+p)/2}const a=e.length-1;return i[a*2]=e[a],i[a*2+1]=e[a],i}clearPlaybackBuffer(){this.playbackWorkletNode&&this.playbackWorkletNode.port.postMessage({type:"clear"})}async startMicrophone(){if(!this.audioContext)throw new Error("Audio context not initialized");try{this.mediaStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:this.inputSampleRate,echoCancellation:!0,noiseSuppression:!0}});const e=this.audioContext.createMediaStreamSource(this.mediaStream);this.captureWorkletNode=new AudioWorkletNode(this.audioContext,"audio-capture-processor"),this.captureWorkletNode.port.onmessage=t=>{!this.active||!this.ws||this.ws.readyState!==WebSocket.OPEN||this.isMuted||this.ws.send(t.data)},e.connect(this.captureWorkletNode),this.emit({type:"microphone_ready"})}catch(e){throw console.error("[GlydeVoice] Microphone error:",e),e}}async saveTranscript(e,t){if(!(!this.config.contextId||!e))try{await fetch(`${this.unityUrl}/api/unity/voice/transcript`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify({context_id:this.config.contextId,content:e,role:t==="assistant"?"assistant":"user"})})}catch{}}setMuted(e){this.isMuted=e}getMuted(){return this.isMuted}isActive(){return this.active}getServerConfig(){return this.serverConfig}stop(){this.active=!1,this.cleanup()}cleanup(){this.captureWorkletNode&&(this.captureWorkletNode.disconnect(),this.captureWorkletNode.port.close(),this.captureWorkletNode=null),this.playbackWorkletNode&&(this.playbackWorkletNode.disconnect(),this.playbackWorkletNode.port.close(),this.playbackWorkletNode=null),this.mediaStream&&(this.mediaStream.getTracks().forEach(e=>e.stop()),this.mediaStream=null),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.ws&&(this.ws.readyState===WebSocket.OPEN&&this.ws.close(),this.ws=null)}emit(e){this.config.onEvent&&this.config.onEvent(e)}renderUI(){if(!this.config.container)return;const e=typeof this.config.container=="string"?document.querySelector(this.config.container):this.config.container;e&&(e.innerHTML=`
134
206
  <div style="padding: 20px; border: 1px solid #ccc; border-radius: 8px; background: #fff;">
135
207
  <h3>Glyde Voice Agent</h3>
136
208
  <p>Status: Active</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glydeunity/voice-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "GLYDE Voice Agent SDK - AI-powered voice interactions for web applications",
5
5
  "type": "module",
6
6
  "main": "./dist/voice-sdk.umd.js",