sillytavern 1.11.3 → 1.11.4

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/package.json CHANGED
@@ -55,11 +55,10 @@
55
55
  "type": "git",
56
56
  "url": "https://github.com/SillyTavern/SillyTavern.git"
57
57
  },
58
- "version": "1.11.3",
58
+ "version": "1.11.4",
59
59
  "scripts": {
60
60
  "start": "node server.js",
61
61
  "start-multi": "node server.js --disableCsrf",
62
- "pkg": "pkg --compress Gzip --no-bytecode --public .",
63
62
  "postinstall": "node post-install.js",
64
63
  "lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
65
64
  "lint-fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix"
@@ -72,24 +71,8 @@
72
71
  "no-var": "off"
73
72
  },
74
73
  "main": "server.js",
75
- "pkg": {
76
- "targets": [
77
- "node18-linux-x64",
78
- "node18-macos-x64",
79
- "node18-windows-x64"
80
- ],
81
- "assets": [
82
- "node_modules/**/*"
83
- ],
84
- "outputPath": "dist",
85
- "scripts": [
86
- "server.js"
87
- ]
88
- },
89
74
  "devDependencies": {
90
75
  "eslint": "^8.55.0",
91
- "jquery": "^3.6.4",
92
- "pkg": "^5.8.1",
93
- "pkg-fetch": "^3.5.2"
76
+ "jquery": "^3.6.4"
94
77
  }
95
78
  }
package/public/index.html CHANGED
@@ -1277,7 +1277,7 @@
1277
1277
  <div data-newbie-hidden data-tg-type="ooba, koboldcpp, aphrodite, tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
1278
1278
  <small data-i18n="Smoothing Factor">Smoothing Factor</small>
1279
1279
  <input class="neo-range-slider" type="range" id="smoothing_factor_textgenerationwebui" name="volume" min="0" max="10" step="0.01" />
1280
- <input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="smoothing_factor_textgenerationwebui" id="smoothing_factor_counter_textgenerationwebui">
1280
+ <input class="neo-range-input" type="number" min="0" max="10" step="0.01" data-for="smoothing_factor_textgenerationwebui" id="smoothing_factor_counter_textgenerationwebui">
1281
1281
  </div>
1282
1282
  <!--
1283
1283
  <div data-tg-type="aphrodite" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" data-i18n="Responses">
@@ -1916,6 +1916,15 @@
1916
1916
  Make sure you run it with <code>--api</code> flag
1917
1917
  </span>
1918
1918
  </div>
1919
+ <h4 data-i18n="API key (optional)">API key (optional)</h4>
1920
+ <div class="flex-container">
1921
+ <input id="api_key_ooba" name="api_key_ooba" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off">
1922
+ <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_ooba">
1923
+ </div>
1924
+ </div>
1925
+ <div data-for="api_key_ooba" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
1926
+ For privacy reasons, your API key will be hidden after you reload the page.
1927
+ </div>
1919
1928
  <div class="flex1">
1920
1929
  <h4 data-i18n="Server url">Server URL</h4>
1921
1930
  <small data-i18n="Example: http://127.0.0.1:5000 ">Example: http://127.0.0.1:5000</small>
package/public/script.js CHANGED
@@ -2179,6 +2179,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh
2179
2179
  environment.user = _name1 ?? name1;
2180
2180
  environment.char = _name2 ?? name2;
2181
2181
  environment.group = environment.charIfNotGroup = _group ?? name2;
2182
+ environment.model = getGeneratingModel();
2182
2183
 
2183
2184
  return evaluateMacros(content, environment);
2184
2185
  }
