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