@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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
//
|
|
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
|
-
|
|
1645
|
-
|
|
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
|
-
|
|
1753
|
-
// Initialize audio
|
|
1734
|
+
// Initialize microphone audio capture
|
|
1754
1735
|
await this.wavRecorder.begin();
|
|
1755
1736
|
await this.wavRecorder.record(this._handleDataAvailable);
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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 };
|