@@ -5420,17 +5421,21 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
5420
5421
  if (response.ok) {
5421
5422
  const allEntities = await response.json();
5422
5423
 
5423
- if (!doRender) {
5424
- return allEntities;
5424
+ if (!Array.isArray(allEntities)) {
5425
+ return [];
5425
5426
  }
5426
5427
 
5427
- const entities = personasFilter.applyFilters(allEntities);
5428
- entities.sort((a, b) => {
5429
- const aName = String(power_user.personas[a]);
5430
- const bName = String(power_user.personas[b]);
5428
+ allEntities.sort((a, b) => {
5429
+ const aName = String(power_user.personas[a] || a);
5430
+ const bName = String(power_user.personas[b] || b);
5431
5431
  return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
5432
5432
  });
5433
5433
 
5434
+ if (!doRender) {
5435
+ return allEntities;
5436
+ }
5437
+
5438
+ const entities = personasFilter.applyFilters(allEntities);
5434
5439
  const storageKey = 'Personas_PerPage';
5435
5440
  const listId = '#user_avatar_block';
5436
5441
  const perPage = Number(localStorage.getItem(storageKey)) || 5;
@@ -5490,6 +5495,7 @@ function highlightSelectedAvatar() {
5490
5495
  * @returns {JQuery<HTMLElement>} Avatar block
5491
5496
  */
5492
5497
  function getUserAvatarBlock(name) {
5498
+ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
5493
5499
  const template = $('#user_avatar_template .avatar-container').clone();
5494
5500
  const personaName = power_user.personas[name];
5495
5501
  const personaDescription = power_user.persona_descriptions[name]?.description;
@@ -5498,7 +5504,11 @@ function getUserAvatarBlock(name) {
5498
5504
  template.attr('imgfile', name);
5499
5505
  template.find('.avatar').attr('imgfile', name).attr('title', name);
5500
5506
  template.toggleClass('default_persona', name === power_user.default_persona);
5501
- template.find('img').attr('src', getUserAvatar(name));
5507
+ let avatarUrl = getUserAvatar(name);
5508
+ if (isFirefox) {
5509
+ avatarUrl += '?t=' + Date.now();
5510
+ }
5511
+ template.find('img').attr('src', avatarUrl);
5502
5512
  $('#user_avatar_block').append(template);
5503
5513
  return template;
5504
5514
  }
@@ -6080,6 +6090,21 @@ export async function getPastCharacterChats(characterId = null) {
6080
6090
  return data;
6081
6091
  }
6082
6092
 
6093
+ /**
6094
+ * Helper for `displayPastChats`, to make the same info consistently available for other functions
6095
+ */
6096
+ function getCurrentChatDetails() {
6097
+ if (!characters[this_chid] && !selected_group) {
6098
+ return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };
6099
+ }
6100
+
6101
+ const group = selected_group ? groups.find(x => x.id === selected_group) : null;
6102
+ const currentChat = selected_group ? group?.chat_id : characters[this_chid]['chat'];
6103
+ const displayName = selected_group ? group?.name : characters[this_chid].name;
6104
+ const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']);
6105
+ return { sessionName: currentChat, group: group, characterName: displayName, avatarImgURL: avatarImg };
6106
+ }
6107
+
6083
6108
  /**
6084
6109
  * Displays the past chats for a character or a group based on the selected context.
6085
6110
  * The function first fetches the chats, processes them, and then displays them in
@@ -6090,7 +6115,6 @@ export async function displayPastChats() {
6090
6115
  $('#select_chat_div').empty();
6091
6116
  $('#select_chat_search').val('').off('input');
6092
6117
 
6093
- const group = selected_group ? groups.find(x => x.id === selected_group) : null;
6094
6118
  const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats());
6095
6119
 
6096
6120
  if (!data) {
@@ -6098,10 +6122,14 @@ export async function displayPastChats() {
6098
6122
  return;
6099
6123
  }
6100
6124
 
6101
- const currentChat = selected_group ? group?.chat_id : characters[this_chid]['chat'];
6102
- const displayName = selected_group ? group?.name : characters[this_chid].name;
6103
- const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']);
6125
+ const chatDetails = getCurrentChatDetails();
6126
+ const group = chatDetails.group;
6127
+ const currentChat = chatDetails.sessionName;
6128
+ const displayName = chatDetails.characterName;
6129
+ const avatarImg = chatDetails.avatarImgURL;
6130
+
6104
6131
  const rawChats = await getChatsFromFiles(data, selected_group);
6132
+
6105
6133
  // Sort by last message date descending
6106
6134
  data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
6107
6135
  console.log(data);
@@ -7808,15 +7836,18 @@ async function doImpersonate() {
7808
7836
  }
7809
7837
 
7810
7838
  async function doDeleteChat() {
7811
- $('#option_select_chat').trigger('click', { fromSlashCommand: true });
7812
- await delay(100);
7839
+ await displayPastChats();
7813
7840
  let currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
7814
- $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
7841
+ $(currentChatDeleteButton).trigger('click');
7815
7842
  await delay(1);
7816
- $('#dialogue_popup_ok').trigger('click');
7817
- //200 delay needed let the past chat view reshow first
7818
- await delay(200);
7819
- $('#select_chat_cross').trigger('click');
7843
+ $('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true });
7844
+ }
7845
+
7846
+ /**
7847
+ * /getchatname` slash command
7848
+ */
7849
+ async function doGetChatName() {
7850
+ return getCurrentChatDetails().sessionName;
7820
7851
  }
7821
7852
 
7822
7853
  const isPwaMode = window.navigator.standalone;
@@ -7970,6 +8001,7 @@ jQuery(async function () {
7970
8001
  registerSlashCommand('api', connectAPISlash, [], `<span class="monospace">(${Object.keys(CONNECT_API_MAP).join(', ')})</span> – connect to an API`, true, true);
7971
8002
  registerSlashCommand('impersonate', doImpersonate, ['imp'], '– calls an impersonation response', true, true);
7972
8003
  registerSlashCommand('delchat', doDeleteChat, [], '– deletes the current chat', true, true);
8004
+ registerSlashCommand('getchatname', doGetChatName, [], '– returns the name of the current chat file into the pipe', false, true);
7973
8005
  registerSlashCommand('closechat', doCloseChat, [], '– closes the current chat', true, true);
7974
8006
  registerSlashCommand('panels', doTogglePanels, ['togglepanels'], '– toggle UI panels on/off', true, true);
7975
8007
  registerSlashCommand('forcesave', doForceSave, [], '– forces a save of the current chat and settings', true, true);
@@ -8199,7 +8231,8 @@ jQuery(async function () {
8199
8231
  $('#character_popup').css('display', 'none');
8200
8232
  });
8201
8233
 
8202
- $('#dialogue_popup_ok').click(async function (e) {
8234
+ $('#dialogue_popup_ok').click(async function (e, customData) {
8235
+ const fromSlashCommand = customData?.fromSlashCommand || false;
8203
8236
  dialogueCloseStop = false;
8204
8237
  $('#shadow_popup').transition({
8205
8238
  opacity: 0,
@@ -8229,14 +8262,16 @@ jQuery(async function () {
8229
8262
  await delChat(chat_file_for_del);
8230
8263
  }
8231
8264
 
8232
- //open the history view again after 2seconds (delay to avoid edge cases for deleting last chat)
8233
- //hide option popup menu
8234
- setTimeout(function () {
8235
- $('#option_select_chat').click();
8236
- $('#options').hide();
8265
+ if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
8266
+ $('#options').hide(); // hide option popup menu
8237
8267
  hideLoader();
8238
- }, 2000);
8239
-
8268
+ } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
8269
+ setTimeout(function () {
8270
+ $('#option_select_chat').click();
8271
+ $('#options').hide(); // hide option popup menu
8272
+ hideLoader();
8273
+ }, 2000);
8274
+ }
8240
8275
  }
8241
8276
  if (popup_type == 'del_ch') {
8242
8277
  const deleteChats = !!$('#del_char_checkbox').prop('checked');
@@ -8532,6 +8567,11 @@ jQuery(async function () {
8532
8567
  await writeSecret(SECRET_KEYS.TOGETHERAI, togetherKey);
8533
8568
  }
8534
8569
 
8570
+ const oobaKey = String($('#api_key_ooba').val()).trim();
8571
+ if (oobaKey.length) {
8572
+ await writeSecret(SECRET_KEYS.OOBA, oobaKey);
8573
+ }
8574
+
8535
8575
  validateTextGenUrl();
8536
8576
  startStatusLoading();
8537
8577
  main_api = 'textgenerationwebui';
@@ -1132,7 +1132,7 @@ export function initRossMods() {
1132
1132
  .not('#right-nav-panel')
1133
1133
  .not('#floatingPrompt')
1134
1134
  .not('#cfgConfig')
1135
- .not("#logprobsViewer")
1135
+ .not('#logprobsViewer')
1136
1136
  .is(':visible')) {
1137
1137
  let visibleDrawerContent = $('.drawer-content:visible')
1138
1138
  .not('#WorldInfo')
@@ -1140,7 +1140,7 @@ export function initRossMods() {
1140
1140
  .not('#right-nav-panel')
1141
1141
  .not('#floatingPrompt')
1142
1142
  .not('#cfgConfig')
1143
- .not("#logprobsViewer");
1143
+ .not('#logprobsViewer');
1144
1144
  $(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
1145
1145
  return;
1146
1146
  }
@@ -58,6 +58,17 @@ function isTalkingHeadEnabled() {
58
58
  return extension_settings.expressions.talkinghead && !extension_settings.expressions.local;
59
59
  }
60
60
 
61
+ /**
62
+ * Toggles Talkinghead mode on/off.
63
+ *
64
+ * Implements the `/th` slash command, which is meant to be bound to a Quick Reply button
65
+ * as a quick way to switch Talkinghead on or off (e.g. to conserve GPU resources when AFK
66
+ * for a long time).
67
+ */
68
+ function toggleTalkingHeadCommand(_) {
69
+ setTalkingHeadState(!extension_settings.expressions.talkinghead);
70
+ }
71
+
61
72
  function isVisualNovelMode() {
62
73
  return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
63
74
  }
@@ -389,13 +400,14 @@ function onExpressionsShowDefaultInput() {
389
400
  }
390
401
 
391
402
  /**
392
- * Stops animating a talkinghead.
403
+ * Stops animating Talkinghead.
393
404
  */
394
405
  async function unloadTalkingHead() {
395
406
  if (!modules.includes('talkinghead')) {
396
407
  console.debug('talkinghead module is disabled');
397
408
  return;
398
409
  }
410
+ console.debug('expressions: Stopping Talkinghead');
399
411
 
400
412
  try {
401
413
  const url = new URL(getApiUrl());
@@ -418,6 +430,7 @@ async function loadTalkingHead() {
418
430
  console.debug('talkinghead module is disabled');
419
431
  return;
420
432
  }
433
+ console.debug('expressions: Starting Talkinghead');
421
434
 
422
435
  const spriteFolderName = getSpriteFolderName();
423
436
 
@@ -528,8 +541,7 @@ function handleImageChange() {
528
541
  return;
529
542
  }
530
543
 
531
- if (isTalkingHeadEnabled()) {
532
- // Method get IP of endpoint
544
+ if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
533
545
  const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`;
534
546
  $('#expression-holder').css({ display: '' });
535
547
  if (imgElement.src !== talkingheadResultFeedSrc) {
@@ -545,20 +557,26 @@ function handleImageChange() {
545
557
  }
546
558
  })
547
559
  .catch(error => {
548
- console.error(error); // Log the error if necessary
560
+ console.error(error);
549
561
  });
550
562
  }
551
563
  }
552
564
  } else {
553
- imgElement.src = ''; //remove incase char doesnt have expressions
554
- setExpression(getContext().name2, FALLBACK_EXPRESSION, true);
565
+ imgElement.src = ''; // remove in case char doesn't have expressions
566
+
567
+ // When switching Talkinghead off, force-set the character to the last known expression, if any.
568
+ // This preserves the same expression Talkinghead had at the moment it was switched off.
569
+ const charName = getContext().name2;
570
+ const last = lastExpression[charName];
571
+ const targetExpression = last ? last : FALLBACK_EXPRESSION;
572
+ setExpression(charName, targetExpression, true);
555
573
  }
556
574
  }
557
575
 
558
576
  async function moduleWorker() {
559
577
  const context = getContext();
560
578
 
561
- // Hide and disable talkinghead while in local mode
579
+ // Hide and disable Talkinghead while in local mode
562
580
  $('#image_type_block').toggle(!extension_settings.expressions.local);
563
581
 
564
582
  if (extension_settings.expressions.local && extension_settings.expressions.talkinghead) {
@@ -691,7 +709,7 @@ async function moduleWorker() {
691
709
  }
692
710
 
693
711
  /**
694
- * Starts/stops talkinghead talking animation.
712
+ * Starts/stops Talkinghead talking animation.
695
713
  *
696
714
  * Talking starts only when all the following conditions are met:
697
715
  * - The LLM is currently streaming its output.
@@ -700,10 +718,13 @@ async function moduleWorker() {
700
718
  *
701
719
  * In all other cases, talking stops.
702
720
  *
703
- * A talkinghead API call is made only when the talking state changes.
721
+ * A Talkinghead API call is made only when the talking state changes.
722
+ *
723
+ * Note that also the TTS system, if enabled, starts/stops the Talkinghead talking animation.
724
+ * See `talkingAnimation` in `SillyTavern/public/scripts/extensions/tts/index.js`.
704
725
  */
705
726
  async function updateTalkingState() {
706
- // Don't bother if talkinghead is disabled or not loaded.
727
+ // Don't bother if Talkinghead is disabled or not loaded.
707
728
  if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
708
729
  return;
709
730
  }
@@ -727,7 +748,7 @@ async function updateTalkingState() {
727
748
  newTalkingState = false;
728
749
  }
729
750
  try {
730
- // Call the talkinghead API only if the talking state changed.
751
+ // Call the Talkinghead API only if the talking state changed.
731
752
  if (newTalkingState !== lastTalkingState) {
732
753
  console.debug(`updateTalkingState: calling ${url.pathname}`);
733
754
  await doExtrasFetch(url);
@@ -787,6 +808,7 @@ function getSpriteFolderName(characterMessage = null, characterName = null) {
787
808
  }
788
809
 
789
810
  function setTalkingHeadState(newState) {
811
+ console.debug(`expressions: New talkinghead state: ${newState}`);
790
812
  extension_settings.expressions.talkinghead = newState; // Store setting
791
813
  saveSettingsDebounced();
792
814
 
@@ -871,12 +893,12 @@ async function setSpriteSlashCommand(_, spriteId) {
871
893
 
872
894
  spriteId = spriteId.trim().toLowerCase();
873
895
 
874
- // In talkinghead mode, don't check for the existence of the sprite
896
+ // In Talkinghead mode, don't check for the existence of the sprite
875
897
  // (emotion names are the same as for sprites, but it only needs "talkinghead.png").
876
898
  const currentLastMessage = getLastCharacterMessage();
877
899
  const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
878
900
  let label = spriteId;
879
- if (!isTalkingHeadEnabled()) {
901
+ if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
880
902
  await validateImages(spriteFolderName);
881
903
 
882
904
  // Fuzzy search for sprite
@@ -1051,6 +1073,8 @@ function drawSpritesList(character, labels, sprites) {
1051
1073
  * @returns {string} Rendered list item template
1052
1074
  */
1053
1075
  function getListItem(item, imageSrc, textClass, isCustom) {
1076
+ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
1077
+ imageSrc = isFirefox ? `${imageSrc}?t=${Date.now()}` : imageSrc;
1054
1078
  return renderExtensionTemplate(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
1055
1079
  }
1056
1080
 
@@ -1144,7 +1168,7 @@ async function getExpressionsList() {
1144
1168
  }
1145
1169
 
1146
1170
  async function setExpression(character, expression, force) {
1147
- if (!isTalkingHeadEnabled()) {
1171
+ if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
1148
1172
  console.debug('entered setExpressions');
1149
1173
  await validateImages(character);
1150
1174
  const img = $('img.expression');
@@ -1255,8 +1279,8 @@ async function setExpression(character, expression, force) {
1255
1279
  document.getElementById('expression-holder').style.display = '';
1256
1280
 
1257
1281
  } else {
1258
- // Set the talkinghead emotion to the specified expression
1259
- // TODO: For now, talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
1282
+ // Set the Talkinghead emotion to the specified expression
1283
+ // TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
1260
1284
  try {
1261
1285
  let result = await isTalkingHeadAvailable();
1262
1286
  if (result) {
@@ -1409,8 +1433,8 @@ async function onClickExpressionUpload(event) {
1409
1433
  // Reset the input
1410
1434
  e.target.form.reset();
1411
1435
 
1412
- // In talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
1413
- if (isTalkingHeadEnabled() && id === 'talkinghead') {
1436
+ // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
1437
+ if (id === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1414
1438
  await loadTalkingHead();
1415
1439
  }
1416
1440
  };
@@ -1520,6 +1544,11 @@ async function onClickExpressionUploadPackButton() {
1520
1544
 
1521
1545
  // Reset the input
1522
1546
  e.target.form.reset();
1547
+
1548
+ // In Talkinghead mode, refresh the live char.
1549
+ if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1550
+ await loadTalkingHead();
1551
+ }
1523
1552
  };
1524
1553
 
1525
1554
  $('#expression_upload_pack')
@@ -1657,6 +1686,34 @@ async function fetchImagesNoCache() {
1657
1686
  $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
1658
1687
  }
1659
1688
 
1689
+ // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.
1690
+ // We currently do this via loading/unloading. Could be improved by adding new pause/unpause endpoints to Extras.
1691
+ document.addEventListener('visibilitychange', function (event) {
1692
+ let pageIsVisible;
1693
+ if (document.hidden) {
1694
+ console.debug('expressions: SillyTavern is now hidden');
1695
+ pageIsVisible = false;
1696
+ } else {
1697
+ console.debug('expressions: SillyTavern is now visible');
1698
+ pageIsVisible = true;
1699
+ }
1700
+
1701
+ if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1702
+ isTalkingHeadAvailable().then(result => {
1703
+ if (result) {
1704
+ if (pageIsVisible) {
1705
+ loadTalkingHead();
1706
+ } else {
1707
+ unloadTalkingHead();
1708
+ }
1709
+ handleImageChange(); // Change image as needed
1710
+ } else {
1711
+ //console.log("talkinghead does not exist.");
1712
+ }
1713
+ });
1714
+ }
1715
+ });
1716
+
1660
1717
  addExpressionImage();
1661
1718
  addVisualNovelMode();
1662
1719
  addSettings();
@@ -1664,7 +1721,7 @@ async function fetchImagesNoCache() {
1664
1721
  const updateFunction = wrapper.update.bind(wrapper);
1665
1722
  setInterval(updateFunction, UPDATE_INTERVAL);
1666
1723
  moduleWorker();
1667
- // For setting the talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
1724
+ // For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
1668
1725
  const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState);
1669
1726
  const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState);
1670
1727
  setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL);
@@ -1701,4 +1758,5 @@ async function fetchImagesNoCache() {
1701
1758
  registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
1702
1759
  registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
1703
1760
  registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
1761
+ registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], '– Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.');
1704
1762
  })();
@@ -73,12 +73,12 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
73
73
  ) {
74
74
  // Check if the depth is within the min/max depth
75
75
  if (typeof depth === 'number' && depth >= 0) {
76
- if (!isNaN(script.minDepth) && script.minDepth >= 0 && depth < script.minDepth) {
76
+ if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {
77
77
  console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is less than minDepth ${script.minDepth}`);
78
78
  return;
79
79
  }
80
80
 
81
- if (!isNaN(script.maxDepth) && script.maxDepth >= 0 && depth > script.maxDepth) {
81
+ if (!isNaN(script.maxDepth) && script.maxDepth !== null && script.maxDepth >= 0 && depth > script.maxDepth) {
82
82
  console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is greater than maxDepth ${script.maxDepth}`);
83
83
  return;
84
84
  }
@@ -536,7 +536,7 @@ async function processTtsQueue() {
536
536
  }
537
537
 
538
538
  if (extension_settings.tts.narrate_quoted_only) {
539
- const special_quotes = /[“”]/g; // Extend this regex to include other special quotes
539
+ const special_quotes = /[“”«»]/g; // Extend this regex to include other special quotes
540
540
  text = text.replace(special_quotes, '"');
541
541
  const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
542
542
  const partJoiner = (ttsProvider?.separator || ' ... ');
@@ -674,7 +674,7 @@ export function parseNovelAILogprobs(data) {
674
674
  // them with a logprob of -Infinity (0% probability)
675
675
  const notInAfter = befores
676
676
  .filter(([id]) => !afters.some(([aid]) => aid === id))
677
- .map(([id]) => [id, -Infinity])
677
+ .map(([id]) => [id, -Infinity]);
678
678
  const merged = afters.concat(notInAfter);
679
679
 
680
680
  // Add the chosen token to `merged` if it's not already there. This can
@@ -31,6 +31,7 @@ import {
31
31
  this_chid,
32
32
  } from '../script.js';
33
33
  import { groups, selected_group } from './group-chats.js';
34
+ import { registerSlashCommand } from './slash-commands.js';
34
35
 
35
36
  import {
36
37
  chatCompletionDefaultPrompts,
@@ -1617,7 +1618,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1617
1618
  }
1618
1619
 
1619
1620
  // Add logprobs request (currently OpenAI only, max 5 on their side)
1620
- if (useLogprobs && isOAI) {
1621
+ if (useLogprobs && (isOAI || isCustom)) {
1621
1622
  generate_data['logprobs'] = 5;
1622
1623
  }
1623
1624
 
@@ -1691,24 +1692,17 @@ async function sendOpenAIRequest(type, messages, signal) {
1691
1692
  throw new Error(`Got response status ${response.status}`);
1692
1693
  }
1693
1694
  if (stream) {
1694
- let reader;
1695
- let isSSEStream = oai_settings.chat_completion_source !== chat_completion_sources.MAKERSUITE;
1696
- if (isSSEStream) {
1697
- const eventStream = new EventSourceStream();
1698
- response.body.pipeThrough(eventStream);
1699
- reader = eventStream.readable.getReader();
1700
- } else {
1701
- reader = response.body.getReader();
1702
- }
1695
+ const eventStream = new EventSourceStream();
1696
+ response.body.pipeThrough(eventStream);
1697
+ const reader = eventStream.readable.getReader();
1703
1698
  return async function* streamData() {
1704
1699
  let text = '';
1705
- let utf8Decoder = new TextDecoder();
1706
1700
  const swipes = [];
1707
1701
  while (true) {
1708
1702
  const { done, value } = await reader.read();
1709
1703
  if (done) return;
1710
- const rawData = isSSEStream ? value.data : utf8Decoder.decode(value, { stream: true });
1711
- if (isSSEStream && rawData === '[DONE]') return;
1704
+ const rawData = value.data;
1705
+ if (rawData === '[DONE]') return;
1712
1706
  tryParseStreamingError(response, rawData);
1713
1707
  const parsed = JSON.parse(rawData);
1714
1708
 
@@ -1749,7 +1743,7 @@ function getStreamingReply(data) {
1749
1743
  if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
1750
1744
  return data?.completion || '';
1751
1745
  } else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
1752
- return data?.candidates[0].content.parts[0].text || '';
1746
+ return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
1753
1747
  } else {
1754
1748
  return data.choices[0]?.delta?.content || data.choices[0]?.message?.content || data.choices[0]?.text || '';
1755
1749
  }
@@ -1768,6 +1762,7 @@ function parseChatCompletionLogprobs(data) {
1768
1762
 
1769
1763
  switch (oai_settings.chat_completion_source) {
1770
1764
  case chat_completion_sources.OPENAI:
1765
+ case chat_completion_sources.CUSTOM:
1771
1766
  if (!data.choices?.length) {
1772
1767
  return null;
1773
1768
  }
@@ -3933,6 +3928,28 @@ $('#delete_proxy').on('click', async function () {
3933
3928
  }
3934
3929
  });
3935
3930
 
3931
+ function runProxyCallback(_, value) {
3932
+ if (!value) {
3933
+ toastr.warning('Proxy preset name is required');
3934
+ return '';
3935
+ }
3936
+
3937
+ const proxyNames = proxies.map(preset => preset.name);
3938
+ const fuse = new Fuse(proxyNames);
3939
+ const result = fuse.search(value);
3940
+
3941
+ if (result.length === 0) {
3942
+ toastr.warning(`Proxy preset "${value}" not found`);
3943
+ return '';
3944
+ }
3945
+
3946
+ const foundName = result[0].item;
3947
+ $('#openai_proxy_preset').val(foundName).trigger('change');
3948
+ return foundName;
3949
+ }
3950
+
3951
+ registerSlashCommand('proxy', runProxyCallback, [], '<span class="monospace">(name)</span> – sets a proxy preset by name');
3952
+
3936
3953
  $(document).ready(async function () {
3937
3954
  $('#test_api_button').on('click', testApiConnection);
3938
3955
 
@@ -17,6 +17,7 @@ export const SECRET_KEYS = {
17
17
  MISTRALAI: 'api_key_mistralai',
18
18
  TOGETHERAI: 'api_key_togetherai',
19
19
  CUSTOM: 'api_key_custom',
20
+ OOBA: 'api_key_ooba',
20
21
  };
21
22
 
22
23
  const INPUT_MAP = {
@@ -35,6 +36,7 @@ const INPUT_MAP = {
35
36
  [SECRET_KEYS.MISTRALAI]: '#api_key_mistralai',
36
37
  [SECRET_KEYS.CUSTOM]: '#api_key_custom',
37
38
  [SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai',
39
+ [SECRET_KEYS.OOBA]: '#api_key_ooba',
38
40
  };
39
41
 
40
42
  async function clearSecret() {
@@ -154,7 +154,7 @@ parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">
154
154
  parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> – asks a specified character card a prompt', true, true);
155
155
  parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> – deletes all messages attributed to a specified name', true, true);
156
156
  parser.addCommand('send', sendUserMessageCallback, [], '<span class="monospace">(text)</span> – adds a user message to the chat log without triggering a generation', true, true);
157
- parser.addCommand('trigger', triggerGenerationCallback, [], ' – triggers a message generation. If in group, can trigger a message for the specified group member index or name.', true, true);
157
+ parser.addCommand('trigger', triggerGenerationCallback, [], ' <span class="monospace">await=true/false</span> – triggers a message generation. If in group, can trigger a message for the specified group member index or name. If <code>await=true</code> named argument passed, the command will await for the triggered generation before continuing.', true, true);
158
158
  parser.addCommand('hide', hideMessageCallback, [], '<span class="monospace">(message index or range)</span> – hides a chat message from the prompt', true, true);
159
159
  parser.addCommand('unhide', unhideMessageCallback, [], '<span class="monospace">(message index or range)</span> – unhides a message from the prompt', true, true);
160
160
  parser.addCommand('disable', disableGroupMemberCallback, [], '<span class="monospace">(member index or name)</span> – disables a group member from being drafted for replies', true, true);
@@ -1029,8 +1029,9 @@ async function addGroupMemberCallback(_, arg) {
1029
1029
  return character.name;
1030
1030
  }
1031
1031
 
1032
- async function triggerGenerationCallback(_, arg) {
1033
- setTimeout(async () => {
1032
+ async function triggerGenerationCallback(args, value) {
1033
+ const shouldAwait = isTrueBoolean(args?.await);
1034
+ const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
1034
1035
  try {
1035
1036
  await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
1036
1037
  } catch {
@@ -1044,16 +1045,21 @@ async function triggerGenerationCallback(_, arg) {
1044
1045
 
1045
1046
  let chid = undefined;
1046
1047
 
1047
- if (selected_group && arg) {
1048
- chid = findGroupMemberId(arg);
1048
+ if (selected_group && value) {
1049
+ chid = findGroupMemberId(value);
1049
1050
 
1050
1051
  if (chid === undefined) {
1051
- console.warn(`WARN: No group member found for argument ${arg}`);
1052
+ console.warn(`WARN: No group member found for argument ${value}`);
1052
1053
  }
1053
1054
  }
1054
1055
 
1055
- setTimeout(() => Generate('normal', { force_chid: chid }), 100);
1056
- }, 1);
1056
+ outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('normal', { force_chid: chid })), 100)));
1057
+ }, 1));
1058
+
1059
+ if (shouldAwait) {
1060
+ const innerPromise = await outerPromise;
1061
+ await innerPromise;
1062
+ }
1057
1063
 
1058
1064
  return '';
1059
1065
  }
@@ -716,6 +716,7 @@ function parseTextgenLogprobs(token, logprobs) {
716
716
  }
717
717
 
718
718
  switch (settings.type) {
719
+ case TABBY:
719
720
  case APHRODITE:
720
721
  case OOBA: {
721
722
  /** @type {Record<string, number>[]} */
@@ -807,6 +808,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
807
808
  'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
808
809
  'top_p': settings.top_p,
809
810
  'typical_p': settings.typical_p,
811
+ 'typical': settings.typical_p,
812
+ 'sampler_seed': settings.seed,
810
813
  'min_p': settings.min_p,
811
814
  'repetition_penalty': settings.rep_pen,
812
815
  'frequency_penalty': settings.freq_pen,
@@ -819,8 +822,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
819
822
  'early_stopping': settings.early_stopping,
820
823
  'add_bos_token': settings.add_bos_token,
821
824
  'dynamic_temperature': settings.dynatemp,
822
- 'dynatemp_low': settings.dynatemp ? settings.min_temp : 0,
823
- 'dynatemp_high': settings.dynatemp ? settings.max_temp : 0,
825
+ 'dynatemp_low': settings.dynatemp ? settings.min_temp : 1,
826
+ 'dynatemp_high': settings.dynatemp ? settings.max_temp : 1,
824
827
  'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0,
825
828
  'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : 1,
826
829
  'smoothing_factor': settings.smoothing_factor,
File without changes
@@ -37,6 +37,14 @@ function getTabbyHeaders() {
37
37
  }) : {};
38
38
  }
39
39
 
40
+ function getOobaHeaders() {
41
+ const apiKey = readSecret(SECRET_KEYS.OOBA);
42
+
43
+ return apiKey ? ({
44
+ 'Authorization': `Bearer ${apiKey}`,
45
+ }) : {};
46
+ }
47
+
40
48
  function getOverrideHeaders(urlHost) {
41
49
  const requestOverrides = getConfigValue('requestOverrides', []);
42
50
  const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers;
@@ -69,6 +77,9 @@ function setAdditionalHeaders(request, args, server) {
69
77
  case TEXTGEN_TYPES.TOGETHERAI:
70
78
  headers = getTogetherAIHeaders();
71
79
  break;
80
+ case TEXTGEN_TYPES.OOBA:
81
+ headers = getOobaHeaders();
82
+ break;
72
83
  default:
73
84
  headers = server ? getOverrideHeaders((new URL(server))?.host) : {};
74
85
  break;
package/src/constants.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const DIRECTORIES = {
2
2
  worlds: 'public/worlds/',
3
+ user: 'public/user',
3
4
  avatars: 'public/User Avatars',
4
5
  images: 'public/img/',
5
6
  userImages: 'public/user/images/',
@@ -267,7 +267,7 @@ async function sendMakerSuiteRequest(request, response) {
267
267
  ? (stream ? 'streamGenerateContent' : 'generateContent')
268
268
  : (isText ? 'generateText' : 'generateMessage');
269
269
 
270
- const generateResponse = await fetch(`https://generativelanguage.googleapis.com/${apiVersion}/models/${model}:${responseType}?key=${apiKey}`, {
270
+ const generateResponse = await fetch(`https://generativelanguage.googleapis.com/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
271
271
  body: JSON.stringify(body),
272
272
  method: 'POST',
273
273
  headers: {
@@ -279,36 +279,8 @@ async function sendMakerSuiteRequest(request, response) {
279
279
  // have to do this because of their busted ass streaming endpoint
280
280
  if (stream) {
281
281
  try {
282
- let partialData = '';
283
- generateResponse.body.on('data', (data) => {
284
- const chunk = data.toString();
285
- if (chunk.startsWith(',') || chunk.endsWith(',') || chunk.startsWith('[') || chunk.endsWith(']')) {
286
- partialData = chunk.slice(1);
287
- } else {
288
- partialData += chunk;
289
- }
290
- while (true) {
291
- let json;
292
- try {
293
- json = JSON.parse(partialData);
294
- } catch (e) {
295
- break;
296
- }
297
- response.write(JSON.stringify(json));
298
- partialData = '';
299
- }
300
- });
301
-
302
- request.socket.on('close', function () {
303
- if (generateResponse.body instanceof Readable) generateResponse.body.destroy();
304
- response.end();
305
- });
306
-
307
- generateResponse.body.on('end', () => {
308
- console.log('Streaming request finished');
309
- response.end();
310
- });
311
-
282
+ // Pipe remote SSE stream to Express response
283
+ forwardFetchResponse(generateResponse, response);
312
284
  } catch (error) {
313
285
  console.log('Error forwarding streaming response:', error);
314
286
  if (!response.headersSent) {
@@ -719,7 +691,7 @@ router.post('/generate', jsonParser, function (request, response) {
719
691
  // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }
720
692
  if (!isTextCompletion && bodyParams.logprobs > 0) {
721
693
  bodyParams.top_logprobs = bodyParams.logprobs;
722
- bodyParams.logprobs = true
694
+ bodyParams.logprobs = true;
723
695
  }
724
696
 
725
697
  if (getConfigValue('openai.randomizeUserId', false)) {
@@ -751,7 +723,16 @@ router.post('/generate', jsonParser, function (request, response) {
751
723
  apiUrl = request.body.custom_url;
752
724
  apiKey = readSecret(SECRET_KEYS.CUSTOM);
753
725
  headers = {};
754
- bodyParams = {};
726
+ bodyParams = {
727
+ logprobs: request.body.logprobs,
728
+ };
729
+
730
+ // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }
731
+ if (!isTextCompletion && bodyParams.logprobs > 0) {
732
+ bodyParams.top_logprobs = bodyParams.logprobs;
733
+ bodyParams.logprobs = true;
734
+ }
735
+
755
736
  mergeObjectWithYaml(bodyParams, request.body.custom_include_body);
756
737
  mergeObjectWithYaml(headers, request.body.custom_include_headers);
757
738
  } else {
@@ -216,7 +216,7 @@ router.post('/generate', jsonParser, async function (req, res) {
216
216
  }
217
217
 
218
218
  const data = await response.json();
219
- console.log("NovelAI Output", data?.output);
219
+ console.log('NovelAI Output', data?.output);
220
220
  return res.send(data);
221
221
  }
222
222
  } catch (error) {
@@ -33,6 +33,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
33
33
  }
34
34
 
35
35
  if (request.body.api === 'ooba') {
36
+ key = readSecret(SECRET_KEYS.OOBA);
36
37
  bodyParams.temperature = 0.1;
37
38
  }
38
39
 
@@ -29,6 +29,7 @@ const SECRET_KEYS = {
29
29
  TOGETHERAI: 'api_key_togetherai',
30
30
  MISTRALAI: 'api_key_mistralai',
31
31
  CUSTOM: 'api_key_custom',
32
+ OOBA: 'api_key_ooba',
32
33
  };
33
34
 
34
35
  /**
@@ -172,7 +172,7 @@ function getSourceSettings(source, request) {
172
172
 
173
173
  const sourceSettings = {
174
174
  extrasUrl: extrasUrl,
175
- extrasKey: extrasKey
175
+ extrasKey: extrasKey,
176
176
  };
177
177
  return sourceSettings;
178
178
  }
@@ -1,37 +0,0 @@
1
- name: Build and Publish Release (Release)
2
-
3
- on:
4
- push:
5
- branches:
6
- - release
7
-
8
- jobs:
9
- build_and_publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v2
15
-
16
- - name: Set up Node.js
17
- uses: actions/setup-node@v2
18
- with:
19
- node-version: 18
20
-
21
- - name: Install dependencies
22
- run: npm ci
23
-
24
- - name: Build and package with pkg
25
- run: |
26
- npm install -g pkg
27
- npm run pkg
28
-
29
- - name: Upload binaries to release
30
- uses: softprops/action-gh-release@v1
31
- with:
32
- files: dist/*
33
- tag_name: ci-release
34
- name: Continuous Release (Release)
35
- prerelease: true
36
- env:
37
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,37 +0,0 @@
1
- name: Build and Publish Release (Staging)
2
-
3
- on:
4
- push:
5
- branches:
6
- - staging
7
-
8
- jobs:
9
- build_and_publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v2
15
-
16
- - name: Set up Node.js
17
- uses: actions/setup-node@v2
18
- with:
19
- node-version: 18
20
-
21
- - name: Install dependencies
22
- run: npm ci
23
-
24
- - name: Build and package with pkg
25
- run: |
26
- npm install -g pkg
27
- npm run pkg
28
-
29
- - name: Upload binaries to release
30
- uses: softprops/action-gh-release@v1
31
- with:
32
- files: dist/*
33
- tag_name: ci-staging
34
- name: Continuous Release (Staging)
35
- prerelease: true
36
- env:
37
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}