@layercode/js-sdk 1.0.6 → 1.0.7

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.
@@ -1510,8 +1510,6 @@ function arrayBufferToBase64(arrayBuffer) {
1510
1510
  * @classdesc Core client for Layercode audio pipeline that manages audio recording, WebSocket communication, and speech processing.
1511
1511
  */
1512
1512
  class LayercodeClient {
1513
- // private currentTurnId: string | null = null;
1514
- // private lastDeltaIdPlayed: string | null = null;
1515
1513
  /**
1516
1514
  * Creates an instance of LayercodeClient.
1517
1515
  * @param {Object} options - Configuration options
@@ -1541,12 +1539,8 @@ class LayercodeClient {
1541
1539
  this.status = 'disconnected';
1542
1540
  this.userAudioAmplitude = 0;
1543
1541
  this.agentAudioAmplitude = 0;
1544
- this.assistantIsSpeaking = false;
1545
- this.assistantHasBeenInterrupted = false;
1546
1542
  this.sessionId = options.sessionId || null;
1547
1543
  this.pushToTalkActive = false;
1548
- // this.currentTurnId = null; // TODO implement
1549
- // this.lastDeltaIdPlayed = null; // TODO implement
1550
1544
  // Bind event handlers
1551
1545
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
1552
1546
  this._handleDataAvailable = this._handleDataAvailable.bind(this);
@@ -1565,48 +1559,21 @@ class LayercodeClient {
1565
1559
  * @private
1566
1560
  */
1567
1561
  _clientResponseAudioReplayFinished() {
1568
- this.assistantIsSpeaking = false;
1569
- this._wsSend({
1570
- type: 'trigger.response.audio.replay_finished',
1571
- reason: 'completed',
1572
- // last_delta_id_played: this.lastDeltaIdPlayed, // TODO implement
1573
- // turn_id: this.currentTurnId, // TODO implement
1574
- });
1575
- }
1576
- async _setupWavPlayer() {
1577
- this.wavPlayer = new WavStreamPlayer({
1578
- finishedPlayingCallback: this._clientResponseAudioReplayFinished.bind(this),
1579
- sampleRate: 16000, // TODO should be set my fetched pipeline config
1580
- });
1581
- await this.wavPlayer.connect();
1582
- // Set up amplitude monitoring only if callbacks are provided
1583
- if (this.options.onAgentAmplitudeChange !== (() => { })) {
1584
- let agentUpdateCounter = 0;
1585
- this.wavPlayer.startAmplitudeMonitoring((amplitude) => {
1586
- if (agentUpdateCounter == this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
1587
- this.agentAudioAmplitude = amplitude;
1588
- this.options.onAgentAmplitudeChange(amplitude);
1589
- agentUpdateCounter = 0; // Reset after each sample
1590
- }
1591
- agentUpdateCounter++;
1592
- });
1593
- }
1562
+ console.log('clientResponseAudioReplayFinished');
1563
+ // NOTE: Not currently used in voice pipelines
1564
+ // this._wsSend({
1565
+ // type: 'trigger.response.audio.replay_finished',
1566
+ // reason: 'completed',
1567
+ // });
1594
1568
  }
1595
1569
  async _clientInterruptAssistantReplay() {
1596
- if (this.assistantIsSpeaking) {
1597
- await this.wavPlayer.interrupt();
1598
- console.log('interrupting assistant replay');
1599
- console.log('setting assistantIsSpeaking to false');
1600
- console.log('setting assistantHasBeenInterrupted to true');
1601
- this.assistantIsSpeaking = false;
1602
- this.assistantHasBeenInterrupted = true;
1603
- this._wsSend({
1604
- type: 'trigger.response.audio.replay_finished',
1605
- reason: 'interrupted',
1606
- // last_delta_id_played: this.lastDeltaIdPlayed, // TODO implement
1607
- // turn_id: this.currentTurnId, // TODO implement
1608
- });
1609
- }
1570
+ await this.wavPlayer.interrupt();
1571
+ // TODO: Use in voice pipeline to know how much of the audio has been played and how much to truncate transcript
1572
+ // this._wsSend({
1573
+ // type: 'trigger.response.audio.replay_finished',
1574
+ // reason: 'interrupted',
1575
+ // delta_id: 'TODO'
1576
+ // });
1610
1577
  }
1611
1578
  async triggerUserTurnStarted() {
1612
1579
  if (!this.pushToTalkActive) {
@@ -1634,33 +1601,20 @@ class LayercodeClient {
1634
1601
  }
1635
1602
  switch (message.type) {
1636
1603
  case 'turn.start':
1637
- // Sent from the server to this client when a new user turn is detected. The client should interrupt any playing assistant audio and send a "trigger.response.audio.replay_finished" event to the server.
1638
- if (message.role === 'user') {
1639
- // Interrupt any playing assistant audio
1640
- // await this._clientInterruptAssistantReplay(); // TODO work whether to call interrupt or not here
1604
+ // Sent from the server to this client when a new user turn is detected
1605
+ console.log('received turn.start from server');
1606
+ if (message.role === 'user' && !this.pushToTalkActive) {
1607
+ // Interrupt any playing assistant audio if this is a turn trigged by the server (and not push to talk, which will have already called interrupt)
1608
+ console.log('interrupting assistant audio, as user turn has started and pushToTalkActive is false');
1609
+ await this._clientInterruptAssistantReplay();
1641
1610
  }
1642
1611
  break;
1643
1612
  case 'response.audio':
1644
- // console.log("received response.audio");
1645
- if (!this.assistantHasBeenInterrupted) {
1646
- // If the assistant has been interrupted, ignore the rest of the audio chunks for this response (turn)
1647
- // TODO: scope audio chunks we ignore based on the turn_id
1648
- if (!this.assistantIsSpeaking) {
1649
- // If we have switch from assistant not speaking to now speaking, this is the start of the assistant's response
1650
- this.assistantIsSpeaking = true;
1651
- console.log('assistantIsSpeaking is currently false, received audio chunk, so setting to true');
1652
- }
1653
- const audioBuffer = base64ToArrayBuffer(message.content);
1654
- this.wavPlayer.add16BitPCM(audioBuffer, 'default');
1655
- }
1656
- else {
1657
- console.log('ignoring response.audio because assistant has been interrupted');
1658
- }
1613
+ const audioBuffer = base64ToArrayBuffer(message.content);
1614
+ this.wavPlayer.add16BitPCM(audioBuffer, message.turn_id);
1659
1615
  break;
1660
1616
  case 'response.end':
1661
1617
  console.log('received response.end');
1662
- console.log('setting assistantHasBeenInterrupted to false');
1663
- this.assistantHasBeenInterrupted = false;
1664
1618
  break;
1665
1619
  case 'response.data':
1666
1620
  console.log('received response.data', message);
@@ -1700,6 +1654,32 @@ class LayercodeClient {
1700
1654
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
1701
1655
  this.ws.send(messageString);
1702
1656
  }
1657
+ else {
1658
+ console.error('WebSocket is not open. Did not send message:', messageString);
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Sets up amplitude monitoring for a given audio source.
1663
+ * @param {WavRecorder | WavStreamPlayer} source - The audio source (recorder or player).
1664
+ * @param {(amplitude: number) => void} callback - The callback function to invoke on amplitude change.
1665
+ * @param {(amplitude: number) => void} updateInternalState - Function to update the internal amplitude state.
1666
+ * @private
1667
+ */
1668
+ _setupAmplitudeMonitoring(source, callback, updateInternalState) {
1669
+ // Set up amplitude monitoring only if a callback is provided
1670
+ // Check against the default no-op function defined in the constructor options
1671
+ if (callback !== (() => { })) {
1672
+ let updateCounter = 0;
1673
+ source.startAmplitudeMonitoring((amplitude) => {
1674
+ // Only update and call callback at the specified sample rate
1675
+ if (updateCounter >= this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
1676
+ updateInternalState(amplitude);
1677
+ callback(amplitude);
1678
+ updateCounter = 0; // Reset counter after sampling
1679
+ }
1680
+ updateCounter++;
1681
+ });
1682
+ }
1703
1683
  }
1704
1684
  /**
1705
1685
  * Connects to the Layercode pipeline and starts the audio session
@@ -1734,6 +1714,8 @@ class LayercodeClient {
1734
1714
  this.ws = new WebSocket(`${this._websocketUrl}?${new URLSearchParams({
1735
1715
  client_session_key: authorizeSessionResponseBody.client_session_key,
1736
1716
  })}`);
1717
+ // Bind the websocket message callbacks
1718
+ this.ws.onmessage = this._handleWebSocketMessage;
1737
1719
  this.ws.onopen = () => {
1738
1720
  console.log('WebSocket connection established');
1739
1721
  this._setStatus('connected');
@@ -1749,26 +1731,15 @@ class LayercodeClient {
1749
1731
  this._setStatus('error');
1750
1732
  this.options.onError(new Error('WebSocket connection error'));
1751
1733
  };
1752
- this.ws.onmessage = this._handleWebSocketMessage;
1753
- // Initialize audio
1734
+ // Initialize microphone audio capture
1754
1735
  await this.wavRecorder.begin();
1755
1736
  await this.wavRecorder.record(this._handleDataAvailable);
1756
- if (this.options.onUserAmplitudeChange !== (() => { })) {
1757
- let userUpdateCounter = 0;
1758
- this.wavRecorder.startAmplitudeMonitoring((amplitude) => {
1759
- if (userUpdateCounter == this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
1760
- this.userAudioAmplitude = amplitude;
1761
- this.options.onUserAmplitudeChange(amplitude);
1762
- userUpdateCounter = 0; // Reset after each sample
1763
- }
1764
- userUpdateCounter++;
1765
- });
1766
- }
1767
- await this._setupWavPlayer();
1768
- // Handle page unload
1769
- window.addEventListener('beforeunload', () => {
1770
- this.disconnect();
1771
- });
1737
+ // Set up microphone amplitude monitoring
1738
+ this._setupAmplitudeMonitoring(this.wavRecorder, this.options.onUserAmplitudeChange, (amp) => (this.userAudioAmplitude = amp));
1739
+ // Initialize audio player
1740
+ await this.wavPlayer.connect();
1741
+ // Set up audio player amplitude monitoring
1742
+ this._setupAmplitudeMonitoring(this.wavPlayer, this.options.onAgentAmplitudeChange, (amp) => (this.agentAudioAmplitude = amp));
1772
1743
  }
1773
1744
  catch (error) {
1774
1745
  console.error('Error connecting to Layercode pipeline:', error);
@@ -1777,47 +1748,6 @@ class LayercodeClient {
1777
1748
  throw error;
1778
1749
  }
1779
1750
  }
1780
- /**
1781
- * Disconnects from the Layercode pipeline and stops audio recording
1782
- */
1783
- disconnect() {
1784
- console.log('disconnecting');
1785
- if (this.ws) {
1786
- this.ws.close();
1787
- this.ws = null;
1788
- }
1789
- // Stop recording user microphone audio
1790
- this.wavRecorder.stop();
1791
- // Handle wavPlayer cleanup without calling disconnect directly
1792
- if (this.wavPlayer) {
1793
- // Use type assertion to access internal properties
1794
- const player = this.wavPlayer;
1795
- // Clean up any audio resources manually
1796
- if (player.stream) {
1797
- player.stream.disconnect();
1798
- player.stream = null;
1799
- }
1800
- if (player.analyser) {
1801
- player.analyser.disconnect();
1802
- }
1803
- if (player.context) {
1804
- player.context.close().catch((err) => console.error('Error closing audio context:', err));
1805
- }
1806
- }
1807
- this._setStatus('disconnected');
1808
- }
1809
- /**
1810
- * Mutes or unmutes the microphone
1811
- * @param {boolean} mute - Whether to mute the microphone
1812
- */
1813
- setMuteMic(mute) {
1814
- if (mute) {
1815
- this.wavRecorder.mute();
1816
- }
1817
- else {
1818
- this.wavRecorder.unmute();
1819
- }
1820
- }
1821
1751
  }
1822
1752
 
1823
1753
  export { LayercodeClient as default };