@layercode/js-sdk 1.0.5 → 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,37 +1607,23 @@ 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
- console.log('adding audio chunk to wavPlayer');
1660
- console.log('message.content length:', message.content.length);
1661
- const audioBuffer = base64ToArrayBuffer(message.content);
1662
- this.wavPlayer.add16BitPCM(audioBuffer, 'default');
1663
- }
1664
- else {
1665
- console.log('ignoring response.audio because assistant has been interrupted');
1666
- }
1619
+ const audioBuffer = base64ToArrayBuffer(message.content);
1620
+ this.wavPlayer.add16BitPCM(audioBuffer, message.turn_id);
1667
1621
  break;
1668
1622
  case 'response.end':
1669
1623
  console.log('received response.end');
1670
- console.log('setting assistantHasBeenInterrupted to false');
1671
- this.assistantHasBeenInterrupted = false;
1672
1624
  break;
1673
1625
  case 'response.data':
1626
+ console.log('received response.data', message);
1674
1627
  this.options.onDataMessage(message);
1675
1628
  break;
1676
1629
  default:
@@ -1707,6 +1660,32 @@ registerProcessor('audio_processor', AudioProcessor);
1707
1660
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
1708
1661
  this.ws.send(messageString);
1709
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
+ }
1710
1689
  }
1711
1690
  /**
1712
1691
  * Connects to the Layercode pipeline and starts the audio session
@@ -1741,6 +1720,8 @@ registerProcessor('audio_processor', AudioProcessor);
1741
1720
  this.ws = new WebSocket(`${this._websocketUrl}?${new URLSearchParams({
1742
1721
  client_session_key: authorizeSessionResponseBody.client_session_key,
1743
1722
  })}`);
1723
+ // Bind the websocket message callbacks
1724
+ this.ws.onmessage = this._handleWebSocketMessage;
1744
1725
  this.ws.onopen = () => {
1745
1726
  console.log('WebSocket connection established');
1746
1727
  this._setStatus('connected');
@@ -1756,26 +1737,15 @@ registerProcessor('audio_processor', AudioProcessor);
1756
1737
  this._setStatus('error');
1757
1738
  this.options.onError(new Error('WebSocket connection error'));
1758
1739
  };
1759
- this.ws.onmessage = this._handleWebSocketMessage;
1760
- // Initialize audio
1740
+ // Initialize microphone audio capture
1761
1741
  await this.wavRecorder.begin();
1762
1742
  await this.wavRecorder.record(this._handleDataAvailable);
1763
- if (this.options.onUserAmplitudeChange !== (() => { })) {
1764
- let userUpdateCounter = 0;
1765
- this.wavRecorder.startAmplitudeMonitoring((amplitude) => {
1766
- if (userUpdateCounter == this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
1767
- this.userAudioAmplitude = amplitude;
1768
- this.options.onUserAmplitudeChange(amplitude);
1769
- userUpdateCounter = 0; // Reset after each sample
1770
- }
1771
- userUpdateCounter++;
1772
- });
1773
- }
1774
- await this._setupWavPlayer();
1775
- // Handle page unload
1776
- window.addEventListener('beforeunload', () => {
1777
- this.disconnect();
1778
- });
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));
1779
1749
  }
1780
1750
  catch (error) {
1781
1751
  console.error('Error connecting to Layercode pipeline:', error);
@@ -1784,47 +1754,6 @@ registerProcessor('audio_processor', AudioProcessor);
1784
1754
  throw error;
1785
1755
  }
1786
1756
  }
1787
- /**
1788
- * Disconnects from the Layercode pipeline and stops audio recording
1789
- */
1790
- disconnect() {
1791
- console.log('disconnecting');
1792
- if (this.ws) {
1793
- this.ws.close();
1794
- this.ws = null;
1795
- }
1796
- // Stop recording user microphone audio
1797
- this.wavRecorder.stop();
1798
- // Handle wavPlayer cleanup without calling disconnect directly
1799
- if (this.wavPlayer) {
1800
- // Use type assertion to access internal properties
1801
- const player = this.wavPlayer;
1802
- // Clean up any audio resources manually
1803
- if (player.stream) {
1804
- player.stream.disconnect();
1805
- player.stream = null;
1806
- }
1807
- if (player.analyser) {
1808
- player.analyser.disconnect();
1809
- }
1810
- if (player.context) {
1811
- player.context.close().catch((err) => console.error('Error closing audio context:', err));
1812
- }
1813
- }
1814
- this._setStatus('disconnected');
1815
- }
1816
- /**
1817
- * Mutes or unmutes the microphone
1818
- * @param {boolean} mute - Whether to mute the microphone
1819
- */
1820
- setMuteMic(mute) {
1821
- if (mute) {
1822
- this.wavRecorder.mute();
1823
- }
1824
- else {
1825
- this.wavRecorder.unmute();
1826
- }
1827
- }
1828
1757
  }
1829
1758
 
1830
1759
  return LayercodeClient;