@glydeunity/voice-sdk 1.3.6 → 1.4.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.
- package/dist/glyde-chat.umd.js +265 -0
- package/dist/glyde-chat.umd.js.map +1 -0
- package/dist/index.d.ts +851 -16
- package/dist/voice-sdk.es.js +1738 -70
- package/dist/voice-sdk.es.js.map +1 -0
- package/package.json +29 -9
- package/dist/voice-sdk.umd.js +0 -140
package/dist/voice-sdk.umd.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
(function(c,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(c=typeof globalThis<"u"?globalThis:c||self,l(c.GlydeVoice={}))})(this,(function(c){"use strict";const l=`
|
|
2
|
-
class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
3
|
-
constructor() {
|
|
4
|
-
super();
|
|
5
|
-
this.bufferSize = 4096;
|
|
6
|
-
this.buffer = new Float32Array(this.bufferSize);
|
|
7
|
-
this.bufferIndex = 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
process(inputs) {
|
|
11
|
-
const input = inputs[0];
|
|
12
|
-
if (!input || !input[0]) return true;
|
|
13
|
-
|
|
14
|
-
const samples = input[0];
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < samples.length; i++) {
|
|
17
|
-
this.buffer[this.bufferIndex++] = samples[i];
|
|
18
|
-
|
|
19
|
-
if (this.bufferIndex >= this.bufferSize) {
|
|
20
|
-
const pcm16 = new Int16Array(this.bufferSize);
|
|
21
|
-
for (let j = 0; j < this.bufferSize; j++) {
|
|
22
|
-
const s = Math.max(-1, Math.min(1, this.buffer[j]));
|
|
23
|
-
pcm16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
this.port.postMessage(pcm16.buffer, [pcm16.buffer]);
|
|
27
|
-
this.bufferIndex = 0;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
registerProcessor('audio-capture-processor', AudioCaptureProcessor);
|
|
36
|
-
`,p=`
|
|
37
|
-
class AudioPlaybackProcessor extends AudioWorkletProcessor {
|
|
38
|
-
constructor() {
|
|
39
|
-
super();
|
|
40
|
-
|
|
41
|
-
this.bufferSize = 48000 * 15;
|
|
42
|
-
this.buffer = new Float32Array(this.bufferSize);
|
|
43
|
-
this.writeIndex = 0;
|
|
44
|
-
this.readIndex = 0;
|
|
45
|
-
this.samplesAvailable = 0;
|
|
46
|
-
this.isPlaying = false;
|
|
47
|
-
|
|
48
|
-
this.port.onmessage = (event) => {
|
|
49
|
-
const { type, data } = event.data;
|
|
50
|
-
|
|
51
|
-
switch (type) {
|
|
52
|
-
case 'audio':
|
|
53
|
-
const audioData = data instanceof Float32Array ? data : new Float32Array(data);
|
|
54
|
-
this.writeAudio(audioData);
|
|
55
|
-
break;
|
|
56
|
-
case 'clear':
|
|
57
|
-
this.clearBuffer();
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
writeAudio(samples) {
|
|
64
|
-
if (!samples || samples.length === 0) return;
|
|
65
|
-
|
|
66
|
-
const samplesToWrite = samples.length;
|
|
67
|
-
|
|
68
|
-
if (this.samplesAvailable + samplesToWrite > this.bufferSize) {
|
|
69
|
-
const overflow = (this.samplesAvailable + samplesToWrite) - this.bufferSize;
|
|
70
|
-
this.readIndex = (this.readIndex + overflow) % this.bufferSize;
|
|
71
|
-
this.samplesAvailable -= overflow;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < samplesToWrite; i++) {
|
|
75
|
-
this.buffer[this.writeIndex] = samples[i];
|
|
76
|
-
this.writeIndex = (this.writeIndex + 1) % this.bufferSize;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.samplesAvailable += samplesToWrite;
|
|
80
|
-
this.isPlaying = true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
clearBuffer() {
|
|
84
|
-
this.readIndex = 0;
|
|
85
|
-
this.writeIndex = 0;
|
|
86
|
-
this.samplesAvailable = 0;
|
|
87
|
-
this.isPlaying = false;
|
|
88
|
-
this.port.postMessage({ type: 'cleared' });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
process(inputs, outputs) {
|
|
92
|
-
const output = outputs[0];
|
|
93
|
-
if (!output || !output[0]) return true;
|
|
94
|
-
|
|
95
|
-
const outputChannel = output[0];
|
|
96
|
-
const samplesToRead = outputChannel.length;
|
|
97
|
-
|
|
98
|
-
if (this.samplesAvailable >= samplesToRead) {
|
|
99
|
-
for (let i = 0; i < samplesToRead; i++) {
|
|
100
|
-
outputChannel[i] = this.buffer[this.readIndex];
|
|
101
|
-
this.readIndex = (this.readIndex + 1) % this.bufferSize;
|
|
102
|
-
}
|
|
103
|
-
this.samplesAvailable -= samplesToRead;
|
|
104
|
-
} else if (this.samplesAvailable > 0) {
|
|
105
|
-
let i = 0;
|
|
106
|
-
while (this.samplesAvailable > 0 && i < samplesToRead) {
|
|
107
|
-
outputChannel[i] = this.buffer[this.readIndex];
|
|
108
|
-
this.readIndex = (this.readIndex + 1) % this.bufferSize;
|
|
109
|
-
this.samplesAvailable--;
|
|
110
|
-
i++;
|
|
111
|
-
}
|
|
112
|
-
while (i < samplesToRead) {
|
|
113
|
-
outputChannel[i] = 0;
|
|
114
|
-
i++;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (this.isPlaying) {
|
|
118
|
-
this.isPlaying = false;
|
|
119
|
-
this.port.postMessage({ type: 'bufferEmpty' });
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
for (let i = 0; i < samplesToRead; i++) {
|
|
123
|
-
outputChannel[i] = 0;
|
|
124
|
-
}
|
|
125
|
-
this.isPlaying = false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
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;sessionContext={};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,s=await fetch(t,{method:"GET",headers:this.getAuthHeaders()});if(!s.ok){const o=await s.json();throw new Error(o.error?.message||o.message||"Failed to fetch voice config")}const{data:i}=await s.json();return i}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 a=await t.json();throw new Error(a.error?.message||a.message||"Failed to authenticate voice session")}const{data:s}=await t.json(),{token:i,agent_config:o,deepgram_config:n}=s;this.setSessionContext({clientUuid:o?.client_uuid,contextId:this.config.contextId,contextType:this.config.contextType,currentJobUuid:o?.job_uuid});const d=this.config.systemPrompt||o.instructions||this.serverConfig?.system_prompt||"You are a helpful AI assistant.";await this.initializeAudio();let u="wss://agent.deepgram.com/v1/agent/converse";const r=this.config.deepgramConfig||n||this.serverConfig?.deepgram_config;if(r?.tags&&r.tags.length>0){const a=new URLSearchParams;r.tags.forEach(h=>a.append("tag",h)),u+=`?${a.toString()}`}this.ws=new WebSocket(u,["bearer",i]),this.ws.onopen=()=>{const a=r||{think:{provider:{type:"open_ai",model:"gpt-4.1-nano"}},speak:{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}}},h={type:"Settings",audio:{input:{encoding:"linear16",sample_rate:this.inputSampleRate},output:{encoding:"linear16",sample_rate:this.outputSampleRate,container:"none"}},agent:{language:"en",speak:a.speak||{provider:{type:"deepgram",model:"aura-2-thalia-en"}},listen:a.listen||{provider:{type:"deepgram",version:"v2",model:"flux-general-en"}},think:{provider:a.think?.provider||{type:"open_ai",model:"gpt-4.1-nano"},...a.think?.functions&&{functions:a.think.functions}},greeting:"Hi! I'm excited you chose to speak with me. Are you ready to start?"}};a.tags&&a.tags.length>0&&(h.tags=a.tags),this.ws.send(JSON.stringify(h)),this.emit({type:"open",payload:{config:o,serverConfig:this.serverConfig}})};const g=d;this.ws.onmessage=a=>{if(typeof a.data=="string"){try{if(JSON.parse(a.data).type==="SettingsApplied"){const y={type:"UpdatePrompt",prompt:g};this.ws.send(JSON.stringify(y)),this.startMicrophone()}}catch{}this.handleTextMessage(a.data)}else a.data instanceof Blob?this.handleAudioData(a.data):a.data instanceof ArrayBuffer&&this.handleAudioBuffer(a.data)},this.ws.onerror=a=>{console.error("[GlydeVoice] WebSocket error:",a),this.emit({type:"error",payload:a})},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(p);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=s=>{const{type:i}=s.data;(i==="cleared"||i==="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 s=t.role==="assistant"?"agent":"user";this.config.onTranscript&&this.config.onTranscript(t.content,s),this.emit({type:"transcript",payload:{text:t.content,role:s}}),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;case"FunctionCallRequest":this.handleFunctionCallRequest(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 s=t-t%2;if(s===0)return;const i=s===t?e:e.slice(0,s),o=new Int16Array(i),n=new Float32Array(o.length);for(let r=0;r<o.length;r++)n[r]=o[r]/32768;const d=this.resample24kTo48k(n);!this.isAgentSpeaking&&!this.agentAudioDoneReceived&&(this.isAgentSpeaking=!0,this.emit({type:"agent_speaking",payload:!0}));const u=new Float32Array(d);this.playbackWorkletNode.port.postMessage({type:"audio",data:u},[u.buffer])}resample24kTo48k(e){const t=e.length*2,s=new Float32Array(t);for(let o=0;o<e.length-1;o++){const n=e[o],d=e[o+1];s[o*2]=n,s[o*2+1]=(n+d)/2}const i=e.length-1;return s[i*2]=e[i],s[i*2+1]=e[i],s}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
|
-
<div style="padding: 20px; border: 1px solid #ccc; border-radius: 8px; background: #fff;">
|
|
135
|
-
<h3>Glyde Voice Agent</h3>
|
|
136
|
-
<p>Status: Active</p>
|
|
137
|
-
<p>Context: ${this.config.contextType}</p>
|
|
138
|
-
<button onclick="this.closest('div').remove()">Close</button>
|
|
139
|
-
</div>
|
|
140
|
-
`)}async handleFunctionCallRequest(e){for(const t of e.functions){console.log("[GlydeVoice] Function call request:",t.name,t.arguments);let s={};try{s=t.arguments?JSON.parse(t.arguments):{}}catch(n){console.warn("[GlydeVoice] Failed to parse function arguments:",n)}let i;try{t.name==="end_conversation"?i=await this.handleEndConversation(s):i=await this.executeVoiceFunction(t.name,t.id,s)}catch(n){console.error("[GlydeVoice] Function call error:",n),i=JSON.stringify({error:"Function execution failed",details:n instanceof Error?n.message:String(n)})}const o={type:"FunctionCallResponse",id:t.id,name:t.name,content:i};this.ws&&this.ws.readyState===WebSocket.OPEN?(this.ws.send(JSON.stringify(o)),console.log("[GlydeVoice] Function response sent:",t.name)):console.error("[GlydeVoice] Cannot send function response - WebSocket not open")}}async executeVoiceFunction(e,t,s){console.log("[GlydeVoice] Executing voice function via Unity API:",e);try{const i=await fetch(`${this.unityUrl}/api/unity/voice/function`,{method:"POST",headers:this.getAuthHeaders(),body:JSON.stringify({function_name:e,function_call_id:t,input:s,context:{context_id:this.sessionContext.contextId,context_type:this.sessionContext.contextType,current_job_uuid:this.sessionContext.currentJobUuid}})});if(!i.ok){const n=await i.json().catch(()=>({}));throw new Error(n.error?.message||`Function call failed: ${i.status}`)}const o=await i.json();if(o.success&&o.data?.output)return typeof o.data.output=="string"?o.data.output:JSON.stringify(o.data.output);throw new Error("Invalid response from voice function endpoint")}catch(i){return console.error("[GlydeVoice] Voice function error:",i),JSON.stringify({success:!1,error:i instanceof Error?i.message:"Function execution failed",fallback_message:"I apologize, but I'm having trouble with that request right now. Is there something else I can help you with?"})}}async handleEndConversation(e){const t=e.item||"user request";return console.log(`[GlydeVoice] End conversation triggered by: ${t}`),setTimeout(()=>{this.stop()},2e3),JSON.stringify({success:!0,message:"Conversation ending. Say goodbye to the user.",trigger_phrase:t})}setSessionContext(e){this.sessionContext={...this.sessionContext,...e},console.log("[GlydeVoice] Session context updated:",{hasContextId:!!e.contextId,contextType:e.contextType,hasJobUuid:!!e.currentJobUuid})}}c.GlydeVoice=f,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}));
|