@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
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
//
|
|
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
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
1759
|
-
// Initialize audio
|
|
1740
|
+
// Initialize microphone audio capture
|
|
1760
1741
|
await this.wavRecorder.begin();
|
|
1761
1742
|
await this.wavRecorder.record(this._handleDataAvailable);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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;
|