@crestapps/ai-chat-ui 2.0.0-preview.199 → 2.0.0-preview.203
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.
package/dist/realtime-audio.js
CHANGED
|
@@ -798,6 +798,10 @@
|
|
|
798
798
|
realtimeMicDeviceId = '',
|
|
799
799
|
realtimeOutputDeviceId = '',
|
|
800
800
|
realtimeLanguage = '',
|
|
801
|
+
// The listener's saved speed (null for the normal pace), and what each session reports about its own.
|
|
802
|
+
realtimeSpeechSpeed = null,
|
|
803
|
+
realtimeSessionSpeechSpeed = 1,
|
|
804
|
+
realtimeSpeechSpeedAdjustable = false,
|
|
801
805
|
realtimeAudioSettingsBuilt = false,
|
|
802
806
|
// The settings popover this instance built. A page can host two chat clients at once -- its own chat
|
|
803
807
|
// and the admin widget -- and both popovers carry the same element id, so looking one up by id would
|
|
@@ -814,6 +818,14 @@
|
|
|
814
818
|
// name merely looked like a room microphone, and the earlier repair deliberately left that alone, so users
|
|
815
819
|
// found interruptions "off by default" with no idea why.
|
|
816
820
|
var REALTIME_PREFS_VERSION = 3;
|
|
821
|
+
|
|
822
|
+
// Mirrors RealtimeSpeechSpeedRange on the server.
|
|
823
|
+
var REALTIME_SPEECH_SPEED_MIN = 0.75,
|
|
824
|
+
REALTIME_SPEECH_SPEED_MAX = 1.5,
|
|
825
|
+
REALTIME_SPEECH_SPEED_DEFAULT = 1;
|
|
826
|
+
function clampRealtimeSpeechSpeed(speed) {
|
|
827
|
+
return Math.round(Math.min(REALTIME_SPEECH_SPEED_MAX, Math.max(REALTIME_SPEECH_SPEED_MIN, speed)) * 100) / 100;
|
|
828
|
+
}
|
|
817
829
|
function loadRealtimeAudioPrefs() {
|
|
818
830
|
var prefs = {
|
|
819
831
|
bargeIn: true,
|
|
@@ -822,6 +834,7 @@
|
|
|
822
834
|
micDeviceId: '',
|
|
823
835
|
outputDeviceId: '',
|
|
824
836
|
language: '',
|
|
837
|
+
speechSpeed: null,
|
|
825
838
|
version: 0
|
|
826
839
|
};
|
|
827
840
|
try {
|
|
@@ -846,6 +859,9 @@
|
|
|
846
859
|
if (typeof parsed.language === 'string') {
|
|
847
860
|
prefs.language = parsed.language;
|
|
848
861
|
}
|
|
862
|
+
if (typeof parsed.speechSpeed === 'number' && isFinite(parsed.speechSpeed)) {
|
|
863
|
+
prefs.speechSpeed = clampRealtimeSpeechSpeed(parsed.speechSpeed);
|
|
864
|
+
}
|
|
849
865
|
if (typeof parsed.version === 'number') {
|
|
850
866
|
prefs.version = parsed.version;
|
|
851
867
|
}
|
|
@@ -875,6 +891,7 @@
|
|
|
875
891
|
realtimeMicDeviceId = prefs.micDeviceId || '';
|
|
876
892
|
realtimeOutputDeviceId = prefs.outputDeviceId || '';
|
|
877
893
|
realtimeLanguage = prefs.language || '';
|
|
894
|
+
realtimeSpeechSpeed = typeof prefs.speechSpeed === 'number' ? prefs.speechSpeed : null;
|
|
878
895
|
// The gate runs on the audio thread and cannot see these variables; push the change to it.
|
|
879
896
|
syncRealtimeGateMode();
|
|
880
897
|
if (realtimeGain) {
|
|
@@ -1021,7 +1038,7 @@
|
|
|
1021
1038
|
gear.className = 'btn btn-outline-secondary';
|
|
1022
1039
|
gear.title = localize('settingsTitle', 'Voice settings');
|
|
1023
1040
|
gear.setAttribute('aria-label', localize('settingsTitle', 'Voice settings'));
|
|
1024
|
-
gear.innerHTML = '<i class="fa-solid fa-
|
|
1041
|
+
gear.innerHTML = '<i class="fa-solid fa-headset"></i>';
|
|
1025
1042
|
var langs = [['en', 'English'], ['es', 'Spanish'], ['fr', 'French'], ['de', 'German'], ['it', 'Italian'], ['pt', 'Portuguese'], ['nl', 'Dutch'], ['zh', 'Chinese'], ['ja', 'Japanese'], ['ko', 'Korean'], ['ar', 'Arabic'], ['hi', 'Hindi'], ['ru', 'Russian']];
|
|
1026
1043
|
var langOptions = '<option value=""' + (prefs.language === '' ? ' selected' : '') + '>' + localize('languageAuto', 'Automatic') + '</option>' + langs.map(function (l) {
|
|
1027
1044
|
return '<option value="' + l[0] + '"' + (prefs.language === l[0] ? ' selected' : '') + '>' + l[1] + '</option>';
|
|
@@ -1029,7 +1046,9 @@
|
|
|
1029
1046
|
var panel = document.createElement('div');
|
|
1030
1047
|
panel.className = 'card shadow-sm';
|
|
1031
1048
|
panel.style.cssText = 'position:absolute;bottom:calc(100% + 6px);right:0;z-index:1080;width:290px;max-height:70vh;overflow:auto;display:none;';
|
|
1032
|
-
panel.innerHTML = '<div class="card-body p-3">' + '<div class="fw-semibold mb-2" style="font-size:0.85rem;">' + localize('settingsTitle', 'Voice settings') + '</div>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('microphone', 'Microphone') + '</label>' + '<select class="form-select form-select-sm js-mic mb-2"><option value="">' + localize('defaultMicrophone', 'Default microphone') + '</option></select>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('speaker', 'Speaker') + '</label>' + '<select class="form-select form-select-sm js-out mb-1"><option value="">' + localize('automatic', 'Automatic') + '</option></select>' + '<button type="button" class="btn btn-outline-secondary btn-sm mb-2 js-pick-out" style="display:none;">' + localize('chooseSpeaker', 'Choose speaker…') + '</button>' + '<div class="form-text mb-2 js-out-help">' + localize('speakerHelp', 'Automatic uses the device your system uses for calls. If you cannot hear the assistant, pick the speakers you are actually listening to.') + '</div>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('assistantVolume', 'Assistant volume') + ': <strong class="js-vol-val">' + Math.round(prefs.volume * 100) + '</strong>%</label>' + '<input type="range" class="form-range js-vol mb-2" min="0" max="100" step="5" value="' + Math.round(prefs.volume * 100) + '">' +
|
|
1049
|
+
panel.innerHTML = '<div class="card-body p-3">' + '<div class="fw-semibold mb-2" style="font-size:0.85rem;">' + localize('settingsTitle', 'Voice settings') + '</div>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('microphone', 'Microphone') + '</label>' + '<select class="form-select form-select-sm js-mic mb-2"><option value="">' + localize('defaultMicrophone', 'Default microphone') + '</option></select>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('speaker', 'Speaker') + '</label>' + '<select class="form-select form-select-sm js-out mb-1"><option value="">' + localize('automatic', 'Automatic') + '</option></select>' + '<button type="button" class="btn btn-outline-secondary btn-sm mb-2 js-pick-out" style="display:none;">' + localize('chooseSpeaker', 'Choose speaker…') + '</button>' + '<div class="form-text mb-2 js-out-help">' + localize('speakerHelp', 'Automatic uses the device your system uses for calls. If you cannot hear the assistant, pick the speakers you are actually listening to.') + '</div>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('assistantVolume', 'Assistant volume') + ': <strong class="js-vol-val">' + Math.round(prefs.volume * 100) + '</strong>%</label>' + '<input type="range" class="form-range js-vol mb-2" min="0" max="100" step="5" value="' + Math.round(prefs.volume * 100) + '">' +
|
|
1050
|
+
// Hidden until the session reports that its speed can change.
|
|
1051
|
+
'<div class="js-speed-row" hidden>' + '<label class="form-label mb-1 d-flex align-items-baseline" style="font-size:0.8rem;">' + '<span>' + localize('speechSpeed', 'Speaking speed') + ': <strong class="js-speed-val">1.00</strong>×</span>' + '<button type="button" class="btn btn-link btn-sm p-0 ms-auto js-speed-reset" style="font-size:0.8rem;" hidden>' + localize('speechSpeedReset', 'Reset') + '</button>' + '</label>' + '<input type="range" class="form-range js-speed mb-0" min="' + REALTIME_SPEECH_SPEED_MIN + '" max="' + REALTIME_SPEECH_SPEED_MAX + '" step="0.05" value="' + REALTIME_SPEECH_SPEED_DEFAULT + '">' + '<div class="form-text mt-0 mb-2">' + localize('speechSpeedHelp', 'Applies from the assistant\'s next reply.') + '</div>' + '</div>' + '<label class="form-label mb-1 d-block" style="font-size:0.8rem;">' + localize('language', 'Language') + '</label>' + '<select class="form-select form-select-sm js-lang mb-2">' + langOptions + '</select>' + '<div class="form-check form-switch mb-1">' + '<input class="form-check-input js-barge" type="checkbox" id="realtime-setting-barge"' + (prefs.bargeIn ? ' checked' : '') + '>' + '<label class="form-check-label" for="realtime-setting-barge">' + localize('allowInterruptions', 'Allow interruptions') + '</label>' + '</div>' + '<div class="form-text mb-2">' + localize('allowInterruptionsHelp', 'Talk over the assistant to interrupt it. Turn this off if the assistant keeps hearing itself through your speakers.') + '</div>' + '<div class="form-check form-switch mb-1">' + '<input class="form-check-input js-ptt" type="checkbox" id="realtime-setting-ptt"' + (prefs.pushToTalk ? ' checked' : '') + '>' + '<label class="form-check-label" for="realtime-setting-ptt">' + localize('pushToTalk', 'Push-to-talk') + '</label>' + '</div>' + '<div class="form-text">' + localize('pushToTalkHelp', 'Hold <kbd>Space</kbd> (or the button) to talk. For very noisy places.') + '</div>' + '</div>';
|
|
1033
1052
|
wrap.appendChild(gear);
|
|
1034
1053
|
wrap.appendChild(panel);
|
|
1035
1054
|
realtimeBtn.insertAdjacentElement('afterend', wrap);
|
|
@@ -1042,20 +1061,35 @@
|
|
|
1042
1061
|
var outSelect = panel.querySelector('.js-out');
|
|
1043
1062
|
var outPickButton = panel.querySelector('.js-pick-out');
|
|
1044
1063
|
var langSelect = panel.querySelector('.js-lang');
|
|
1045
|
-
|
|
1046
|
-
|
|
1064
|
+
var speedInput = panel.querySelector('.js-speed');
|
|
1065
|
+
var speedVal = panel.querySelector('.js-speed-val');
|
|
1066
|
+
var speedReset = panel.querySelector('.js-speed-reset');
|
|
1067
|
+
function readPanelPrefs() {
|
|
1068
|
+
return {
|
|
1047
1069
|
bargeIn: bargeInput.checked,
|
|
1048
1070
|
pushToTalk: pttInput.checked,
|
|
1049
1071
|
volume: (parseInt(volInput.value, 10) || 0) / 100,
|
|
1050
1072
|
micDeviceId: micSelect.value || '',
|
|
1051
1073
|
outputDeviceId: outSelect.value || '',
|
|
1052
1074
|
language: langSelect.value || '',
|
|
1075
|
+
speechSpeed: realtimeSpeechSpeed,
|
|
1053
1076
|
version: REALTIME_PREFS_VERSION
|
|
1054
1077
|
};
|
|
1078
|
+
}
|
|
1079
|
+
function persist() {
|
|
1080
|
+
var next = readPanelPrefs();
|
|
1055
1081
|
applyRealtimeAudioPrefs(next);
|
|
1056
1082
|
saveRealtimeAudioPrefs(next);
|
|
1057
1083
|
pushRealtimeSettingsToServer();
|
|
1058
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
// Kept out of persist(), which resends turn detection on every volume tick.
|
|
1087
|
+
function persistSpeechSpeed(speed) {
|
|
1088
|
+
realtimeSpeechSpeed = speed;
|
|
1089
|
+
saveRealtimeAudioPrefs(readPanelPrefs());
|
|
1090
|
+
syncRealtimeSpeechSpeedControl();
|
|
1091
|
+
pushRealtimeSpeechSpeedToServer();
|
|
1092
|
+
}
|
|
1059
1093
|
function populateDevices() {
|
|
1060
1094
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
|
1061
1095
|
return;
|
|
@@ -1154,6 +1188,17 @@
|
|
|
1154
1188
|
volVal.textContent = volInput.value;
|
|
1155
1189
|
persist();
|
|
1156
1190
|
});
|
|
1191
|
+
// The label follows the drag; the speed is sent once, on release.
|
|
1192
|
+
speedInput.addEventListener('input', function () {
|
|
1193
|
+
speedVal.textContent = formatRealtimeSpeechSpeed(parseFloat(speedInput.value));
|
|
1194
|
+
});
|
|
1195
|
+
speedInput.addEventListener('change', function () {
|
|
1196
|
+
persistSpeechSpeed(clampRealtimeSpeechSpeed(parseFloat(speedInput.value)));
|
|
1197
|
+
});
|
|
1198
|
+
// Back to the model's normal pace.
|
|
1199
|
+
speedReset.addEventListener('click', function () {
|
|
1200
|
+
persistSpeechSpeed(null);
|
|
1201
|
+
});
|
|
1157
1202
|
micSelect.addEventListener('change', persist);
|
|
1158
1203
|
langSelect.addEventListener('change', persist);
|
|
1159
1204
|
outSelect.addEventListener('change', function () {
|
|
@@ -1294,6 +1339,50 @@
|
|
|
1294
1339
|
connection.send('UpdateRealtimeSettings', realtimeBargeIn, null, null);
|
|
1295
1340
|
} catch (err) {}
|
|
1296
1341
|
}
|
|
1342
|
+
function formatRealtimeSpeechSpeed(speed) {
|
|
1343
|
+
return (isFinite(speed) ? speed : REALTIME_SPEECH_SPEED_DEFAULT).toFixed(2);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// The listener's saved speed wins over the session's own.
|
|
1347
|
+
function effectiveRealtimeSpeechSpeed() {
|
|
1348
|
+
return realtimeSpeechSpeed !== null ? realtimeSpeechSpeed : realtimeSessionSpeechSpeed;
|
|
1349
|
+
}
|
|
1350
|
+
function syncRealtimeSpeechSpeedControl() {
|
|
1351
|
+
if (!realtimeAudioSettingsElement) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
var row = realtimeAudioSettingsElement.querySelector('.js-speed-row');
|
|
1355
|
+
var input = realtimeAudioSettingsElement.querySelector('.js-speed');
|
|
1356
|
+
var value = realtimeAudioSettingsElement.querySelector('.js-speed-val');
|
|
1357
|
+
var reset = realtimeAudioSettingsElement.querySelector('.js-speed-reset');
|
|
1358
|
+
if (!row || !input || !value || !reset) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
var speed = effectiveRealtimeSpeechSpeed();
|
|
1362
|
+
row.hidden = !realtimeSpeechSpeedAdjustable;
|
|
1363
|
+
input.value = String(speed);
|
|
1364
|
+
value.textContent = formatRealtimeSpeechSpeed(speed);
|
|
1365
|
+
reset.hidden = realtimeSpeechSpeed === null;
|
|
1366
|
+
}
|
|
1367
|
+
function pushRealtimeSpeechSpeedToServer() {
|
|
1368
|
+
if (!isRealtimeActive || !realtimeSpeechSpeedAdjustable || !connection || typeof connection.send !== 'function') {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
connection.send('UpdateRealtimeSpeechSpeed', effectiveRealtimeSpeechSpeed());
|
|
1373
|
+
} catch (err) {}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// An empty payload means this session's speed can't change; otherwise a saved speed is applied before anyone speaks.
|
|
1377
|
+
function applySessionSpeechSpeed(payload) {
|
|
1378
|
+
var speed = parseFloat(payload);
|
|
1379
|
+
realtimeSpeechSpeedAdjustable = isFinite(speed);
|
|
1380
|
+
realtimeSessionSpeechSpeed = realtimeSpeechSpeedAdjustable ? speed : REALTIME_SPEECH_SPEED_DEFAULT;
|
|
1381
|
+
syncRealtimeSpeechSpeedControl();
|
|
1382
|
+
if (realtimeSpeechSpeed !== null && realtimeSpeechSpeed !== realtimeSessionSpeechSpeed) {
|
|
1383
|
+
pushRealtimeSpeechSpeedToServer();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1297
1386
|
|
|
1298
1387
|
// Reports the session's state to the host so it can show something more honest than a button that flips to
|
|
1299
1388
|
// "End Conversation" before anything has connected.
|
|
@@ -1337,7 +1426,9 @@
|
|
|
1337
1426
|
if (!isRealtimeActive) {
|
|
1338
1427
|
return;
|
|
1339
1428
|
}
|
|
1340
|
-
if (type === '
|
|
1429
|
+
if (type === 'speech_speed') {
|
|
1430
|
+
applySessionSpeechSpeed(payload);
|
|
1431
|
+
} else if (type === 'session_ready') {
|
|
1341
1432
|
realtimeSessionReady = true;
|
|
1342
1433
|
setRealtimeState('listening');
|
|
1343
1434
|
} else if (type === 'session_ended') {
|
|
@@ -1565,6 +1656,9 @@
|
|
|
1565
1656
|
var attempt = ++realtimeAttemptToken;
|
|
1566
1657
|
realtimeFellBack = false;
|
|
1567
1658
|
realtimeSessionReady = false;
|
|
1659
|
+
// Hidden until this session reports its own speed.
|
|
1660
|
+
realtimeSpeechSpeedAdjustable = false;
|
|
1661
|
+
syncRealtimeSpeechSpeedControl();
|
|
1568
1662
|
realtimeEndedNotice = null;
|
|
1569
1663
|
clearRealtimeStatus();
|
|
1570
1664
|
bindRealtimeLifecycleHandlers();
|