Merge branch 'staging' into pollinations

This commit is contained in:
Cohee
2024-04-10 21:14:36 +03:00
64 changed files with 1871 additions and 815 deletions

View File

@ -103,7 +103,8 @@ function downloadAssetsList(url) {
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
</div>`);
}
@ -180,6 +181,7 @@ function downloadAssetsList(url) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const assetBlock = $('<i></i>')
@ -187,7 +189,7 @@ function downloadAssetsList(url) {
.append(`<div class="flex-container flexFlowColumn flexNoGap">
<span class="asset-name flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>

View File

@ -30,7 +30,7 @@ function migrateSettings() {
if (extension_settings.caption.source === 'openai') {
extension_settings.caption.source = 'multimodal';
extension_settings.caption.multimodal_api = 'openai';
extension_settings.caption.multimodal_model = 'gpt-4-vision-preview';
extension_settings.caption.multimodal_model = 'gpt-4-turbo';
}
if (!extension_settings.caption.multimodal_api) {
@ -38,7 +38,7 @@ function migrateSettings() {
}
if (!extension_settings.caption.multimodal_model) {
extension_settings.caption.multimodal_model = 'gpt-4-vision-preview';
extension_settings.caption.multimodal_model = 'gpt-4-turbo';
}
if (!extension_settings.caption.prompt) {
@ -369,6 +369,7 @@ jQuery(function () {
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>

View File

@ -11,7 +11,7 @@ const MODULE_NAME = 'expressions';
const UPDATE_INTERVAL = 2000;
const STREAMING_UPDATE_INTERVAL = 6000;
const TALKINGCHECK_UPDATE_INTERVAL = 500;
const FALLBACK_EXPRESSION = 'joy';
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
const DEFAULT_EXPRESSIONS = [
'talkinghead',
'admiration',
@ -58,6 +58,14 @@ function isTalkingHeadEnabled() {
return extension_settings.expressions.talkinghead && !extension_settings.expressions.local;
}
/**
* Returns the fallback expression if explicitly chosen, otherwise the default one
* @returns {string} expression name
*/
function getFallbackExpression() {
return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION;
}
/**
* Toggles Talkinghead mode on/off.
*
@ -157,7 +165,8 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
const sprites = spriteCache[spriteFolderName];
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path;
const defaultExpression = getFallbackExpression();
const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
const noSprites = sprites.length === 0;
if (expressionImage.length > 0) {
@ -568,7 +577,7 @@ function handleImageChange() {
// This preserves the same expression Talkinghead had at the moment it was switched off.
const charName = getContext().name2;
const last = lastExpression[charName];
const targetExpression = last ? last : FALLBACK_EXPRESSION;
const targetExpression = last ? last : getFallbackExpression();
setExpression(charName, targetExpression, true);
}
}
@ -691,8 +700,8 @@ async function moduleWorker() {
const force = !!context.groupId;
// Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) {
expression = FALLBACK_EXPRESSION;
if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression())) {
expression = getFallbackExpression();
}
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
@ -885,6 +894,22 @@ async function setSpriteSetCommand(_, folder) {
moduleWorker();
}
async function classifyCommand(_, text) {
if (!text) {
console.log('No text provided');
return '';
}
if (!modules.includes('classify') && !extension_settings.expressions.local) {
toastr.warning('Text classification is disabled or not available');
return '';
}
const label = getExpressionLabel(text);
console.debug(`Classification result for "${text}": ${label}`);
return label;
}
async function setSpriteSlashCommand(_, spriteId) {
if (!spriteId) {
console.log('No sprite id provided');
@ -949,7 +974,7 @@ function sampleClassifyText(text) {
async function getExpressionLabel(text) {
// Return if text is undefined, saving a costly fetch request
if ((!modules.includes('classify') && !extension_settings.expressions.local) || !text) {
return FALLBACK_EXPRESSION;
return getFallbackExpression();
}
text = sampleClassifyText(text);
@ -988,7 +1013,7 @@ async function getExpressionLabel(text) {
}
} catch (error) {
console.log(error);
return FALLBACK_EXPRESSION;
return getFallbackExpression();
}
}
@ -1092,6 +1117,11 @@ async function getSpritesList(name) {
}
}
async function renderAdditionalExpressionSettings() {
renderCustomExpressions();
await renderFallbackExpressionPicker();
}
function renderCustomExpressions() {
if (!Array.isArray(extension_settings.expressions.custom)) {
extension_settings.expressions.custom = [];
@ -1112,6 +1142,23 @@ function renderCustomExpressions() {
}
}
async function renderFallbackExpressionPicker() {
const expressions = await getExpressionsList();
const defaultPicker = $('#expression_fallback');
defaultPicker.empty();
const fallbackExpression = getFallbackExpression();
for (const expression of expressions) {
const option = document.createElement('option');
option.value = expression;
option.text = expression;
option.selected = expression == fallbackExpression;
defaultPicker.append(option);
}
}
async function getExpressionsList() {
// Return cached list if available
if (Array.isArray(expressionsList)) {
@ -1349,7 +1396,7 @@ async function onClickExpressionAddCustom() {
// Add custom expression into settings
extension_settings.expressions.custom.push(expressionName);
renderCustomExpressions();
await renderAdditionalExpressionSettings();
saveSettingsDebounced();
// Force refresh sprites list
@ -1376,7 +1423,11 @@ async function onClickExpressionRemoveCustom() {
// Remove custom expression from settings
const index = extension_settings.expressions.custom.indexOf(selectedExpression);
extension_settings.expressions.custom.splice(index, 1);
renderCustomExpressions();
if (selectedExpression == getFallbackExpression()) {
toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`);
extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION;
}
await renderAdditionalExpressionSettings();
saveSettingsDebounced();
// Force refresh sprites list
@ -1385,6 +1436,14 @@ async function onClickExpressionRemoveCustom() {
moduleWorker();
}
function onExpressionFallbackChanged() {
const expression = this.value;
if (expression) {
extension_settings.expressions.fallback_expression = expression;
saveSettingsDebounced();
}
}
async function handleFileUpload(url, formData) {
try {
const data = await jQuery.ajax({
@ -1632,7 +1691,7 @@ async function fetchImagesNoCache() {
return await Promise.allSettled(promises);
}
(function () {
(async function () {
function addExpressionImage() {
const html = `
<div id="expression-wrapper">
@ -1652,7 +1711,7 @@ async function fetchImagesNoCache() {
element.hide();
$('body').append(element);
}
function addSettings() {
async function addSettings() {
$('#extensions_settings').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
@ -1680,10 +1739,11 @@ async function fetchImagesNoCache() {
}
});
renderCustomExpressions();
await renderAdditionalExpressionSettings();
$('#expression_custom_add').on('click', onClickExpressionAddCustom);
$('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
$('#expression_fallback').on('change', onExpressionFallbackChanged)
}
// Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.
@ -1716,7 +1776,7 @@ async function fetchImagesNoCache() {
addExpressionImage();
addVisualNovelMode();
addSettings();
await addSettings();
const wrapper = new ModuleWorkerWrapper(moduleWorker);
const updateFunction = wrapper.update.bind(wrapper);
setInterval(updateFunction, UPDATE_INTERVAL);
@ -1758,5 +1818,6 @@ async function fetchImagesNoCache() {
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> force sets the sprite for the current character', true, true);
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);
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> Returns the last set sprite / expression for the named character.', true, true);
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], ' Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.');
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], ' Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', true, true);
registerSlashCommand('classify', classifyCommand, [], '<span class="monospace">(text)</span> performs an emotion classification of the given text and returns a label.', true, true);
})();

View File

@ -18,6 +18,11 @@
<input id="image_type_toggle" type="checkbox">
<span>Image Type - talkinghead (extras)</span>
</label>
<div class="expression_fallback_block m-b-1 m-t-1">
<label for="expression_fallback">Default / Fallback Expression</label>
<small>Set the default and fallback expression being used when no matching expression is found.</small>
<select id="expression_fallback" class="flex1 margin0" data-i18n="Fallback Expression" placeholder="Fallback Expression"></select>
</div>
<div class="expression_custom_block m-b-1 m-t-1">
<label for="expression_custom">Custom Expressions</label>
<small>Can be set manually or with an <tt>/emote</tt> slash command.</small>

View File

@ -16,12 +16,20 @@
<label for="qr--modal-message">
Message / Command:
</label>
<small>
<div class="qr--modal-editorSettings">
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-wrap">
<span>Word wrap</span>
</label>
</small>
<label class="checkbox_label">
<span>Tab size:</span>
<input type="number" min="1" max="9" id="qr--modal-tabSize" class="text_pole">
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeShortcut">
<span>Ctrl+Enter to execute</span>
</label>
</div>
<textarea class="monospace" id="qr--modal-message"></textarea>
</div>
</div>
@ -78,7 +86,7 @@
<input type="checkbox" id="qr--executeOnGroupMemberDraft">
<span><i class="fa-solid fa-fw fa-people-group"></i> Execute before group member message</span>
</label>
<div class="flex-container alignItemsBaseline" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
<div class="flex-container alignItemsBaseline flexFlowColumn flexNoGap" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
<small>Automation ID</small>
<input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )">
</div>

View File

@ -104,7 +104,7 @@ const loadSets = async () => {
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
qr.automationId = slot.automationId ?? false;
qr.automationId = slot.automationId ?? '';
qr.contextList = (slot.contextMenu ?? []).map(it=>({
set: it.preset,
isChained: it.chain,

View File

@ -1,4 +1,4 @@
import { callPopup } from '../../../../script.js';
import { POPUP_TYPE, Popup } from '../../../popup.js';
import { getSortableDelay } from '../../../utils.js';
import { log, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@ -44,6 +44,13 @@ export class QuickReply {
/**@type {HTMLInputElement}*/ settingsDomLabel;
/**@type {HTMLTextAreaElement}*/ settingsDomMessage;
/**@type {Popup}*/ editorPopup;
/**@type {HTMLElement}*/ editorExecuteBtn;
/**@type {HTMLElement}*/ editorExecuteErrors;
/**@type {HTMLInputElement}*/ editorExecuteHide;
/**@type {Promise}*/ editorExecutePromise;
get hasContext() {
return this.contextList && this.contextList.length > 0;
@ -192,7 +199,8 @@ export class QuickReply {
/**@type {HTMLElement} */
// @ts-ignore
const dom = this.template.cloneNode(true);
const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
const popupResult = this.editorPopup.show();
// basics
/**@type {HTMLInputElement}*/
@ -209,7 +217,7 @@ export class QuickReply {
});
/**@type {HTMLInputElement}*/
const wrap = dom.querySelector('#qr--modal-wrap');
wrap.checked = JSON.parse(localStorage.getItem('qr--wrap'));
wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
wrap.addEventListener('click', () => {
localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
updateWrap();
@ -221,9 +229,26 @@ export class QuickReply {
message.style.whiteSpace = 'pre';
}
};
/**@type {HTMLInputElement}*/
const tabSize = dom.querySelector('#qr--modal-tabSize');
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
const updateTabSize = () => {
message.style.tabSize = tabSize.value;
};
tabSize.addEventListener('change', () => {
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
updateTabSize();
});
/**@type {HTMLInputElement}*/
const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true');
executeShortcut.addEventListener('click', () => {
localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
});
/**@type {HTMLTextAreaElement}*/
const message = dom.querySelector('#qr--modal-message');
updateWrap();
updateTabSize();
message.value = this.message;
message.addEventListener('input', () => {
this.updateMessage(message.value);
@ -257,6 +282,12 @@ export class QuickReply {
message.selectionStart = start - 1;
message.selectionEnd = end - count;
this.updateMessage(message.value);
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
evt.stopPropagation();
evt.preventDefault();
if (executeShortcut.checked) {
this.executeFromEditor();
}
}
});
@ -385,27 +416,15 @@ export class QuickReply {
/**@type {HTMLElement}*/
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
this.editorExecuteErrors = executeErrors;
/**@type {HTMLInputElement}*/
const executeHide = dom.querySelector('#qr--modal-executeHide');
let executePromise;
this.editorExecuteHide = executeHide;
/**@type {HTMLElement}*/
const executeBtn = dom.querySelector('#qr--modal-execute');
this.editorExecuteBtn = executeBtn;
executeBtn.addEventListener('click', async()=>{
if (executePromise) return;
executeBtn.classList.add('qr--busy');
executeErrors.innerHTML = '';
if (executeHide.checked) {
document.querySelector('#shadow_popup').classList.add('qr--hide');
}
try {
executePromise = this.execute();
await executePromise;
} catch (ex) {
executeErrors.textContent = ex.message;
}
executePromise = null;
executeBtn.classList.remove('qr--busy');
document.querySelector('#shadow_popup').classList.remove('qr--hide');
await this.executeFromEditor();
});
await popupResult;
@ -414,6 +433,24 @@ export class QuickReply {
}
}
async executeFromEditor() {
if (this.editorExecutePromise) return;
this.editorExecuteBtn.classList.add('qr--busy');
this.editorExecuteErrors.innerHTML = '';
if (this.editorExecuteHide.checked) {
this.editorPopup.dom.classList.add('qr--hide');
}
try {
this.editorExecutePromise = this.execute();
await this.editorExecutePromise;
} catch (ex) {
this.editorExecuteErrors.textContent = ex.message;
}
this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy');
this.editorPopup.dom.classList.remove('qr--hide');
}

View File

@ -216,71 +216,85 @@
align-items: baseline;
}
@media screen and (max-width: 750px) {
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
}
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex-direction: column;
}
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
min-height: 90svh;
}
}
#dialogue_popup:has(#qr--modalEditor) {
.dialogue_popup:has(#qr--modalEditor) {
aspect-ratio: unset;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text {
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
flex: 1 1 auto;
display: flex;
flex-direction: row;
gap: 1em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
flex: 0 0 auto;
display: flex;
flex-direction: row;
gap: 0.5em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input {
flex: 0 0 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
display: flex;
flex-direction: row;
gap: 1em;
color: var(--grey70);
font-size: smaller;
align-items: baseline;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label {
white-space: nowrap;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
font-size: inherit;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
flex: 1 1 auto;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
display: flex;
flex-direction: row;
gap: 0.5em;
}
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
opacity: 0.5;
cursor: wait;
}
#shadow_popup.qr--hide {
.shadow_popup.qr--hide {
opacity: 0 !important;
}

View File

@ -242,7 +242,7 @@
@media screen and (max-width: 750px) {
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
flex-direction: column;
> #qr--main > .qr--labels {
flex-direction: column;
@ -252,10 +252,10 @@
}
}
}
#dialogue_popup:has(#qr--modalEditor) {
.dialogue_popup:has(#qr--modalEditor) {
aspect-ratio: unset;
#dialogue_popup_text {
.dialogue_popup_text {
display: flex;
flex-direction: column;
@ -293,6 +293,20 @@
flex: 1 1 auto;
display: flex;
flex-direction: column;
> .qr--modal-editorSettings {
display: flex;
flex-direction: row;
gap: 1em;
color: var(--grey70);
font-size: smaller;
align-items: baseline;
> .checkbox_label {
white-space: nowrap;
> input {
font-size: inherit;
}
}
}
> #qr--modal-message {
flex: 1 1 auto;
}
@ -312,6 +326,6 @@
}
}
#shadow_popup.qr--hide {
.shadow_popup.qr--hide {
opacity: 0 !important;
}

View File

@ -56,7 +56,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
if (!isGoogle) {
requestBody.api = extension_settings.caption.multimodal_api || 'openai';
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-vision-preview';
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-turbo';
requestBody.reverse_proxy = proxyUrl;
requestBody.proxy_password = proxyPassword;
}
@ -83,7 +83,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
if (isCustom) {
requestBody.server_url = oai_settings.custom_url;
requestBody.model = oai_settings.custom_model || 'gpt-4-vision-preview';
requestBody.model = oai_settings.custom_model || 'gpt-4-turbo';
requestBody.custom_include_headers = oai_settings.custom_include_headers;
requestBody.custom_include_body = oai_settings.custom_include_body;
requestBody.custom_exclude_body = oai_settings.custom_exclude_body;

View File

@ -2629,7 +2629,7 @@ async function generateComfyImage(prompt, negativePrompt) {
}
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
placeholders.forEach(ph => {
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
});

View File

@ -19,8 +19,9 @@ const UPDATE_INTERVAL = 1000;
let voiceMapEntries = [];
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
let storedvalue = false;
let talkingHeadState = false;
let lastChatId = null;
let lastMessage = null;
let lastMessageHash = null;
const DEFAULT_VOICE_MARKER = '[Default Voice]';
@ -67,7 +68,7 @@ export function getPreviewString(lang) {
return previewStrings[lang] ?? fallbackPreview;
}
let ttsProviders = {
const ttsProviders = {
ElevenLabs: ElevenLabsTtsProvider,
Silero: SileroTtsProvider,
XTTSv2: XTTSTtsProvider,
@ -82,7 +83,6 @@ let ttsProviders = {
let ttsProvider;
let ttsProviderName;
let ttsLastMessage = null;
async function onNarrateOneMessage() {
audioElement.src = '/sounds/silence.mp3';
@ -130,103 +130,13 @@ async function onNarrateText(args, text) {
}
async function moduleWorker() {
// Primarily determining when to add new chat to the TTS queue
const enabled = $('#tts_enabled').is(':checked');
$('body').toggleClass('tts', enabled);
if (!enabled) {
if (!extension_settings.tts.enabled) {
return;
}
const context = getContext();
const chat = context.chat;
processTtsQueue();
processAudioJobQueue();
updateUiAudioPlayState();
// Auto generation is disabled
if (extension_settings.tts.auto_generation == false) {
return;
}
// no characters or group selected
if (!context.groupId && context.characterId === undefined) {
return;
}
// Chat changed
if (
context.chatId !== lastChatId
) {
currentMessageNumber = context.chat.length ? context.chat.length : 0;
saveLastValues();
// Force to speak on the first message in the new chat
if (context.chat.length === 1) {
lastMessageHash = -1;
}
return;
}
// take the count of messages
let lastMessageNumber = context.chat.length ? context.chat.length : 0;
// There's no new messages
let diff = lastMessageNumber - currentMessageNumber;
let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '');
// if messages got deleted, diff will be < 0
if (diff < 0) {
// necessary actions will be taken by the onChatDeleted() handler
return;
}
// if no new messages, or same message, or same message hash, do nothing
if (diff == 0 && hashNew === lastMessageHash) {
return;
}
// If streaming, wait for streaming to finish before processing new messages
if (context.streamingProcessor && !context.streamingProcessor.isFinished) {
return;
}
// clone message object, as things go haywire if message object is altered below (it's passed by reference)
const message = structuredClone(chat[chat.length - 1]);
// if last message within current message, message got extended. only send diff to TTS.
if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) {
let tmp = message.mes;
message.mes = message.mes.replace(ttsLastMessage, '');
ttsLastMessage = tmp;
} else {
ttsLastMessage = message.mes;
}
// We're currently swiping. Don't generate voice
if (!message || message.mes === '...' || message.mes === '') {
return;
}
// Don't generate if message doesn't have a display text
if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) {
return;
}
// Don't generate if message is a user message and user message narration is disabled
if (message.is_user && !extension_settings.tts.narrate_user) {
return;
}
// New messages, add new chat to history
lastMessageHash = hashNew;
currentMessageNumber = lastMessageNumber;
console.debug(
`Adding message from ${message.name} for TTS processing: "${message.mes}"`,
);
ttsJobQueue.push(message);
}
function talkingAnimation(switchValue) {
@ -238,11 +148,11 @@ function talkingAnimation(switchValue) {
const apiUrl = getApiUrl();
const animationType = switchValue ? 'start' : 'stop';
if (switchValue !== storedvalue) {
if (switchValue !== talkingHeadState) {
try {
console.log(animationType + ' Talking Animation');
doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
storedvalue = switchValue; // Update the storedvalue to the current switchValue
talkingHeadState = switchValue;
} catch (error) {
// Handle the error here or simply ignore it to prevent logging
}
@ -289,7 +199,6 @@ function debugTtsPlayback() {
{
'ttsProviderName': ttsProviderName,
'voiceMap': voiceMap,
'currentMessageNumber': currentMessageNumber,
'audioPaused': audioPaused,
'audioJobQueue': audioJobQueue,
'currentAudioJob': currentAudioJob,
@ -465,6 +374,7 @@ async function processAudioJobQueue() {
playAudioData(currentAudioJob);
talkingAnimation(true);
} catch (error) {
toastr.error(error.toString());
console.error(error);
audioQueueProcessorReady = true;
}
@ -476,21 +386,12 @@ async function processAudioJobQueue() {
let ttsJobQueue = [];
let currentTtsJob; // Null if nothing is currently being processed
let currentMessageNumber = 0;
function completeTtsJob() {
console.info(`Current TTS job for ${currentTtsJob?.name} completed.`);
currentTtsJob = null;
}
function saveLastValues() {
const context = getContext();
lastChatId = context.chatId;
lastMessageHash = getStringHash(
(context.chat.length && context.chat[context.chat.length - 1].mes) ?? '',
);
}
async function tts(text, voiceId, char) {
async function processResponse(response) {
// RVC injection
@ -581,8 +482,9 @@ async function processTtsQueue() {
toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`);
throw `Unable to attain voiceId for ${char}`;
}
tts(text, voiceId, char);
await tts(text, voiceId, char);
} catch (error) {
toastr.error(error.toString());
console.error(error);
currentTtsJob = null;
}
@ -654,6 +556,7 @@ function onRefreshClick() {
initVoiceMap();
updateVoiceMap();
}).catch(error => {
toastr.error(error.toString());
console.error(error);
setTtsStatus(error, false);
});
@ -761,26 +664,103 @@ async function onChatChanged() {
await resetTtsPlayback();
const voiceMapInit = initVoiceMap();
await Promise.race([voiceMapInit, delay(1000)]);
ttsLastMessage = null;
lastMessage = null;
}
async function onChatDeleted() {
async function onMessageEvent(messageId) {
// If TTS is disabled, do nothing
if (!extension_settings.tts.enabled) {
return;
}
// Auto generation is disabled
if (!extension_settings.tts.auto_generation) {
return;
}
const context = getContext();
// no characters or group selected
if (!context.groupId && context.characterId === undefined) {
return;
}
// Chat changed
if (context.chatId !== lastChatId) {
lastChatId = context.chatId;
lastMessageHash = getStringHash(context.chat[messageId]?.mes ?? '');
// Force to speak on the first message in the new chat
if (context.chat.length === 1) {
lastMessageHash = -1;
}
}
// clone message object, as things go haywire if message object is altered below (it's passed by reference)
const message = structuredClone(context.chat[messageId]);
const hashNew = getStringHash(message?.mes ?? '');
// if no new messages, or same message, or same message hash, do nothing
if (hashNew === lastMessageHash) {
return;
}
const isLastMessageInCurrent = () =>
lastMessage &&
typeof lastMessage === 'object' &&
message.swipe_id === lastMessage.swipe_id &&
message.name === lastMessage.name &&
message.is_user === lastMessage.is_user &&
message.mes.indexOf(lastMessage.mes) !== -1;
// if last message within current message, message got extended. only send diff to TTS.
if (isLastMessageInCurrent()) {
const tmp = structuredClone(message);
message.mes = message.mes.replace(lastMessage.mes, '');
lastMessage = tmp;
} else {
lastMessage = structuredClone(message);
}
// We're currently swiping. Don't generate voice
if (!message || message.mes === '...' || message.mes === '') {
return;
}
// Don't generate if message doesn't have a display text
if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) {
return;
}
// Don't generate if message is a user message and user message narration is disabled
if (message.is_user && !extension_settings.tts.narrate_user) {
return;
}
// New messages, add new chat to history
lastMessageHash = hashNew;
lastChatId = context.chatId;
console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
ttsJobQueue.push(message);
}
async function onMessageDeleted() {
const context = getContext();
// update internal references to new last message
lastChatId = context.chatId;
currentMessageNumber = context.chat.length ? context.chat.length : 0;
// compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
const messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
if (messageHash === lastMessageHash) {
return;
}
lastMessageHash = messageHash;
ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '';
lastMessage = context.chat.length ? structuredClone(context.chat[context.chat.length - 1]) : null;
// stop any tts playback since message might not exist anymore
await resetTtsPlayback();
resetTtsPlayback();
}
/**
@ -1076,8 +1056,10 @@ $(document).ready(function () {
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.MESSAGE_DELETED, onChatDeleted);
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], '<span class="monospace">(text)</span> narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt>', true, true);
document.body.appendChild(audioElement);
});