Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging

This commit is contained in:
RossAscends
2024-04-12 08:21:18 +09:00
25 changed files with 1174 additions and 771 deletions

View File

@@ -108,13 +108,22 @@ export function humanizeGenTime(total_gen_time) {
return time_spent;
}
let parsedUA = null;
try {
parsedUA = Bowser.parse(navigator.userAgent);
} catch {
// In case the user agent is an empty string or Bowser can't parse it for some other reason
}
/**
* DON'T OPTIMIZE, don't change this to a const or let, it needs to be a var.
*/
var parsedUA = null;
function getParsedUA() {
if (!parsedUA) {
try {
parsedUA = Bowser.parse(navigator.userAgent);
} catch {
// In case the user agent is an empty string or Bowser can't parse it for some other reason
}
}
return parsedUA;
}
/**
* Checks if the device is a mobile device.
@@ -123,7 +132,7 @@ try {
export function isMobile() {
const mobileTypes = ['mobile', 'tablet'];
return mobileTypes.includes(parsedUA?.platform?.type);
return mobileTypes.includes(getParsedUA()?.platform?.type);
}
export function shouldSendOnEnter() {
@@ -275,7 +284,7 @@ export async function favsToHotswap() {
//helpful instruction message if no characters are favorited
if (favs.length == 0) {
container.html('<small><span><i class="fa-solid fa-star"></i> <span data-i18n="Favorite characters to add them to HotSwaps">Favorite characters to add them to HotSwaps</span></span></small>');
container.html(`<small><span><i class="fa-solid fa-star"></i>${DOMPurify.sanitize(container.attr('no_favs'))}</span></small>`);
return;
}
@@ -285,7 +294,8 @@ export async function favsToHotswap() {
//changes input bar and send button display depending on connection status
function RA_checkOnlineStatus() {
if (online_status == 'no_connection') {
$('#send_textarea').attr('placeholder', 'Not connected to API!'); //Input bar placeholder tells users they are not connected
const send_textarea = $('#send_textarea');
send_textarea.attr('placeholder', send_textarea.attr('no_connection_text')); //Input bar placeholder tells users they are not connected
$('#send_form').addClass('no-connection'); //entire input form area is red when not connected
$('#send_but').addClass('displayNone'); //send button is hidden when not connected;
$('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
@@ -294,7 +304,8 @@ function RA_checkOnlineStatus() {
connection_made = false;
} else {
if (online_status !== undefined && online_status !== 'no_connection') {
$('#send_textarea').attr('placeholder', 'Type a message, or /? for help'); //on connect, placeholder tells user to type message
const send_textarea = $('#send_textarea');
send_textarea.attr('placeholder', send_textarea.attr('connected_text')); //on connect, placeholder tells user to type message
$('#send_form').removeClass('no-connection');
$('#API-status-top').removeClass('fa-plug-circle-exclamation redOverlayGlow');
$('#API-status-top').addClass('fa-plug');

View File

@@ -1,5 +1,6 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate, animation_duration } from '../script.js';
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { hideLoader, showLoader } from './loader.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { isSubsetOf, setValueByPath } from './utils.js';
export {
getContext,
@@ -50,17 +51,31 @@ export function saveMetadataDebounced() {
}
/**
* Provides an ability for extensions to render HTML templates.
* Provides an ability for extensions to render HTML templates synchronously.
* Templates sanitation and localization is forced.
* @param {string} extensionName Extension name
* @param {string} templateId Template ID
* @param {object} templateData Additional data to pass to the template
* @returns {string} Rendered HTML
*
* @deprecated Use renderExtensionTemplateAsync instead.
*/
export function renderExtensionTemplate(extensionName, templateId, templateData = {}, sanitize = true, localize = true) {
return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
}
/**
* Provides an ability for extensions to render HTML templates asynchronously.
* Templates sanitation and localization is forced.
* @param {string} extensionName Extension name
* @param {string} templateId Template ID
* @param {object} templateData Additional data to pass to the template
* @returns {Promise<string>} Rendered HTML
*/
export function renderExtensionTemplateAsync(extensionName, templateId, templateData = {}, sanitize = true, localize = true) {
return renderTemplateAsync(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
}
// Disables parallel updates
class ModuleWorkerWrapper {
constructor(callback) {

View File

@@ -4,7 +4,7 @@ TODO:
//const DEBUG_TONY_SAMA_FORK_MODE = true
import { getRequestHeaders, callPopup, processDroppedFiles } from '../../../script.js';
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplate } from '../../extensions.js';
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { getStringHash, isValidUrl } from '../../utils.js';
export { MODULE_NAME };
@@ -355,7 +355,8 @@ async function updateCurrentAssets() {
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(renderExtensionTemplate(MODULE_NAME, 'window', {}));
const windowTemplate = await renderExtensionTemplateAsync(MODULE_NAME, 'window', {});
const windowHtml = $(windowTemplate);
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
assetsJsonUrl.val(ASSETS_JSON_URL);
@@ -366,7 +367,7 @@ jQuery(async () => {
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
const template = renderExtensionTemplate(MODULE_NAME, 'confirm', { url });
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'confirm', { url });
const confirmation = skipConfirm || await callPopup(template, 'confirm');
if (confirmation) {

View File

@@ -1,6 +1,6 @@
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from '../../../script.js';
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from '../../extensions.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
@@ -594,7 +594,7 @@ async function moduleWorker() {
}
// non-characters not supported
if (!context.groupId && (context.characterId === undefined || context.characterId === 'invalid-safety-id')) {
if (!context.groupId && context.characterId === undefined) {
removeExpression();
return;
}
@@ -977,6 +977,10 @@ async function getExpressionLabel(text) {
return getFallbackExpression();
}
if (extension_settings.expressions.translate && typeof window['translate'] === 'function') {
text = await window['translate'](text, 'en');
}
text = sampleClassifyText(text);
try {
@@ -1051,18 +1055,18 @@ async function validateImages(character, forceRedrawCached) {
if (spriteCache[character]) {
if (forceRedrawCached && $('#image_list').data('name') !== character) {
console.debug('force redrawing character sprites list');
drawSpritesList(character, labels, spriteCache[character]);
await drawSpritesList(character, labels, spriteCache[character]);
}
return;
}
const sprites = await getSpritesList(character);
let validExpressions = drawSpritesList(character, labels, sprites);
let validExpressions = await drawSpritesList(character, labels, sprites);
spriteCache[character] = validExpressions;
}
function drawSpritesList(character, labels, sprites) {
async function drawSpritesList(character, labels, sprites) {
let validExpressions = [];
$('#no_chat_expressions').hide();
$('#open_chat_expressions').show();
@@ -1074,18 +1078,20 @@ function drawSpritesList(character, labels, sprites) {
return [];
}
labels.sort().forEach((item) => {
for (const item of labels.sort()) {
const sprite = sprites.find(x => x.label == item);
const isCustom = extension_settings.expressions.custom.includes(item);
if (sprite) {
validExpressions.push(sprite);
$('#image_list').append(getListItem(item, sprite.path, 'success', isCustom));
const listItem = await getListItem(item, sprite.path, 'success', isCustom);
$('#image_list').append(listItem);
}
else {
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure', isCustom));
const listItem = await getListItem(item, '/img/No-Image-Placeholder.svg', 'failure', isCustom);
$('#image_list').append(listItem);
}
});
}
return validExpressions;
}
@@ -1095,12 +1101,12 @@ function drawSpritesList(character, labels, sprites) {
* @param {string} imageSrc Path to image
* @param {'success' | 'failure'} textClass 'success' or 'failure'
* @param {boolean} isCustom If expression is added by user
* @returns {string} Rendered list item template
* @returns {Promise<string>} Rendered list item template
*/
function getListItem(item, imageSrc, textClass, isCustom) {
async function getListItem(item, imageSrc, textClass, isCustom) {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
imageSrc = isFirefox ? `${imageSrc}?t=${Date.now()}` : imageSrc;
return renderExtensionTemplate(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
}
async function getSpritesList(name) {
@@ -1162,7 +1168,7 @@ async function renderFallbackExpressionPicker() {
async function getExpressionsList() {
// Return cached list if available
if (Array.isArray(expressionsList)) {
return expressionsList;
return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
}
/**
@@ -1211,7 +1217,7 @@ async function getExpressionsList() {
}
const result = await resolveExpressionsList();
return [...result, ...extension_settings.expressions.custom];
return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
}
async function setExpression(character, expression, force) {
@@ -1367,7 +1373,8 @@ function onClickExpressionImage() {
}
async function onClickExpressionAddCustom() {
let expressionName = await callPopup(renderExtensionTemplate(MODULE_NAME, 'add-custom-expression'), 'input');
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'add-custom-expression');
let expressionName = await callPopup(template, 'input');
if (!expressionName) {
console.debug('No custom expression name provided');
@@ -1406,14 +1413,15 @@ async function onClickExpressionAddCustom() {
}
async function onClickExpressionRemoveCustom() {
const selectedExpression = $('#expression_custom').val();
const selectedExpression = String($('#expression_custom').val());
if (!selectedExpression) {
console.debug('No custom expression selected');
return;
}
const confirmation = await callPopup(renderExtensionTemplate(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression }), 'confirm');
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression });
const confirmation = await callPopup(template, 'confirm');
if (!confirmation) {
console.debug('Custom expression removal cancelled');
@@ -1712,11 +1720,16 @@ async function fetchImagesNoCache() {
$('body').append(element);
}
async function addSettings() {
$('#extensions_settings').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');
$('#extensions_settings').append(template);
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
$('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () {
extension_settings.expressions.translate = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#expression_local').prop('checked', extension_settings.expressions.local).on('input', function () {
extension_settings.expressions.local = !!$(this).prop('checked');
moduleWorker();
@@ -1743,7 +1756,7 @@ async function fetchImagesNoCache() {
$('#expression_custom_add').on('click', onClickExpressionAddCustom);
$('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
$('#expression_fallback').on('change', onExpressionFallbackChanged)
$('#expression_fallback').on('change', onExpressionFallbackChanged);
}
// Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.

View File

@@ -10,6 +10,10 @@
<input id="expression_local" type="checkbox" />
<span data-i18n="Local server classification">Local server classification</span>
</label>
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
<input id="expression_translate" type="checkbox">
<span>Translate text to English before classification</span>
</label>
<label class="checkbox_label" for="expressions_show_default">
<input id="expressions_show_default" type="checkbox">
<span>Show default images (emojis) if sprite missing</span>

View File

@@ -1,5 +1,5 @@
import { getStringHash, debounce, waitUntilCondition, extractAllWords, delay } from '../../utils.js';
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js';
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import {
activateSendButtons,
deactivateSendButtons,
@@ -847,9 +847,9 @@ function setupListeners() {
});
}
jQuery(function () {
function addExtensionControls() {
const settingsHtml = renderExtensionTemplate('memory', 'settings', { defaultSettings });
jQuery(async function () {
async function addExtensionControls() {
const settingsHtml = await renderExtensionTemplateAsync('memory', 'settings', { defaultSettings });
$('#extensions_settings2').append(settingsHtml);
setupListeners();
$('#summaryExtensionPopoutButton').off('click').on('click', function (e) {
@@ -858,7 +858,7 @@ jQuery(function () {
});
}
addExtensionControls();
await addExtensionControls();
loadSettings();
eventSource.on(event_types.MESSAGE_RECEIVED, onChatEvent);
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);

View File

@@ -1,5 +1,5 @@
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
import { resolveVariable } from '../../variables.js';
@@ -71,7 +71,7 @@ async function deleteRegexScript({ existingId }) {
async function loadRegexScripts() {
$('#saved_regex_scripts').empty();
const scriptTemplate = $(await $.get('scripts/extensions/regex/scriptTemplate.html'));
const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
extension_settings.regex.forEach((script) => {
// Have to clone here
@@ -113,7 +113,7 @@ async function loadRegexScripts() {
}
async function onRegexEditorOpenClick(existingId) {
const editorHtml = $(await $.get('scripts/extensions/regex/editor.html'));
const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
// If an ID exists, fill in all the values
let existingScriptIndex = -1;
@@ -316,7 +316,7 @@ jQuery(async () => {
return;
}
const settingsHtml = await $.get('scripts/extensions/regex/dropdown.html');
const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
$('#extensions_settings2').append(settingsHtml);
$('#open_regex_editor').on('click', function () {
onRegexEditorOpenClick(false);

View File

@@ -18,7 +18,7 @@ import {
formatCharacterAvatar,
substituteParams,
} from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean } from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
@@ -2990,7 +2990,8 @@ jQuery(async () => {
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
const template = await renderExtensionTemplateAsync('stable-diffusion', 'settings', defaultSettings);
$('#extensions_settings').append(template);
$('#sd_source').on('change', onSourceChange);
$('#sd_scale').on('input', onScaleInput);
$('#sd_steps').on('input', onStepsInput);

View File

@@ -509,6 +509,8 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
window['translate'] = translate;
jQuery(() => {
const html = `
<div class="translation_settings">

View File

@@ -14,6 +14,8 @@ class ElevenLabsTtsProvider {
defaultSettings = {
stability: 0.75,
similarity_boost: 0.75,
style_exaggeration: 0.00,
speaker_boost: true,
apiKey: '',
model: 'eleven_monolingual_v1',
voiceMap: {},
@@ -26,27 +28,57 @@ class ElevenLabsTtsProvider {
<input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
<label for="elevenlabs_tts_model">Model</label>
<select id="elevenlabs_tts_model" class="text_pole">
<option value="eleven_monolingual_v1">Monolingual</option>
<option value="eleven_monolingual_v1">English v1</option>
<option value="eleven_multilingual_v1">Multilingual v1</option>
<option value="eleven_multilingual_v2">Multilingual v2</option>
<option value="eleven_turbo_v2">Turbo v2</option>
</select>
<input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
<label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" />
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.01" />
<div id="elevenlabs_tts_v2_options" style="display: none;">
<label for="elevenlabs_tts_style_exaggeration">Style Exaggeration: <span id="elevenlabs_tts_style_exaggeration_output"></span></label>
<input id="elevenlabs_tts_style_exaggeration" type="range" value="${this.defaultSettings.style_exaggeration}" min="0" max="1" step="0.01" />
<label for="elevenlabs_tts_speaker_boost">Speaker Boost:</label>
<input id="elevenlabs_tts_speaker_boost" style="display: inline-grid" type="checkbox" />
</div>
<hr>
<div id="elevenlabs_tts_voice_cloning">
<span>Instant Voice Cloning</span><br>
<input id="elevenlabs_tts_voice_cloning_name" type="text" class="text_pole" placeholder="Voice Name"/>
<input id="elevenlabs_tts_voice_cloning_description" type="text" class="text_pole" placeholder="Voice Description"/>
<input id="elevenlabs_tts_voice_cloning_labels" type="text" class="text_pole" placeholder="Labels"/>
<div class="menu_button menu_button_icon" id="upload_audio_file">
<i class="fa-solid fa-file-import"></i>
<span>Upload Audio Files</span>
</div>
<input id="elevenlabs_tts_audio_files" type="file" name="audio_files" accept="audio/*" style="display: none;" multiple>
<div id="elevenlabs_tts_selected_files_list"></div>
<input id="elevenlabs_tts_clone_voice_button" class="menu_button menu_button_icon" type="button" value="Clone Voice">
</div>
<hr>
</div>
`;
return html;
}
shouldInvolveExtendedSettings() {
return this.settings.model === 'eleven_multilingual_v2';
}
onSettingsChange() {
// Update dynamically
this.settings.stability = $('#elevenlabs_tts_stability').val();
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val();
this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val();
this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked');
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
$('#elevenlabs_tts_stability_output').text(this.settings.stability * 100 + '%');
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost * 100 + '%');
$('#elevenlabs_tts_style_exaggeration_output').text(this.settings.style_exaggeration * 100 + '%');
$('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
saveTtsProviderSettings();
}
@@ -75,21 +107,28 @@ class ElevenLabsTtsProvider {
$('#elevenlabs_tts_stability').val(this.settings.stability);
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost);
$('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
$('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
$('#elevenlabs_tts_api_key').val(this.settings.apiKey);
$('#elevenlabs_tts_model').val(this.settings.model);
$('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
$('#elevenlabs_tts_style_exaggeration_output').text(this.settings.style_exaggeration);
$('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
try {
await this.checkReady();
console.debug('ElevenLabs: Settings loaded');
} catch {
console.debug('ElevenLabs: Settings loaded, but not ready');
}
this.setupVoiceCloningMenu();
}
// Perform a simple readiness check by trying to fetch voiceIds
@@ -107,6 +146,63 @@ class ElevenLabsTtsProvider {
});
}
setupVoiceCloningMenu() {
const audioFilesInput = document.getElementById('elevenlabs_tts_audio_files');
const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');
const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');
const uploadAudioFileButton = document.getElementById('upload_audio_file');
const voiceCloningNameInput = document.getElementById('elevenlabs_tts_voice_cloning_name');
const voiceCloningDescriptionInput = document.getElementById('elevenlabs_tts_voice_cloning_description');
const voiceCloningLabelsInput = document.getElementById('elevenlabs_tts_voice_cloning_labels');
const updateCloneVoiceButtonVisibility = () => {
cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';
};
const clearSelectedFiles = () => {
audioFilesInput.value = '';
selectedFilesListElement.innerHTML = '';
updateCloneVoiceButtonVisibility();
};
uploadAudioFileButton.addEventListener('click', () => {
audioFilesInput.click();
});
audioFilesInput.addEventListener('change', () => {
selectedFilesListElement.innerHTML = '';
for (const file of audioFilesInput.files) {
const listItem = document.createElement('div');
listItem.textContent = file.name;
selectedFilesListElement.appendChild(listItem);
}
updateCloneVoiceButtonVisibility();
});
cloneVoiceButton.addEventListener('click', async () => {
const voiceName = voiceCloningNameInput.value.trim();
const voiceDescription = voiceCloningDescriptionInput.value.trim();
const voiceLabels = voiceCloningLabelsInput.value.trim();
if (!voiceName) {
toastr.error('Please provide a name for the cloned voice.');
return;
}
try {
await this.addVoice(voiceName, voiceDescription, voiceLabels);
toastr.success('Voice cloned successfully. Hit reload to see the new voice in the voice listing.');
clearSelectedFiles();
voiceCloningNameInput.value = '';
voiceCloningDescriptionInput.value = '';
voiceCloningLabelsInput.value = '';
} catch (error) {
toastr.error(`Failed to clone voice: ${error.message}`);
}
});
updateCloneVoiceButtonVisibility();
}
async updateApiKey() {
// Using this call to validate API key
@@ -206,24 +302,26 @@ class ElevenLabsTtsProvider {
async fetchTtsGeneration(text, voiceId) {
let model = this.settings.model ?? 'eleven_monolingual_v1';
console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: 'POST',
headers: {
'xi-api-key': this.settings.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model_id: model,
text: text,
voice_settings: {
stability: Number(this.settings.stability),
similarity_boost: Number(this.settings.similarity_boost),
},
}),
const request = {
model_id: model,
text: text,
voice_settings: {
stability: Number(this.settings.stability),
similarity_boost: Number(this.settings.similarity_boost),
},
);
};
if (this.shouldInvolveExtendedSettings()) {
request.voice_settings.style_exaggeration = Number(this.settings.style_exaggeration);
request.voice_settings.speaker_boost = Boolean(this.settings.speaker_boost);
}
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
method: 'POST',
headers: {
'xi-api-key': this.settings.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
@@ -260,4 +358,33 @@ class ElevenLabsTtsProvider {
const responseJson = await response.json();
return responseJson.history;
}
async addVoice(name, description, labels) {
const selected_files = document.querySelectorAll('input[type="file"][name="audio_files"]');
const formData = new FormData();
formData.append('name', name);
formData.append('description', description);
formData.append('labels', labels);
for (const file of selected_files) {
if (file.files.length > 0) {
formData.append('files', file.files[0]);
}
}
const response = await fetch('https://api.elevenlabs.io/v1/voices/add', {
method: 'POST',
headers: {
'xi-api-key': this.settings.apiKey,
},
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return await response.json();
}
}

View File

@@ -1,5 +1,5 @@
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, extension_settings, getContext, modules, renderExtensionTemplate } from '../../extensions.js';
import { ModuleWorkerWrapper, extension_settings, getContext, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { collapseNewlines } from '../../power-user.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
@@ -658,7 +658,8 @@ jQuery(async () => {
// Migrate from TensorFlow to Transformers
settings.source = settings.source !== 'local' ? settings.source : 'transformers';
$('#extensions_settings2').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');
$('#extensions_settings2').append(template);
$('#vectors_enabled_chats').prop('checked', settings.enabled_chats).on('input', () => {
settings.enabled_chats = $('#vectors_enabled_chats').prop('checked');
Object.assign(extension_settings.vectors, settings);

View File

@@ -260,6 +260,7 @@ const default_settings = {
use_ai21_tokenizer: false,
use_google_tokenizer: false,
claude_use_sysprompt: false,
use_makersuite_sysprompt: true,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@@ -330,6 +331,7 @@ const oai_settings = {
use_ai21_tokenizer: false,
use_google_tokenizer: false,
claude_use_sysprompt: false,
use_makersuite_sysprompt: true,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@@ -1733,6 +1735,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const stopStringsLimit = 3; // 5 - 2 (nameStopString and new_chat_prompt)
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['stop'] = [nameStopString, substituteParams(oai_settings.new_chat_prompt), ...getCustomStoppingStrings(stopStringsLimit)];
generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;
}
if (isAI21) {
@@ -2668,6 +2671,7 @@ function loadOpenAISettings(data, settings) {
if (settings.use_ai21_tokenizer !== undefined) { oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; }
if (settings.use_google_tokenizer !== undefined) oai_settings.use_google_tokenizer = !!settings.use_google_tokenizer;
if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
@@ -2707,6 +2711,7 @@ function loadOpenAISettings(data, settings) {
$('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer);
$('#use_google_tokenizer').prop('checked', oai_settings.use_google_tokenizer);
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
$('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct);
@@ -2976,6 +2981,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
use_ai21_tokenizer: settings.use_ai21_tokenizer,
use_google_tokenizer: settings.use_google_tokenizer,
claude_use_sysprompt: settings.claude_use_sysprompt,
use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
@@ -3354,6 +3360,7 @@ function onSettingsPresetChange() {
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true],
use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
@@ -4290,6 +4297,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#use_makersuite_sysprompt').on('change', function () {
oai_settings.use_makersuite_sysprompt = !!$('#use_makersuite_sysprompt').prop('checked');
saveSettingsDebounced();
});
$('#send_if_empty_textarea').on('input', function () {
oai_settings.send_if_empty = String($('#send_if_empty_textarea').val());
saveSettingsDebounced();

View File

@@ -404,7 +404,7 @@ function onPersonaDescriptionInput() {
}
$(`.avatar-container[imgfile="${user_avatar}"] .ch_description`)
.text(power_user.persona_description || '[No description]')
.text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text'))
.toggleClass('text_muted', !power_user.persona_description);
saveSettingsDebounced();
}

View File

@@ -13,7 +13,6 @@ import {
printCharactersDebounced,
setCharacterId,
setEditedMessageId,
renderTemplate,
chat,
getFirstDisplayedMessageId,
showMoreMessages,
@@ -39,6 +38,7 @@ import { registerSlashCommand } from './slash-commands.js';
import { tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
@@ -1363,8 +1363,8 @@ export function registerDebugFunction(functionId, name, description, func) {
debug_functions.push({ functionId, name, description, func });
}
function showDebugMenu() {
const template = renderTemplate('debug', { functions: debug_functions });
async function showDebugMenu() {
const template = await renderTemplateAsync('debug', { functions: debug_functions });
callPopup(template, 'text', '', { wide: true, large: true });
}

130
public/scripts/templates.js Normal file
View File

@@ -0,0 +1,130 @@
import { applyLocale } from './i18n.js';
/**
* @type {Map<string, function>}
* @description Cache for Handlebars templates.
*/
const TEMPLATE_CACHE = new Map();
/**
* Loads a URL content using XMLHttpRequest synchronously.
* @param {string} url URL to load synchronously
* @returns {string} Response text
*/
function getUrlSync(url) {
console.debug('Loading URL synchronously', url);
const request = new XMLHttpRequest();
request.open('GET', url, false); // `false` makes the request synchronous
request.send();
if (request.status >= 200 && request.status < 300) {
return request.responseText;
}
throw new Error(`Error loading ${url}: ${request.status} ${request.statusText}`);
}
/**
* Loads a URL content using XMLHttpRequest asynchronously.
* @param {string} url URL to load asynchronously
* @returns {Promise<string>} Response text
*/
function getUrlAsync(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = () => {
if (request.status >= 200 && request.status < 300) {
resolve(request.responseText);
} else {
reject(new Error(`Error loading ${url}: ${request.status} ${request.statusText}`));
}
};
request.onerror = () => {
reject(new Error(`Error loading ${url}: ${request.status} ${request.statusText}`));
};
request.send();
});
}
/**
* Renders a Handlebars template asynchronously.
* @param {string} templateId ID of the template to render
* @param {Record<string, any>} templateData The data to pass to the template
* @param {boolean} sanitize Should the template be sanitized with DOMPurify
* @param {boolean} localize Should the template be localized
* @param {boolean} fullPath Should the template ID be treated as a full path or a relative path
* @returns {Promise<string>} Rendered template
*/
export async function renderTemplateAsync(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
async function fetchTemplateAsync(pathToTemplate) {
let template = TEMPLATE_CACHE.get(pathToTemplate);
if (!template) {
const templateContent = await getUrlAsync(pathToTemplate);
template = Handlebars.compile(templateContent);
TEMPLATE_CACHE.set(pathToTemplate, template);
}
return template;
}
try {
const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
const template = await fetchTemplateAsync(pathToTemplate);
let result = template(templateData);
if (sanitize) {
result = DOMPurify.sanitize(result);
}
if (localize) {
result = applyLocale(result);
}
return result;
} catch (err) {
console.error('Error rendering template', templateId, templateData, err);
toastr.error('Check the DevTools console for more information.', 'Error rendering template');
}
}
/**
* Renders a Handlebars template synchronously.
* @param {string} templateId ID of the template to render
* @param {Record<string, any>} templateData The data to pass to the template
* @param {boolean} sanitize Should the template be sanitized with DOMPurify
* @param {boolean} localize Should the template be localized
* @param {boolean} fullPath Should the template ID be treated as a full path or a relative path
* @returns {string} Rendered template
*
* @deprecated Use renderTemplateAsync instead.
*/
export function renderTemplate(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
function fetchTemplateSync(pathToTemplate) {
let template = TEMPLATE_CACHE.get(pathToTemplate);
if (!template) {
const templateContent = getUrlSync(pathToTemplate);
template = Handlebars.compile(templateContent);
TEMPLATE_CACHE.set(pathToTemplate, template);
}
return template;
}
try {
const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
const template = fetchTemplateSync(pathToTemplate);
let result = template(templateData);
if (sanitize) {
result = DOMPurify.sanitize(result);
}
if (localize) {
result = applyLocale(result);
}
return result;
} catch (err) {
console.error('Error rendering template', templateId, templateData, err);
toastr.error('Check the DevTools console for more information.', 'Error rendering template');
}
}