@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
|
-
|
|
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,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
|
|
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
|
-
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
|
-
|
|
1760
|
-
// Initialize audio
|
|
1740
|
+
// Initialize microphone audio capture
|
|
1761
1741
|
await this.wavRecorder.begin();
|
|
1762
1742
|
await this.wavRecorder.record(this._handleDataAvailable);
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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;
|