Merge branch 'staging' into l18n-tw
This commit is contained in:
commit
abafdadf33
|
@ -67,8 +67,10 @@ jobs:
|
|||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Release version tag if the workflow is triggered by a release
|
||||
# Branch name tag if the workflow is triggered by a push
|
||||
# Latest tag if the branch is release and the workflow is triggered by a push
|
||||
tags: |
|
||||
${{ github.event_name == 'release' && github.ref_name || env.BRANCH_NAME }}
|
||||
${{ github.event_name == 'push' && env.BRANCH_NAME == 'release' && 'latest' || '' }}
|
||||
|
||||
# Login into package repository as the person who created the release
|
||||
- name: Log in to the Container registry
|
||||
|
@ -90,11 +92,3 @@ jobs:
|
|||
push: true
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
# If the workflow is triggered by a release, marks and push the image as such
|
||||
- name: Docker tag latest and push
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Looking for setting snapshots or chat backups?
|
||||
|
||||
Individual user backups are now located in the data directory.
|
||||
|
||||
Example for the default user under default data root:
|
||||
|
||||
/data/default-user/backups
|
||||
|
||||
This folder remains for historical purposes only.
|
|
@ -12,7 +12,7 @@
|
|||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"@zeldafan0225/ai_horde": "^5.1.0",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
|
@ -880,12 +880,14 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@zeldafan0225/ai_horde": {
|
||||
"version": "4.0.1",
|
||||
"license": "MIT",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.1.0.tgz",
|
||||
"integrity": "sha512-rPC0nmmFSXK808Oon0zFPA7yGSUKBXiLtMejkmKTyfAzzOHHQt/i2lO4ccfN2e355LzX1lBLwSi+nlATVA43Sw==",
|
||||
"dependencies": {
|
||||
"@thunder04/supermap": "^3.0.2",
|
||||
"centra": "^2.5.0",
|
||||
"esbuild": "^0.12.28"
|
||||
"@thunder04/supermap": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
|
@ -2122,12 +2124,6 @@
|
|||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"name": "dry-uninstall",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dry-uninstall/-/dry-uninstall-0.3.0.tgz",
|
||||
"integrity": "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"@zeldafan0225/ai_horde": "^5.1.0",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
|
@ -44,7 +44,7 @@
|
|||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"overrides": {
|
||||
"parse-bmfont-xml": {
|
||||
|
@ -59,9 +59,6 @@
|
|||
"axios": {
|
||||
"follow-redirects": "^1.15.4"
|
||||
},
|
||||
"@zeldafan0225/ai_horde": {
|
||||
"esbuild": "npm:dry-uninstall"
|
||||
},
|
||||
"node-fetch": {
|
||||
"whatwg-url": "^14.0.0"
|
||||
}
|
||||
|
|
|
@ -1137,7 +1137,7 @@
|
|||
<div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div>
|
||||
</small>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="mancer, vllm, aphrodite" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="mancer, vllm, aphrodite, tabby" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
|
||||
<input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" />
|
||||
</div>
|
||||
|
@ -1739,7 +1739,7 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom">
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq">
|
||||
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_function_calling" type="checkbox" />
|
||||
<span data-i18n="Enable function calling">Enable function calling</span>
|
||||
|
|
|
@ -96,7 +96,7 @@ EventEmitter.prototype.removeListener = function (event, listener) {
|
|||
|
||||
EventEmitter.prototype.emit = async function (event) {
|
||||
if (localStorage.getItem('eventTracing') === 'true') {
|
||||
console.trace('Event emitted: ' + event);
|
||||
console.trace('Event emitted: ' + event, args);
|
||||
} else {
|
||||
console.debug('Event emitted: ' + event);
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ EventEmitter.prototype.emit = async function (event) {
|
|||
|
||||
EventEmitter.prototype.emitAndWait = function (event) {
|
||||
if (localStorage.getItem('eventTracing') === 'true') {
|
||||
console.trace('Event emitted: ' + event);
|
||||
console.trace('Event emitted: ' + event, args);
|
||||
} else {
|
||||
console.debug('Event emitted: ' + event);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
parseTabbyLogprobs,
|
||||
} from './scripts/textgen-settings.js';
|
||||
|
||||
const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types;
|
||||
const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types;
|
||||
|
||||
import {
|
||||
world_info,
|
||||
|
@ -5005,7 +5005,7 @@ function extractMultiSwipes(data, type) {
|
|||
return swipes;
|
||||
}
|
||||
|
||||
if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [MANCER, VLLM, APHRODITE].includes(textgen_settings.type))) {
|
||||
if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [MANCER, VLLM, APHRODITE, TABBY].includes(textgen_settings.type))) {
|
||||
if (!Array.isArray(data.choices)) {
|
||||
return swipes;
|
||||
}
|
||||
|
|
|
@ -1348,11 +1348,11 @@ class PromptManager {
|
|||
const promptManagerDiv = this.containerElement;
|
||||
promptManagerDiv.innerHTML = '';
|
||||
|
||||
const errorDiv = `
|
||||
const errorDiv = this.error ? `
|
||||
<div class="${this.configuration.prefix}prompt_manager_error">
|
||||
<span class="fa-solid tooltip fa-triangle-exclamation text_danger"></span> ${DOMPurify.sanitize(this.error)}
|
||||
</div>
|
||||
`;
|
||||
` : '';
|
||||
|
||||
const totalActiveTokens = this.tokenUsage;
|
||||
|
||||
|
|
|
@ -463,33 +463,50 @@ export function encodeStyleTags(text) {
|
|||
*/
|
||||
export function decodeStyleTags(text) {
|
||||
const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/gms;
|
||||
const mediaAllowed = isExternalMediaAllowed();
|
||||
|
||||
function sanitizeRule(rule) {
|
||||
if (Array.isArray(rule.selectors)) {
|
||||
for (let i = 0; i < rule.selectors.length; i++) {
|
||||
const selector = rule.selectors[i];
|
||||
if (selector) {
|
||||
const selectors = (selector.split(' ') ?? []).map((v) => {
|
||||
if (v.startsWith('.')) {
|
||||
return '.custom-' + v.substring(1);
|
||||
}
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) {
|
||||
rule.declarations = rule.declarations.filter(declaration => !declaration.value.includes('://'));
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeRuleSet(ruleSet) {
|
||||
if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) {
|
||||
sanitizeRule(ruleSet);
|
||||
}
|
||||
|
||||
if (Array.isArray(ruleSet.rules)) {
|
||||
ruleSet.rules = ruleSet.rules.filter(rule => rule.type !== 'import');
|
||||
|
||||
for (const mediaRule of ruleSet.rules) {
|
||||
sanitizeRuleSet(mediaRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text.replaceAll(styleDecodeRegex, (_, style) => {
|
||||
try {
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
const ast = css.parse(styleCleaned);
|
||||
const rules = ast?.stylesheet?.rules;
|
||||
if (rules) {
|
||||
for (const rule of rules) {
|
||||
|
||||
if (rule.type === 'rule') {
|
||||
if (rule.selectors) {
|
||||
for (let i = 0; i < rule.selectors.length; i++) {
|
||||
let selector = rule.selectors[i];
|
||||
if (selector) {
|
||||
let selectors = (selector.split(' ') ?? []).map((v) => {
|
||||
if (v.startsWith('.')) {
|
||||
return '.custom-' + v.substring(1);
|
||||
}
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const sheet = ast?.stylesheet;
|
||||
if (sheet) {
|
||||
sanitizeRuleSet(ast.stylesheet);
|
||||
}
|
||||
return `<style>${css.stringify(ast)}</style>`;
|
||||
} catch (error) {
|
||||
|
@ -751,7 +768,7 @@ async function moveAttachment(attachment, source, callback) {
|
|||
* @param {boolean} [confirm=true] If true, show a confirmation dialog
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||
*/
|
||||
async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
export async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
if (confirm) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
|
@ -838,6 +855,12 @@ async function openAttachmentManager() {
|
|||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
|
||||
};
|
||||
|
||||
const selected = template
|
||||
.find(sources[source])
|
||||
.find('.attachmentListItemCheckbox:checked')
|
||||
.map((_, el) => $(el).closest('.attachmentListItem').attr('data-attachment-url'))
|
||||
.get();
|
||||
|
||||
template.find(sources[source]).empty();
|
||||
|
||||
// Sort attachments by sortField and sortOrder, and apply filter
|
||||
|
@ -847,6 +870,8 @@ async function openAttachmentManager() {
|
|||
const isDisabled = isAttachmentDisabled(attachment);
|
||||
const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
|
||||
attachmentTemplate.toggleClass('disabled', isDisabled);
|
||||
attachmentTemplate.attr('data-attachment-url', attachment.url);
|
||||
attachmentTemplate.attr('data-attachment-source', source);
|
||||
attachmentTemplate.find('.attachmentFileIcon').attr('title', attachment.url);
|
||||
attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
|
||||
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||
|
@ -859,6 +884,10 @@ async function openAttachmentManager() {
|
|||
attachmentTemplate.find('.enableAttachmentButton').toggle(isDisabled).on('click', () => enableAttachment(attachment, renderAttachments));
|
||||
attachmentTemplate.find('.disableAttachmentButton').toggle(!isDisabled).on('click', () => disableAttachment(attachment, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
|
||||
if (selected.includes(attachment.url)) {
|
||||
attachmentTemplate.find('.attachmentListItemCheckbox').prop('checked', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1027,6 +1056,57 @@ async function openAttachmentManager() {
|
|||
localStorage.setItem('DataBank_sortOrder', sortOrder);
|
||||
renderAttachments();
|
||||
});
|
||||
template.find('.bulkActionDelete').on('click', async () => {
|
||||
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
|
||||
|
||||
if (selectedAttachments.length === 0) {
|
||||
toastr.info('No attachments selected.', 'Data Bank');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callGenericPopup('Are you sure you want to delete the selected attachments?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = getDataBankAttachments();
|
||||
selectedAttachments.forEach(async (checkbox) => {
|
||||
const listItem = checkbox.closest('.attachmentListItem');
|
||||
if (!(listItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const url = listItem.dataset.attachmentUrl;
|
||||
const source = listItem.dataset.attachmentSource;
|
||||
const attachment = attachments.find(a => a.url === url);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
await deleteAttachment(attachment, source, () => {}, false);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
await renderAttachments();
|
||||
});
|
||||
template.find('.bulkActionSelectAll').on('click', () => {
|
||||
$('.attachmentListItemCheckbox:visible').each((_, checkbox) => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
template.find('.bulkActionSelectNone').on('click', () => {
|
||||
$('.attachmentListItemCheckbox:visible').each((_, checkbox) => {
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
|
@ -1099,7 +1179,7 @@ async function runScraper(scraperId, target, callback) {
|
|||
* Uploads a file attachment to the server.
|
||||
* @param {File} file File to upload
|
||||
* @param {string} target Target for the attachment
|
||||
* @returns
|
||||
* @returns {Promise<string>} Path to the uploaded file
|
||||
*/
|
||||
export async function uploadFileAttachmentToServer(file, target) {
|
||||
const isValid = await validateFile(file);
|
||||
|
@ -1156,6 +1236,8 @@ export async function uploadFileAttachmentToServer(file, target) {
|
|||
saveSettingsDebounced();
|
||||
break;
|
||||
}
|
||||
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
function ensureAttachmentsExist() {
|
||||
|
@ -1183,36 +1265,42 @@ function ensureAttachmentsExist() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available attachments. Ignores disabled attachments.
|
||||
* Gets all currently available attachments. Ignores disabled attachments by default.
|
||||
* @param {boolean} [includeDisabled=false] If true, include disabled attachments
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachments() {
|
||||
export function getDataBankAttachments(includeDisabled = false) {
|
||||
ensureAttachmentsExist();
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => !isAttachmentDisabled(x));
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => includeDisabled || !isAttachmentDisabled(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attachments for a specific source. Includes disabled attachments.
|
||||
* Gets all attachments for a specific source. Includes disabled attachments by default.
|
||||
* @param {string} source Attachment source
|
||||
* @param {boolean} [includeDisabled=true] If true, include disabled attachments
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachmentsForSource(source) {
|
||||
export function getDataBankAttachmentsForSource(source, includeDisabled = true) {
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
function getBySource() {
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
return getBySource().filter(x => includeDisabled || !isAttachmentDisabled(x));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1374,6 +1462,7 @@ jQuery(function () {
|
|||
});
|
||||
|
||||
$(document).on('click', 'body.documentstyle .mes .mes_text', function () {
|
||||
if (window.getSelection().toString()) return;
|
||||
if ($('.edit_textarea').length) return;
|
||||
$(this).closest('.mes').find('.mes_edit').trigger('click');
|
||||
});
|
||||
|
|
|
@ -1,15 +1,301 @@
|
|||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
|
||||
/**
|
||||
* List of attachment sources
|
||||
* @type {string[]}
|
||||
*/
|
||||
const TYPES = ['global', 'character', 'chat'];
|
||||
const FIELDS = ['name', 'url'];
|
||||
|
||||
/**
|
||||
* Get attachments from the data bank. Includes disabled attachments.
|
||||
* @param {string} [source] Source for the attachments
|
||||
* @returns {import('../../chats').FileAttachment[]} List of attachments
|
||||
*/
|
||||
function getAttachments(source) {
|
||||
if (!source || !TYPES.includes(source)) {
|
||||
return getDataBankAttachments(true);
|
||||
}
|
||||
|
||||
return getDataBankAttachmentsForSource(source, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment by a single name or URL.
|
||||
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {import('../../chats').FileAttachment} Attachment
|
||||
*/
|
||||
function getAttachmentByField(attachments, value) {
|
||||
const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase();
|
||||
const fullMatchByURL = attachments.find(it => match(it.url));
|
||||
const fullMatchByName = attachments.find(it => match(it.name));
|
||||
return fullMatchByURL || fullMatchByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment by multiple fields.
|
||||
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
|
||||
* @param {string[]} values Name and URL of the attachment to search for
|
||||
* @returns
|
||||
*/
|
||||
function getAttachmentByFields(attachments, values) {
|
||||
for (const value of values) {
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
if (attachment) {
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for listing attachments in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @returns {string} JSON string of the list of attachments
|
||||
*/
|
||||
function listDataBankAttachments(args) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const field = args?.field;
|
||||
return JSON.stringify(attachments.map(a => FIELDS.includes(field) ? a[field] : a.url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for getting text from an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Content of the attachment
|
||||
*/
|
||||
async function getDataBankText(args, value) {
|
||||
if (!value) {
|
||||
toastr.warning('No attachment name or URL provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await getFileAttachment(attachment.url);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for adding an attachment to the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Content of the attachment
|
||||
* @returns {Promise<string>} URL of the attachment
|
||||
*/
|
||||
async function uploadDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const name = args?.name || new Date().toLocaleString();
|
||||
const file = new File([value], name, { type: 'text/plain' });
|
||||
const url = await uploadFileAttachmentToServer(file, source);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for updating an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Content of the attachment
|
||||
* @returns {Promise<string>} URL of the attachment
|
||||
*/
|
||||
async function updateDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const attachments = getAttachments(source);
|
||||
const attachment = getAttachmentByFields(attachments, [args?.url, args?.name]);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
const file = new File([value], attachment.name, { type: 'text/plain' });
|
||||
const url = await uploadFileAttachmentToServer(file, source);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for deleting an attachment from the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function deleteDataBankAttachment(args, value) {
|
||||
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
|
||||
const attachments = getAttachments(source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
await deleteAttachment(attachment, source, () => { }, false);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for disabling an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function disableDataBankAttachment(args, value) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (extension_settings.disabled_attachments.includes(attachment.url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
extension_settings.disabled_attachments.push(attachment.url);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for enabling an attachment in the data bank.
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Name or URL of the attachment
|
||||
* @returns {Promise<string>} Empty string
|
||||
*/
|
||||
async function enableDataBankAttachment(args, value) {
|
||||
const attachments = getAttachments(args?.source);
|
||||
const attachment = getAttachmentByField(attachments, value);
|
||||
|
||||
if (!attachment) {
|
||||
toastr.warning('Attachment not found.');
|
||||
return '';
|
||||
}
|
||||
|
||||
const index = extension_settings.disabled_attachments.indexOf(attachment.url);
|
||||
if (index === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
extension_settings.disabled_attachments.splice(index, 1);
|
||||
return '';
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db',
|
||||
callback: () => document.getElementById('manageAttachments')?.click(),
|
||||
aliases: ['databank', 'data-bank'],
|
||||
helpString: 'Open the data bank',
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-list',
|
||||
callback: listDataBankAttachments,
|
||||
aliases: ['databank-list', 'data-bank-list'],
|
||||
helpString: 'List attachments in the Data Bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.LIST,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-get',
|
||||
callback: getDataBankText,
|
||||
aliases: ['databank-get', 'data-bank-get'],
|
||||
helpString: 'Get attachment text from the Data Bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-add',
|
||||
callback: uploadDataBankAttachment,
|
||||
aliases: ['databank-add', 'data-bank-add'],
|
||||
helpString: 'Add an attachment to the Data Bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-update',
|
||||
callback: updateDataBankAttachment,
|
||||
aliases: ['databank-update', 'data-bank-update'],
|
||||
helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
|
||||
new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-disable',
|
||||
callback: disableDataBankAttachment,
|
||||
aliases: ['databank-disable', 'data-bank-disable'],
|
||||
helpString: 'Disable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-enable',
|
||||
callback: enableDataBankAttachment,
|
||||
aliases: ['databank-enable', 'data-bank-enable'],
|
||||
helpString: 'Enable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-delete',
|
||||
callback: deleteDataBankAttachment,
|
||||
aliases: ['databank-delete', 'data-bank-delete'],
|
||||
helpString: 'Delete an attachment from the Data Bank.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="wide100p padding5">
|
||||
<div class="wide100p padding5 dataBankAttachments">
|
||||
<h2 class="marginBot5">
|
||||
<span data-i18n="Data Bank">
|
||||
Data Bank
|
||||
|
@ -37,7 +37,27 @@
|
|||
Size (Largest First)
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<label class="margin0 menu_button menu_button_icon attachmentsBulkEditButton">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
<span data-i18n="Bulk Edit">Bulk Edit</span>
|
||||
<input type="checkbox" class="displayNone attachmentsBulkEditCheckbox" hidden>
|
||||
</label>
|
||||
</div>
|
||||
<div class="attachmentBulkActionsContainer flex-container marginTopBot5 alignItemsBaseline">
|
||||
<div class="flex-container">
|
||||
<div class="menu_button menu_button_icon bulkActionSelectAll" title="Select all *visible* attachments">
|
||||
<i class="fa-solid fa-check-square"></i>
|
||||
<span data-i18n="Select All">Select All</span>
|
||||
</div>
|
||||
<div class="menu_button menu_button_icon bulkActionSelectNone" title="Deselect all *visible* attachments">
|
||||
<i class="fa-solid fa-square"></i>
|
||||
<span data-i18n="Select None">Select None</span>
|
||||
</div>
|
||||
<div class="menu_button menu_button_icon bulkActionDelete" title="Delete selected attachments">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span data-i18n="Delete">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justifyLeft globalAttachmentsBlock marginBot10">
|
||||
<h3 class="globalAttachmentsTitle margin0 title_restorable">
|
||||
|
@ -102,6 +122,7 @@
|
|||
|
||||
<div class="attachmentListItemTemplate template_element">
|
||||
<div class="attachmentListItem flex-container alignItemsCenter flexGap10">
|
||||
<div class="attachmentListItemCheckboxContainer"><input type="checkbox" class="attachmentListItemCheckbox"></div>
|
||||
<div class="attachmentFileIcon fa-solid fa-file-alt"></div>
|
||||
<div class="attachmentListItemName flex1"></div>
|
||||
<small class="attachmentListItemCreated"></small>
|
||||
|
|
|
@ -37,3 +37,27 @@
|
|||
.attachmentListItemCreated {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attachmentListItemCheckboxContainer,
|
||||
.attachmentBulkActionsContainer,
|
||||
.attachmentsBulkEditCheckbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@supports selector(:has(*)) {
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentsBulkEditButton {
|
||||
color: var(--golden);
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentBulkActionsContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentListItemCheckboxContainer {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentFileIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -926,5 +926,6 @@ jQuery(async function () {
|
|||
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
|
||||
],
|
||||
helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>
|
||||
<li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>
|
||||
<li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>
|
||||
<li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li>
|
||||
<li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
|
||||
<li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>
|
||||
<li data-placeholder="user_avatar" class="sd_comfy_workflow_editor_not_found">"%user_avatar%"</li>
|
||||
|
|
|
@ -18,9 +18,9 @@ import {
|
|||
formatCharacterAvatar,
|
||||
substituteParams,
|
||||
} from '../../../script.js';
|
||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
|
||||
import { selected_group } from '../../group-chats.js';
|
||||
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean } from '../../utils.js';
|
||||
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
|
||||
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
|
||||
|
@ -29,6 +29,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
|||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'sd';
|
||||
|
@ -185,6 +186,7 @@ const defaultSettings = {
|
|||
sampler: 'DDIM',
|
||||
model: '',
|
||||
vae: '',
|
||||
seed: -1,
|
||||
|
||||
// Automatic1111/Horde exclusives
|
||||
restore_faces: false,
|
||||
|
@ -229,6 +231,12 @@ const defaultSettings = {
|
|||
hr_second_pass_steps_max: 150,
|
||||
hr_second_pass_steps_step: 1,
|
||||
|
||||
// CLIP skip
|
||||
clip_skip_min: 1,
|
||||
clip_skip_max: 12,
|
||||
clip_skip_step: 1,
|
||||
clip_skip: 1,
|
||||
|
||||
// NovelAI settings
|
||||
novel_upscale_ratio_min: 1.0,
|
||||
novel_upscale_ratio_max: 4.0,
|
||||
|
@ -237,6 +245,7 @@ const defaultSettings = {
|
|||
novel_anlas_guard: false,
|
||||
novel_sm: false,
|
||||
novel_sm_dyn: false,
|
||||
novel_decrisper: false,
|
||||
|
||||
// OpenAI settings
|
||||
openai_style: 'vivid',
|
||||
|
@ -254,6 +263,8 @@ const defaultSettings = {
|
|||
pollinations_refine: false,
|
||||
};
|
||||
|
||||
const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
|
||||
|
||||
function processTriggers(chat, _, abort) {
|
||||
if (!extension_settings.sd.interactive_mode) {
|
||||
return;
|
||||
|
@ -381,6 +392,7 @@ async function loadSettings() {
|
|||
$('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
|
||||
$('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
|
||||
$('#sd_novel_sm_dyn').prop('disabled', !extension_settings.sd.novel_sm);
|
||||
$('#sd_novel_decrisper').prop('checked', extension_settings.sd.novel_decrisper);
|
||||
$('#sd_pollinations_enhance').prop('checked', extension_settings.sd.pollinations_enhance);
|
||||
$('#sd_pollinations_refine').prop('checked', extension_settings.sd.pollinations_refine);
|
||||
$('#sd_horde').prop('checked', extension_settings.sd.horde);
|
||||
|
@ -404,6 +416,9 @@ async function loadSettings() {
|
|||
$('#sd_comfy_url').val(extension_settings.sd.comfy_url);
|
||||
$('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
|
||||
$('#sd_snap').prop('checked', extension_settings.sd.snap);
|
||||
$('#sd_clip_skip').val(extension_settings.sd.clip_skip);
|
||||
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip);
|
||||
$('#sd_seed').val(extension_settings.sd.seed);
|
||||
|
||||
for (const style of extension_settings.sd.styles) {
|
||||
const option = document.createElement('option');
|
||||
|
@ -465,7 +480,7 @@ function addPromptTemplates() {
|
|||
const label = $('<label></label>')
|
||||
.text(modeLabels[name])
|
||||
.attr('for', `sd_prompt_${name}`)
|
||||
.attr('data-i18n', `sd_prompt_${name}`);
|
||||
.attr('data-i18n', `sd_prompt_${name}`);
|
||||
const textarea = $('<textarea></textarea>')
|
||||
.addClass('textarea_compact text_pole')
|
||||
.attr('id', `sd_prompt_${name}`)
|
||||
|
@ -477,7 +492,7 @@ function addPromptTemplates() {
|
|||
const button = $('<button></button>')
|
||||
.addClass('menu_button fa-solid fa-undo')
|
||||
.attr('title', 'Restore default')
|
||||
.attr('data-i18n', 'Restore default')
|
||||
.attr('data-i18n', 'Restore default')
|
||||
.on('click', () => {
|
||||
textarea.val(promptTemplates[name]);
|
||||
extension_settings.sd.prompts[name] = promptTemplates[name];
|
||||
|
@ -522,6 +537,42 @@ function onStyleSelect() {
|
|||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onDeleteStyleClick() {
|
||||
const selectedStyle = String($('#sd_style').find(':selected').val());
|
||||
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
|
||||
|
||||
if (!styleObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await callPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, 'confirm', '', { okButton: 'Delete' });
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = extension_settings.sd.styles.indexOf(styleObject);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.sd.styles.splice(index, 1);
|
||||
$('#sd_style').find(`option[value="${selectedStyle}"]`).remove();
|
||||
|
||||
if (extension_settings.sd.styles.length > 0) {
|
||||
extension_settings.sd.style = extension_settings.sd.styles[0].name;
|
||||
$('#sd_style').val(extension_settings.sd.style).trigger('change');
|
||||
} else {
|
||||
extension_settings.sd.style = '';
|
||||
$('#sd_prompt_prefix').val('').trigger('input');
|
||||
$('#sd_negative_prompt').val('').trigger('input');
|
||||
$('#sd_style').val('');
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onSaveStyleClick() {
|
||||
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
|
||||
|
||||
|
@ -611,9 +662,27 @@ function onChatChanged() {
|
|||
}
|
||||
|
||||
$('#sd_character_prompt_block').show();
|
||||
|
||||
const key = getCharaFilename(this_chid);
|
||||
$('#sd_character_prompt').val(key ? (extension_settings.sd.character_prompts[key] || '') : '');
|
||||
$('#sd_character_negative_prompt').val(key ? (extension_settings.sd.character_negative_prompts[key] || '') : '');
|
||||
let characterPrompt = key ? (extension_settings.sd.character_prompts[key] || '') : '';
|
||||
let negativePrompt = key ? (extension_settings.sd.character_negative_prompts[key] || '') : '';
|
||||
|
||||
const context = getContext();
|
||||
const sharedPromptData = context?.characters[this_chid]?.data?.extensions?.sd_character_prompt;
|
||||
const hasSharedData = sharedPromptData && typeof sharedPromptData === 'object';
|
||||
|
||||
if (typeof sharedPromptData?.positive === 'string' && !characterPrompt && sharedPromptData.positive) {
|
||||
characterPrompt = sharedPromptData.positive;
|
||||
extension_settings.sd.character_prompts[key] = characterPrompt;
|
||||
}
|
||||
if (typeof sharedPromptData?.negative === 'string' && !negativePrompt && sharedPromptData.negative) {
|
||||
negativePrompt = sharedPromptData.negative;
|
||||
extension_settings.sd.character_negative_prompts[key] = negativePrompt;
|
||||
}
|
||||
|
||||
$('#sd_character_prompt').val(characterPrompt);
|
||||
$('#sd_character_negative_prompt').val(negativePrompt);
|
||||
$('#sd_character_prompt_share').prop('checked', hasSharedData);
|
||||
}
|
||||
|
||||
function onCharacterPromptInput() {
|
||||
|
@ -621,6 +690,7 @@ function onCharacterPromptInput() {
|
|||
extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
|
||||
resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
writePromptFieldsDebounced(this_chid);
|
||||
}
|
||||
|
||||
function onCharacterNegativePromptInput() {
|
||||
|
@ -628,6 +698,7 @@ function onCharacterNegativePromptInput() {
|
|||
extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
|
||||
resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
writePromptFieldsDebounced(this_chid);
|
||||
}
|
||||
|
||||
function getCharacterPrefix() {
|
||||
|
@ -691,6 +762,17 @@ function onRefineModeInput() {
|
|||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onClipSkipInput() {
|
||||
extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val());
|
||||
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onSeedInput() {
|
||||
extension_settings.sd.seed = Number($('#sd_seed').val());
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onScaleInput() {
|
||||
extension_settings.sd.scale = Number($('#sd_scale').val());
|
||||
$('#sd_scale_value').text(extension_settings.sd.scale.toFixed(1));
|
||||
|
@ -776,6 +858,7 @@ async function onSourceChange() {
|
|||
extension_settings.sd.source = $('#sd_source').find(':selected').val();
|
||||
extension_settings.sd.model = null;
|
||||
extension_settings.sd.sampler = null;
|
||||
extension_settings.sd.scheduler = null;
|
||||
toggleSourceControls();
|
||||
saveSettingsDebounced();
|
||||
await loadSettingOptions();
|
||||
|
@ -832,6 +915,11 @@ function onNovelSmDynInput() {
|
|||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onNovelDecrisperInput() {
|
||||
extension_settings.sd.novel_decrisper = !!$('#sd_novel_decrisper').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onPollinationsEnhanceInput() {
|
||||
extension_settings.sd.pollinations_enhance = !!$('#sd_pollinations_enhance').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@ -1118,6 +1206,26 @@ async function getAutoRemoteUpscalers() {
|
|||
}
|
||||
}
|
||||
|
||||
async function getAutoRemoteSchedulers() {
|
||||
try {
|
||||
const result = await fetch('/api/sd/schedulers', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(getSdRequestBody()),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error('SD WebUI returned an error.');
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return ['N/A'];
|
||||
}
|
||||
}
|
||||
|
||||
async function getVladRemoteUpscalers() {
|
||||
try {
|
||||
const result = await fetch('/api/sd/sd-next/upscalers', {
|
||||
|
@ -1138,6 +1246,27 @@ async function getVladRemoteUpscalers() {
|
|||
}
|
||||
}
|
||||
|
||||
async function getDrawthingsRemoteUpscalers() {
|
||||
try {
|
||||
const result = await fetch('/api/sd/drawthings/get-upscaler', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(getSdRequestBody()),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error('SD DrawThings API returned an error.');
|
||||
}
|
||||
|
||||
const data = await result.text();
|
||||
|
||||
return data ? [data] : ['N/A'];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return ['N/A'];
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAutoRemoteModel() {
|
||||
try {
|
||||
const result = await fetch('/api/sd/set-model', {
|
||||
|
@ -1572,6 +1701,21 @@ async function loadDrawthingsModels() {
|
|||
|
||||
const data = [{ value: currentModel, text: currentModel }];
|
||||
|
||||
|
||||
const upscalers = await getDrawthingsRemoteUpscalers();
|
||||
|
||||
if (Array.isArray(upscalers) && upscalers.length > 0) {
|
||||
$('#sd_hr_upscaler').empty();
|
||||
|
||||
for (const upscaler of upscalers) {
|
||||
const option = document.createElement('option');
|
||||
option.innerText = upscaler;
|
||||
option.value = upscaler;
|
||||
option.selected = upscaler === extension_settings.sd.hr_upscaler;
|
||||
$('#sd_hr_upscaler').append(option);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log('Error loading DrawThings API models:', error);
|
||||
|
@ -1697,7 +1841,7 @@ async function loadSchedulers() {
|
|||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.auto:
|
||||
schedulers = ['N/A'];
|
||||
schedulers = await getAutoRemoteSchedulers();
|
||||
break;
|
||||
case sources.novel:
|
||||
schedulers = ['N/A'];
|
||||
|
@ -1729,6 +1873,11 @@ async function loadSchedulers() {
|
|||
option.selected = scheduler === extension_settings.sd.scheduler;
|
||||
$('#sd_scheduler').append(option);
|
||||
}
|
||||
|
||||
if (!extension_settings.sd.scheduler && schedulers.length > 0 && schedulers[0] !== 'N/A') {
|
||||
extension_settings.sd.scheduler = schedulers[0];
|
||||
$('#sd_scheduler').val(extension_settings.sd.scheduler).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadComfySchedulers() {
|
||||
|
@ -2121,6 +2270,7 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
|
|||
}
|
||||
|
||||
try {
|
||||
const toast = toastr.info('Generating multimodal caption...', 'Image Generation');
|
||||
const response = await fetch(avatarUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -2131,6 +2281,7 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
|
|||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
|
||||
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
|
||||
toastr.clear(toast);
|
||||
|
||||
if (!caption) {
|
||||
throw new Error('No caption returned from the API.');
|
||||
|
@ -2269,6 +2420,7 @@ async function generateTogetherAIImage(prompt, negativePrompt) {
|
|||
steps: extension_settings.sd.steps,
|
||||
width: extension_settings.sd.width,
|
||||
height: extension_settings.sd.height,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2293,6 +2445,7 @@ async function generatePollinationsImage(prompt, negativePrompt) {
|
|||
height: extension_settings.sd.height,
|
||||
enhance: extension_settings.sd.pollinations_enhance,
|
||||
refine: extension_settings.sd.pollinations_refine,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2335,6 +2488,7 @@ async function generateExtrasImage(prompt, negativePrompt) {
|
|||
hr_scale: extension_settings.sd.hr_scale,
|
||||
denoising_strength: extension_settings.sd.denoising_strength,
|
||||
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2371,6 +2525,8 @@ async function generateHordeImage(prompt, negativePrompt) {
|
|||
restore_faces: !!extension_settings.sd.restore_faces,
|
||||
enable_hr: !!extension_settings.sd.enable_hr,
|
||||
sanitize: !!extension_settings.sd.horde_sanitize,
|
||||
clip_skip: extension_settings.sd.clip_skip,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2399,6 +2555,7 @@ async function generateAutoImage(prompt, negativePrompt) {
|
|||
prompt: prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
sampler_name: extension_settings.sd.sampler,
|
||||
scheduler: extension_settings.sd.scheduler,
|
||||
steps: extension_settings.sd.steps,
|
||||
cfg_scale: extension_settings.sd.scale,
|
||||
width: extension_settings.sd.width,
|
||||
|
@ -2409,6 +2566,14 @@ async function generateAutoImage(prompt, negativePrompt) {
|
|||
hr_scale: extension_settings.sd.hr_scale,
|
||||
denoising_strength: extension_settings.sd.denoising_strength,
|
||||
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
// For AUTO1111
|
||||
override_settings: {
|
||||
CLIP_stop_at_last_layers: extension_settings.sd.clip_skip,
|
||||
},
|
||||
override_settings_restore_afterwards: true,
|
||||
// For SD.Next
|
||||
clip_skip: extension_settings.sd.clip_skip,
|
||||
// Ensure generated img is saved to disk
|
||||
save_images: true,
|
||||
send_images: true,
|
||||
|
@ -2449,6 +2614,9 @@ async function generateDrawthingsImage(prompt, negativePrompt) {
|
|||
restore_faces: !!extension_settings.sd.restore_faces,
|
||||
enable_hr: !!extension_settings.sd.enable_hr,
|
||||
denoising_strength: extension_settings.sd.denoising_strength,
|
||||
clip_skip: extension_settings.sd.clip_skip,
|
||||
upscaler_scale: extension_settings.sd.hr_scale,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
// TODO: advanced API parameters: hr, upscaler
|
||||
}),
|
||||
});
|
||||
|
@ -2485,8 +2653,10 @@ async function generateNovelImage(prompt, negativePrompt) {
|
|||
height: height,
|
||||
negative_prompt: negativePrompt,
|
||||
upscale_ratio: extension_settings.sd.novel_upscale_ratio,
|
||||
decrisper: extension_settings.sd.novel_decrisper,
|
||||
sm: sm,
|
||||
sm_dyn: sm_dyn,
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2633,6 +2803,7 @@ async function generateComfyImage(prompt, negativePrompt) {
|
|||
'scale',
|
||||
'width',
|
||||
'height',
|
||||
'clip_skip',
|
||||
];
|
||||
|
||||
const workflowResponse = await fetch('/api/sd/comfy/workflow', {
|
||||
|
@ -2648,7 +2819,9 @@ 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.replaceAll('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
|
||||
|
||||
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
|
||||
placeholders.forEach(ph => {
|
||||
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
|
||||
});
|
||||
|
@ -3048,8 +3221,35 @@ $('#sd_dropdown [id]').on('click', function () {
|
|||
}
|
||||
});
|
||||
|
||||
async function onCharacterPromptShareInput() {
|
||||
// Not a valid state to share character prompt
|
||||
if (this_chid === undefined || selected_group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldShare = !!$('#sd_character_prompt_share').prop('checked');
|
||||
|
||||
if (shouldShare) {
|
||||
await writePromptFields(this_chid);
|
||||
} else {
|
||||
await writeExtensionField(this_chid, 'sd_character_prompt', null);
|
||||
}
|
||||
}
|
||||
|
||||
async function writePromptFields(characterId) {
|
||||
const key = getCharaFilename(characterId);
|
||||
const promptPrefix = key ? (extension_settings.sd.character_prompts[key] || '') : '';
|
||||
const negativePromptPrefix = key ? (extension_settings.sd.character_negative_prompts[key] || '') : '';
|
||||
const promptObject = {
|
||||
positive: promptPrefix,
|
||||
negative: negativePromptPrefix,
|
||||
};
|
||||
await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'imagine',
|
||||
callback: generatePicture,
|
||||
aliases: ['sd', 'img', 'image'],
|
||||
namedArgumentList: [
|
||||
|
@ -3075,7 +3275,8 @@ jQuery(async () => {
|
|||
`,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'imagine-comfy-workflow',
|
||||
callback: changeComfyWorkflow,
|
||||
aliases: ['icw'],
|
||||
unnamedArgumentList: [
|
||||
|
@ -3127,6 +3328,7 @@ jQuery(async () => {
|
|||
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
|
||||
$('#sd_novel_sm').on('input', onNovelSmInput);
|
||||
$('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);
|
||||
$('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
|
||||
$('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
|
||||
$('#sd_pollinations_refine').on('input', onPollinationsRefineInput);
|
||||
$('#sd_comfy_validate').on('click', validateComfyUrl);
|
||||
|
@ -3138,12 +3340,16 @@ jQuery(async () => {
|
|||
$('#sd_expand').on('input', onExpandInput);
|
||||
$('#sd_style').on('change', onStyleSelect);
|
||||
$('#sd_save_style').on('click', onSaveStyleClick);
|
||||
$('#sd_delete_style').on('click', onDeleteStyleClick);
|
||||
$('#sd_character_prompt_block').hide();
|
||||
$('#sd_interactive_mode').on('input', onInteractiveModeInput);
|
||||
$('#sd_openai_style').on('change', onOpenAiStyleSelect);
|
||||
$('#sd_openai_quality').on('change', onOpenAiQualitySelect);
|
||||
$('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput);
|
||||
$('#sd_snap').on('input', onSnapInput);
|
||||
$('#sd_clip_skip').on('input', onClipSkipInput);
|
||||
$('#sd_seed').on('input', onSeedInput);
|
||||
$('#sd_character_prompt_share').on('input', onCharacterPromptShareInput);
|
||||
|
||||
$('.sd_settings .inline-drawer-toggle').on('click', function () {
|
||||
initScrollHeight($('#sd_prompt_prefix'));
|
||||
|
|
|
@ -182,23 +182,33 @@
|
|||
</div>
|
||||
<label for="sd_scale" data-i18n="CFG Scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
|
||||
<input id="sd_scale" type="range" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" />
|
||||
<div data-sd-source="novel" class="marginTopBot5">
|
||||
<label class="checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values.">
|
||||
<input id="sd_novel_decrisper" type="checkbox" />
|
||||
<span data-i18n="Decrisper">
|
||||
Decrisper
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="sd_steps" data-i18n="Sampling steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
|
||||
<input id="sd_steps" type="range" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" />
|
||||
<label for="sd_width" data-i18n="Width">Width (<span id="sd_width_value"></span>)</label>
|
||||
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" />
|
||||
<label for="sd_height" data-i18n="Height">Height (<span id="sd_height_value"></span>)</label>
|
||||
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" />
|
||||
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
|
||||
<select id="sd_resolution"><!-- Populated in JS --></select>
|
||||
<label for="sd_model" data-i18n="Model">Model</label>
|
||||
<select id="sd_model"></select>
|
||||
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
|
||||
<select id="sd_sampler"></select>
|
||||
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label">
|
||||
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label marginTopBot5">
|
||||
<input id="sd_horde_karras" type="checkbox" />
|
||||
<span data-i18n="Karras (not all samplers supported)">
|
||||
Karras (not all samplers supported)
|
||||
</span>
|
||||
</label>
|
||||
<div data-sd-source="novel" class="flex-container">
|
||||
<div data-sd-source="novel" class="flex-container marginTopBot5">
|
||||
<label class="flex1 checkbox_label" data-i18n="[title]SMEA versions of samplers are modified to perform better at high resolution." title="SMEA versions of samplers are modified to perform better at high resolution.">
|
||||
<input id="sd_novel_sm" type="checkbox" />
|
||||
<span data-i18n="SMEA">
|
||||
|
@ -212,9 +222,7 @@
|
|||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
|
||||
<select id="sd_resolution"><!-- Populated in JS --></select>
|
||||
<div data-sd-source="comfy">
|
||||
<div data-sd-source="comfy,auto">
|
||||
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
</div>
|
||||
|
@ -222,7 +230,7 @@
|
|||
<label for="sd_vae">VAE</label>
|
||||
<select id="sd_vae"></select>
|
||||
</div>
|
||||
<div class="flex-container marginTop10 margin-bot-10px">
|
||||
<div class="flex-container marginTopBot5">
|
||||
<label class="flex1 checkbox_label">
|
||||
<input id="sd_restore_faces" type="checkbox" />
|
||||
<span data-i18n="Restore Faces">Restore Faces</span>
|
||||
|
@ -232,11 +240,17 @@
|
|||
<span data-i18n="Hires. Fix">Hires. Fix</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-sd-source="auto,vlad">
|
||||
<div data-sd-source="auto,vlad,comfy,horde,drawthings,extras" class="marginTopBot5">
|
||||
<label for="sd_clip_skip">CLIP Skip (<span id="sd_clip_skip_value"></span>)</label>
|
||||
<input type="range" id="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" />
|
||||
</div>
|
||||
<div data-sd-source="auto,vlad,drawthings">
|
||||
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
|
||||
<select id="sd_hr_upscaler"></select>
|
||||
<label for="sd_hr_scale"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_hr_scale_value"></span>)</label>
|
||||
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" />
|
||||
</div>
|
||||
<div data-sd-source="auto,vlad">
|
||||
<label for="sd_denoising_strength"><span data-i18n="Denoising strength">Denoising strength</span> (<span id="sd_denoising_strength_value"></span>)</label>
|
||||
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" />
|
||||
<label for="sd_hr_second_pass_steps"><span data-i18n="Hires steps (2nd pass)">Hires steps (2nd pass)</span> (<span id="sd_hr_second_pass_steps_value"></span>)</label>
|
||||
|
@ -246,6 +260,11 @@
|
|||
<label for="sd_novel_upscale_ratio"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_novel_upscale_ratio_value"></span>)</label>
|
||||
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
|
||||
</div>
|
||||
<div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5">
|
||||
<label for="sd_seed">Seed</label>
|
||||
<small>(-1 for random)</small>
|
||||
<input id="sd_seed" type="number" class="text_pole" min="-1" max="9999999999" step="1" />
|
||||
</div>
|
||||
<hr>
|
||||
<h4 data-i18n="[title]Preset for prompt prefix and negative prompt" title="Preset for prompt prefix and negative prompt">
|
||||
<span data-i18n="Style">Style</span>
|
||||
|
@ -255,6 +274,9 @@
|
|||
<div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">
|
||||
<i class="fa-solid fa-save"></i>
|
||||
</div>
|
||||
<div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</div>
|
||||
</div>
|
||||
<label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label>
|
||||
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
|
||||
|
@ -267,6 +289,15 @@
|
|||
<label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label>
|
||||
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
|
||||
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix. Example: jewellery, shoes, glasses"></textarea>
|
||||
<label for="sd_character_prompt_share" class="checkbox_label flexWrap marginTop5">
|
||||
<input id="sd_character_prompt_share" type="checkbox" />
|
||||
<span data-i18n="Shareable">
|
||||
Shareable
|
||||
</span>
|
||||
<small class="flexBasis100p">
|
||||
When checked, character-specific prompts will be saved with the character card data.
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,9 @@ import {
|
|||
} from '../../../script.js';
|
||||
import { extension_settings, getContext } from '../../extensions.js';
|
||||
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
|
||||
export const autoModeOptions = {
|
||||
|
@ -649,4 +652,21 @@ jQuery(() => {
|
|||
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
|
||||
|
||||
document.body.classList.add('translate');
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'translate',
|
||||
helpString: 'Translate text to a target language. If target language is not provided, the value from the extension settings will be used.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('target', 'The target language code to translate to', ARGUMENT_TYPE.STRING, false, false, '', Object.values(languageCodes)),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''),
|
||||
],
|
||||
callback: async (args, value) => {
|
||||
const target = args?.target && Object.values(languageCodes).includes(String(args.target))
|
||||
? String(args.target)
|
||||
: extension_settings.translate.target_language;
|
||||
return await translate(String(value), target);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -21,10 +21,14 @@ import {
|
|||
} from '../../extensions.js';
|
||||
import { collapseNewlines } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getDataBankAttachments, getFileAttachment } from '../../chats.js';
|
||||
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { getSortedEntries } from '../../world-info.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
|
||||
const MODULE_NAME = 'vectors';
|
||||
|
||||
|
@ -38,6 +42,8 @@ const settings = {
|
|||
togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
|
||||
openai_model: 'text-embedding-ada-002',
|
||||
cohere_model: 'embed-english-v3.0',
|
||||
ollama_model: 'mxbai-embed-large',
|
||||
ollama_keep: false,
|
||||
summarize: false,
|
||||
summarize_sent: false,
|
||||
summary_source: 'main',
|
||||
|
@ -272,6 +278,10 @@ async function synchronizeChat(batchSize = 5) {
|
|||
switch (cause) {
|
||||
case 'api_key_missing':
|
||||
return 'API key missing. Save it in the "API Connections" panel.';
|
||||
case 'api_url_missing':
|
||||
return 'API URL missing. Save it in the "API Connections" panel.';
|
||||
case 'api_model_missing':
|
||||
return 'Vectorization Source Model is required, but not set.';
|
||||
case 'extras_module_missing':
|
||||
return 'Extras API must provide an "embeddings" module.';
|
||||
default:
|
||||
|
@ -325,28 +335,7 @@ async function processFiles(chat) {
|
|||
return;
|
||||
}
|
||||
|
||||
const dataBank = getDataBankAttachments();
|
||||
const dataBankCollectionIds = [];
|
||||
|
||||
for (const file of dataBank) {
|
||||
const collectionId = getFileCollectionId(file.url);
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
dataBankCollectionIds.push(collectionId);
|
||||
|
||||
// File is already in the collection
|
||||
if (hashesInCollection.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download and process the file
|
||||
file.text = await getFileAttachment(file.url);
|
||||
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold_db * 1024;
|
||||
// Use chunk size from settings if file is larger than threshold
|
||||
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
|
||||
await vectorizeFile(file.text, file.name, collectionId, chunkSize);
|
||||
}
|
||||
const dataBankCollectionIds = await ingestDataBankAttachments();
|
||||
|
||||
if (dataBankCollectionIds.length) {
|
||||
const queryText = await getQueryText(chat);
|
||||
|
@ -393,6 +382,39 @@ async function processFiles(chat) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that data bank attachments are ingested and inserted into the vector index.
|
||||
* @param {string} [source] Optional source filter for data bank attachments.
|
||||
* @returns {Promise<string[]>} Collection IDs
|
||||
*/
|
||||
async function ingestDataBankAttachments(source) {
|
||||
// Exclude disabled files
|
||||
const dataBank = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
|
||||
const dataBankCollectionIds = [];
|
||||
|
||||
for (const file of dataBank) {
|
||||
const collectionId = getFileCollectionId(file.url);
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
dataBankCollectionIds.push(collectionId);
|
||||
|
||||
// File is already in the collection
|
||||
if (hashesInCollection.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download and process the file
|
||||
file.text = await getFileAttachment(file.url);
|
||||
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold_db * 1024;
|
||||
// Use chunk size from settings if file is larger than threshold
|
||||
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
|
||||
await vectorizeFile(file.text, file.name, collectionId, chunkSize);
|
||||
}
|
||||
|
||||
return dataBankCollectionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts file chunks from the Data Bank into the prompt.
|
||||
* @param {string} queryText Text to query
|
||||
|
@ -401,7 +423,7 @@ async function processFiles(chat) {
|
|||
*/
|
||||
async function injectDataBankChunks(queryText, collectionIds) {
|
||||
try {
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db);
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db, settings.score_threshold);
|
||||
console.debug(`Vectors: Retrieved ${collectionIds.length} Data Bank collections`, queryResults);
|
||||
let textResult = '';
|
||||
|
||||
|
@ -637,6 +659,12 @@ function getVectorHeaders() {
|
|||
case 'cohere':
|
||||
addCohereHeaders(headers);
|
||||
break;
|
||||
case 'ollama':
|
||||
addOllamaHeaders(headers);
|
||||
break;
|
||||
case 'llamacpp':
|
||||
addLlamaCppHeaders(headers);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -685,6 +713,28 @@ function addCohereHeaders(headers) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the Ollama API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addOllamaHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-Ollama-Model': extension_settings.vectors.ollama_model,
|
||||
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
|
||||
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the LlamaCpp API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addLlamaCppHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts vector items into a collection
|
||||
* @param {string} collectionId - The collection to insert into
|
||||
|
@ -692,18 +742,7 @@ function addCohereHeaders(headers) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function insertVectorItems(collectionId, items) {
|
||||
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
|
||||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
|
||||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
|
||||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) {
|
||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||
}
|
||||
|
||||
if (settings.source === 'extras' && !modules.includes('embeddings')) {
|
||||
throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
|
||||
}
|
||||
throwIfSourceInvalid();
|
||||
|
||||
const headers = getVectorHeaders();
|
||||
|
||||
|
@ -722,6 +761,33 @@ async function insertVectorItems(collectionId, items) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if the source is invalid (missing API key or URL, or missing module)
|
||||
*/
|
||||
function throwIfSourceInvalid() {
|
||||
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
|
||||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
|
||||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
|
||||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) {
|
||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||
}
|
||||
|
||||
if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
|
||||
settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
|
||||
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
|
||||
}
|
||||
|
||||
if (settings.source === 'ollama' && !settings.ollama_model) {
|
||||
throw new Error('Vectors: API model missing', { cause: 'api_model_missing' });
|
||||
}
|
||||
|
||||
if (settings.source === 'extras' && !modules.includes('embeddings')) {
|
||||
throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes vector items from a collection
|
||||
* @param {string} collectionId - The collection to delete from
|
||||
|
@ -777,9 +843,10 @@ async function queryCollection(collectionId, searchText, topK) {
|
|||
* @param {string[]} collectionIds - Collection IDs to query
|
||||
* @param {string} searchText - Text to query
|
||||
* @param {number} topK - Number of results to return
|
||||
* @param {number} threshold - Score threshold
|
||||
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
|
||||
*/
|
||||
async function queryMultipleCollections(collectionIds, searchText, topK) {
|
||||
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
|
||||
const headers = getVectorHeaders();
|
||||
|
||||
const response = await fetch('/api/vector/query-multi', {
|
||||
|
@ -790,7 +857,7 @@ async function queryMultipleCollections(collectionIds, searchText, topK) {
|
|||
searchText: searchText,
|
||||
topK: topK,
|
||||
source: settings.source,
|
||||
threshold: settings.score_threshold,
|
||||
threshold: threshold ?? settings.score_threshold,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -870,6 +937,8 @@ function toggleSettings() {
|
|||
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
|
||||
$('#openai_vectorsModel').toggle(settings.source === 'openai');
|
||||
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
|
||||
$('#ollama_vectorsModel').toggle(settings.source === 'ollama');
|
||||
$('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
|
||||
$('#nomicai_apiKey').toggle(settings.source === 'nomicai');
|
||||
}
|
||||
|
||||
|
@ -900,8 +969,8 @@ async function onViewStatsClick() {
|
|||
toastr.info(`Total hashes: <b>${totalHashes}</b><br>
|
||||
Unique hashes: <b>${uniqueHashes}</b><br><br>
|
||||
I'll mark collected messages with a green circle.`,
|
||||
`Stats for chat ${chatId}`,
|
||||
{ timeOut: 10000, escapeHtml: false });
|
||||
`Stats for chat ${chatId}`,
|
||||
{ timeOut: 10000, escapeHtml: false });
|
||||
|
||||
const chat = getContext().chat;
|
||||
for (const message of chat) {
|
||||
|
@ -1072,7 +1141,7 @@ async function activateWorldInfo(chat) {
|
|||
return;
|
||||
}
|
||||
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries);
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries, settings.score_threshold);
|
||||
const activatedHashes = Object.values(queryResults).flatMap(x => x.hashes).filter(onlyUnique);
|
||||
const activatedEntries = [];
|
||||
|
||||
|
@ -1154,6 +1223,17 @@ jQuery(async () => {
|
|||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_ollama_model').val(settings.ollama_model).on('input', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.ollama_model = String($('#vectors_ollama_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => {
|
||||
settings.ollama_keep = $('#vectors_ollama_keep').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_template').val(settings.template).on('input', () => {
|
||||
settings.template = String($('#vectors_template').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
|
@ -1332,4 +1412,60 @@ jQuery(async () => {
|
|||
eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex);
|
||||
eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex);
|
||||
eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex);
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-ingest',
|
||||
callback: async () => {
|
||||
await ingestDataBankAttachments();
|
||||
return '';
|
||||
},
|
||||
aliases: ['databank-ingest', 'data-bank-ingest'],
|
||||
helpString: 'Force the ingestion of all Data Bank attachments.',
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-purge',
|
||||
callback: async () => {
|
||||
const dataBank = getDataBankAttachments();
|
||||
|
||||
for (const file of dataBank) {
|
||||
await purgeFileVectorIndex(file.url);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
aliases: ['databank-purge', 'data-bank-purge'],
|
||||
helpString: 'Purge the vector index for all Data Bank attachments.',
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'db-search',
|
||||
callback: async (args, query) => {
|
||||
const clamp = (v) => Number.isNaN(v) ? null : Math.min(1, Math.max(0, v));
|
||||
const threshold = clamp(Number(args?.threshold ?? settings.score_threshold));
|
||||
const source = String(args?.source ?? '');
|
||||
const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
|
||||
const collectionIds = await ingestDataBankAttachments(String(source));
|
||||
const queryResults = await queryMultipleCollections(collectionIds, String(query), settings.chunk_count_db, threshold);
|
||||
|
||||
// Map collection IDs to file URLs
|
||||
const urls = Object
|
||||
.keys(queryResults)
|
||||
.map(x => attachments.find(y => getFileCollectionId(y.url) === x))
|
||||
.filter(x => x)
|
||||
.map(x => x.url);
|
||||
|
||||
return JSON.stringify(urls);
|
||||
},
|
||||
aliases: ['databank-search', 'data-bank-search'],
|
||||
helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('threshold', 'Threshold for the similarity score in the [0, 1] range. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''),
|
||||
new SlashCommandNamedArgument('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
returns: ARGUMENT_TYPE.LIST,
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -12,14 +12,37 @@
|
|||
<select id="vectors_source" class="text_pole">
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||
<option value="palm">Google MakerSuite</option>
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="mistral">MistralAI</option>
|
||||
<option value="nomicai">NomicAI</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="ollama_vectorsModel">
|
||||
<label for="vectors_ollama_model">
|
||||
Vectorization Model
|
||||
</label>
|
||||
<input id="vectors_ollama_model" class="text_pole" type="text" placeholder="Model tag, e.g. llama3" />
|
||||
<label for="vectors_ollama_keep" class="checkbox_label" title="When checked, the model will not be unloaded after use.">
|
||||
<input id="vectors_ollama_keep" type="checkbox" />
|
||||
<span>Keep model in memory</span>
|
||||
</label>
|
||||
<i>
|
||||
Hint: Download models and set the URL in the API connection settings.
|
||||
</i>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">
|
||||
<span>
|
||||
The server MUST be started with the <code>--embedding</code> flag to use this feature!
|
||||
</span>
|
||||
<i>
|
||||
Hint: Set the URL in the API connection settings.
|
||||
</i>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="openai_vectorsModel">
|
||||
<label for="vectors_openai_model">
|
||||
Vectorization Model
|
||||
|
|
|
@ -1853,7 +1853,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
|||
|
||||
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
|
||||
|
||||
if (isFunctionCallingSupported()) {
|
||||
if (isFunctionCallingSupported() && !stream) {
|
||||
await registerFunctionTools(type, generate_data);
|
||||
}
|
||||
|
||||
|
@ -1968,7 +1968,14 @@ async function registerFunctionTools(type, data) {
|
|||
}
|
||||
|
||||
async function checkFunctionToolCalls(data) {
|
||||
if ([chat_completion_sources.OPENAI, chat_completion_sources.CUSTOM, chat_completion_sources.MISTRALAI].includes(oai_settings.chat_completion_source)) {
|
||||
const oaiCompat = [
|
||||
chat_completion_sources.OPENAI,
|
||||
chat_completion_sources.CUSTOM,
|
||||
chat_completion_sources.MISTRALAI,
|
||||
chat_completion_sources.OPENROUTER,
|
||||
chat_completion_sources.GROQ,
|
||||
];
|
||||
if (oaiCompat.includes(oai_settings.chat_completion_source)) {
|
||||
if (!Array.isArray(data?.choices)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1999,6 +2006,21 @@ async function checkFunctionToolCalls(data) {
|
|||
}
|
||||
}
|
||||
|
||||
if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
|
||||
if (!Array.isArray(data?.content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const content of data.content) {
|
||||
if (content.type === 'tool_use') {
|
||||
/** @type {FunctionToolCall} */
|
||||
const args = { name: content.name, arguments: JSON.stringify(content.input) };
|
||||
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
|
||||
data.allowEmptyResponse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
|
||||
if (!Array.isArray(data?.tool_calls)) {
|
||||
return;
|
||||
|
@ -2028,6 +2050,9 @@ export function isFunctionCallingSupported() {
|
|||
chat_completion_sources.COHERE,
|
||||
chat_completion_sources.CUSTOM,
|
||||
chat_completion_sources.MISTRALAI,
|
||||
chat_completion_sources.CLAUDE,
|
||||
chat_completion_sources.OPENROUTER,
|
||||
chat_completion_sources.GROQ,
|
||||
];
|
||||
return supportedSources.includes(oai_settings.chat_completion_source);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { getRequestHeaders } from '../script.js';
|
||||
import { renderExtensionTemplateAsync } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { isValidUrl } from './utils.js';
|
||||
|
||||
/**
|
||||
|
@ -441,6 +444,32 @@ class YouTubeScraper {
|
|||
this.description = 'Download a transcript from a YouTube video.';
|
||||
this.iconClass = 'fa-brands fa-youtube';
|
||||
this.iconAvailable = true;
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'yt-script',
|
||||
callback: async (args, url) => {
|
||||
try {
|
||||
if (!url) {
|
||||
throw new Error('URL or ID of the YouTube video is required');
|
||||
}
|
||||
|
||||
const lang = String(args?.lang || '');
|
||||
const { transcript } = await this.getScript(String(url).trim(), lang);
|
||||
return transcript;
|
||||
} catch (error) {
|
||||
toastr.error(error.message);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
helpString: 'Scrape a transcript from a YouTube video by ID or URL.',
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, ''),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('URL or ID of the YouTube video', ARGUMENT_TYPE.STRING, true, false),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -456,7 +485,12 @@ class YouTubeScraper {
|
|||
* @param {string} url URL of the YouTube video
|
||||
* @returns {string} ID of the YouTube video
|
||||
*/
|
||||
parseId(url){
|
||||
parseId(url) {
|
||||
// If the URL is already an ID, return it
|
||||
if (/^[a-zA-Z0-9_-]{11}$/.test(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const regex = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/;
|
||||
const match = url.match(regex);
|
||||
return (match?.length && match[1] ? match[1] : url);
|
||||
|
@ -479,8 +513,22 @@ class YouTubeScraper {
|
|||
return;
|
||||
}
|
||||
|
||||
const id = this.parseId(String(videoUrl).trim());
|
||||
const toast = toastr.info('Working, please wait...');
|
||||
const { transcript, id } = await this.getScript(videoUrl, lang);
|
||||
toastr.clear(toast);
|
||||
|
||||
const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' });
|
||||
return [file];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the transcript of a YouTube video.
|
||||
* @param {string} videoUrl Video URL or ID
|
||||
* @param {string} lang Video language
|
||||
* @returns {Promise<{ transcript: string, id: string }>} Transcript of the YouTube video with the video ID
|
||||
*/
|
||||
async getScript(videoUrl, lang) {
|
||||
const id = this.parseId(String(videoUrl).trim());
|
||||
|
||||
const result = await fetch('/api/serpapi/transcript', {
|
||||
method: 'POST',
|
||||
|
@ -494,10 +542,7 @@ class YouTubeScraper {
|
|||
}
|
||||
|
||||
const transcript = await result.text();
|
||||
toastr.clear(toast);
|
||||
|
||||
const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' });
|
||||
return [file];
|
||||
return { transcript, id };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export const ARGUMENT_TYPE = {
|
|||
|
||||
export class SlashCommandArgument {
|
||||
/**
|
||||
* Creates an unnamed argument from a poperties object.
|
||||
* Creates an unnamed argument from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} props.description description of the argument
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
|
||||
|
@ -75,7 +75,7 @@ export class SlashCommandArgument {
|
|||
|
||||
export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
/**
|
||||
* Creates an unnamed argument from a poperties object.
|
||||
* Creates an unnamed argument from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} props.name the argument's name
|
||||
* @param {string[]} [props.aliasList] list of aliases
|
||||
|
|
|
@ -1143,6 +1143,10 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
|||
delete params.dynatemp_high;
|
||||
}
|
||||
|
||||
if (settings.type === TABBY) {
|
||||
params.n = canMultiSwipe ? settings.n : 1;
|
||||
}
|
||||
|
||||
switch (settings.type) {
|
||||
case VLLM:
|
||||
params = Object.assign(params, vllmParams);
|
||||
|
|
|
@ -477,7 +477,7 @@ export function sortByCssOrder(a, b) {
|
|||
* trimToEndSentence('Hello, world! I am from'); // 'Hello, world!'
|
||||
*/
|
||||
export function trimToEndSentence(input, include_newline = false) {
|
||||
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '’', '」']); // extend this as you see fit
|
||||
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '’', '」', '_']); // extend this as you see fit
|
||||
let last = -1;
|
||||
|
||||
for (let i = input.length - 1; i >= 0; i--) {
|
||||
|
|
|
@ -3071,7 +3071,7 @@ async function checkWorldInfo(chat, maxContext) {
|
|||
|
||||
if (shouldWIAddPrompt) {
|
||||
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
|
||||
const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`;
|
||||
const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`.replace(/(^\n)|(\n$)/g, '');
|
||||
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
|
||||
}
|
||||
|
||||
|
|
|
@ -656,12 +656,11 @@ body .panelControlBar {
|
|||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 300ms;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#rightSendForm>div:hover,
|
||||
|
@ -690,8 +689,13 @@ body .panelControlBar {
|
|||
|
||||
|
||||
#form_sheld.isExecutingCommandsFromChatInput {
|
||||
#send_but {
|
||||
|
||||
#send_but,
|
||||
#mes_continue {
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#rightSendForm>div:not(.mes_send).stscript_btn {
|
||||
|
@ -3168,14 +3172,9 @@ grammarly-extension {
|
|||
|
||||
.avatar_div .menu_button,
|
||||
.form_create_bottom_buttons_block .menu_button {
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
filter: grayscale(0.5);
|
||||
text-align: center;
|
||||
font-size: 17px;
|
||||
aspect-ratio: 1 / 1;
|
||||
flex: 0.05;
|
||||
}
|
||||
|
||||
.menu_button:hover,
|
||||
|
@ -5134,4 +5133,4 @@ body:not(.movingUI) .drawer-content.maximized {
|
|||
color: #FAF8F6;
|
||||
}
|
||||
|
||||
/* Pastel White */
|
||||
/* Pastel White */
|
||||
|
|
|
@ -164,6 +164,17 @@ function getOverrideHeaders(urlHost) {
|
|||
* @param {string|null} server API server for new request
|
||||
*/
|
||||
function setAdditionalHeaders(request, args, server) {
|
||||
setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} requestHeaders Request headers
|
||||
* @param {string} type API type
|
||||
* @param {string|null} server API server for new request
|
||||
* @param {import('./users').UserDirectoryList} directories User directories
|
||||
*/
|
||||
function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
|
||||
const headerGetters = {
|
||||
[TEXTGEN_TYPES.MANCER]: getMancerHeaders,
|
||||
[TEXTGEN_TYPES.VLLM]: getVllmHeaders,
|
||||
|
@ -178,13 +189,13 @@ function setAdditionalHeaders(request, args, server) {
|
|||
[TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders,
|
||||
};
|
||||
|
||||
const getHeaders = headerGetters[request.body.api_type];
|
||||
const headers = getHeaders ? getHeaders(request.user.directories) : {};
|
||||
const getHeaders = headerGetters[type];
|
||||
const headers = getHeaders ? getHeaders(directories) : {};
|
||||
|
||||
if (typeof server === 'string' && server.length > 0) {
|
||||
try {
|
||||
const url = new URL(server);
|
||||
const overrideHeaders = getOverrideHeaders(url.host);
|
||||
const overrideHeaders = getOverrideHeaders(url.host);
|
||||
|
||||
if (overrideHeaders && Object.keys(overrideHeaders).length > 0) {
|
||||
Object.assign(headers, overrideHeaders);
|
||||
|
@ -194,10 +205,11 @@ function setAdditionalHeaders(request, args, server) {
|
|||
}
|
||||
}
|
||||
|
||||
Object.assign(args.headers, headers);
|
||||
Object.assign(requestHeaders, headers);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOverrideHeaders,
|
||||
setAdditionalHeaders,
|
||||
setAdditionalHeadersByType,
|
||||
};
|
||||
|
|
|
@ -41,6 +41,7 @@ const USER_DIRECTORY_TEMPLATE = Object.freeze({
|
|||
comfyWorkflows: 'user/workflows',
|
||||
files: 'user/files',
|
||||
vectors: 'vectors',
|
||||
backups: 'backups',
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -115,6 +115,7 @@ async function sendClaudeRequest(request, response) {
|
|||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
const additionalHeaders = {};
|
||||
let use_system_prompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
|
||||
let converted_prompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, use_system_prompt, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);
|
||||
// Add custom stop sequences
|
||||
|
@ -136,6 +137,18 @@ async function sendClaudeRequest(request, response) {
|
|||
if (use_system_prompt) {
|
||||
requestBody.system = converted_prompt.systemPrompt;
|
||||
}
|
||||
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
|
||||
// Claude doesn't do prefills on function calls, and doesn't allow empty messages
|
||||
if (converted_prompt.messages.length && converted_prompt.messages[converted_prompt.messages.length - 1].role === 'assistant') {
|
||||
converted_prompt.messages.push({ role: 'user', content: '.' });
|
||||
}
|
||||
additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
|
||||
requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
|
||||
requestBody.tools = request.body.tools
|
||||
.filter(tool => tool.type === 'function')
|
||||
.map(tool => tool.function)
|
||||
.map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
|
||||
}
|
||||
console.log('Claude request:', requestBody);
|
||||
|
||||
const generateResponse = await fetch(apiUrl + '/messages', {
|
||||
|
@ -146,6 +159,7 @@ async function sendClaudeRequest(request, response) {
|
|||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
'x-api-key': apiKey,
|
||||
...additionalHeaders,
|
||||
},
|
||||
timeout: 0,
|
||||
});
|
||||
|
@ -163,8 +177,8 @@ async function sendClaudeRequest(request, response) {
|
|||
const responseText = generateResponseJson.content[0].text;
|
||||
console.log('Claude response:', generateResponseJson);
|
||||
|
||||
// Wrap it back to OAI format
|
||||
const reply = { choices: [{ 'message': { 'content': responseText } }] };
|
||||
// Wrap it back to OAI format + save the original content
|
||||
const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };
|
||||
return response.send(reply);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -905,6 +919,18 @@ router.post('/generate', jsonParser, function (request, response) {
|
|||
apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
|
||||
headers = {};
|
||||
bodyParams = {};
|
||||
|
||||
// 'required' tool choice is not supported by Groq
|
||||
if (request.body.tool_choice === 'required') {
|
||||
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
|
||||
request.body.tool_choice = request.body.tools.length > 1
|
||||
? 'auto' :
|
||||
{ type: 'function', function: { name: request.body.tools[0]?.function?.name } };
|
||||
|
||||
} else {
|
||||
request.body.tool_choice = 'none';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('This chat completion source is not supported yet.');
|
||||
return response.status(400).send({ error: true });
|
||||
|
|
|
@ -6,15 +6,16 @@ const sanitize = require('sanitize-filename');
|
|||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { PUBLIC_DIRECTORIES, UPLOADS_PATH } = require('../constants');
|
||||
const { UPLOADS_PATH } = require('../constants');
|
||||
const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');
|
||||
|
||||
/**
|
||||
* Saves a chat to the backups directory.
|
||||
* @param {string} directory The user's backups directory.
|
||||
* @param {string} name The name of the chat.
|
||||
* @param {string} chat The serialized chat to save.
|
||||
*/
|
||||
function backupChat(name, chat) {
|
||||
function backupChat(directory, name, chat) {
|
||||
try {
|
||||
const isBackupDisabled = getConfigValue('disableChatBackup', false);
|
||||
|
||||
|
@ -22,17 +23,13 @@ function backupChat(name, chat) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(PUBLIC_DIRECTORIES.backups)) {
|
||||
fs.mkdirSync(PUBLIC_DIRECTORIES.backups);
|
||||
}
|
||||
|
||||
// replace non-alphanumeric characters with underscores
|
||||
name = sanitize(name).replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
|
||||
const backupFile = path.join(PUBLIC_DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.jsonl`);
|
||||
const backupFile = path.join(directory, `chat_${name}_${generateTimestamp()}.jsonl`);
|
||||
writeFileAtomicSync(backupFile, chat, 'utf-8');
|
||||
|
||||
removeOldBackups(`chat_${name}_`);
|
||||
removeOldBackups(directory, `chat_${name}_`);
|
||||
} catch (err) {
|
||||
console.log(`Could not backup chat for ${name}`, err);
|
||||
}
|
||||
|
@ -151,7 +148,7 @@ router.post('/save', jsonParser, function (request, response) {
|
|||
const fileName = `${sanitize(String(request.body.file_name))}.jsonl`;
|
||||
const filePath = path.join(request.user.directories.chats, directoryName, fileName);
|
||||
writeFileAtomicSync(filePath, jsonlData, 'utf8');
|
||||
backupChat(directoryName, jsonlData);
|
||||
backupChat(request.user.directories.backups, directoryName, jsonlData);
|
||||
return response.send({ result: 'ok' });
|
||||
} catch (error) {
|
||||
response.send(error);
|
||||
|
@ -455,7 +452,7 @@ router.post('/group/save', jsonParser, (request, response) => {
|
|||
let chat_data = request.body.chat;
|
||||
let jsonlData = chat_data.map(JSON.stringify).join('\n');
|
||||
writeFileAtomicSync(pathToFile, jsonlData, 'utf8');
|
||||
backupChat(String(id), jsonlData);
|
||||
backupChat(request.user.directories.backups, String(id), jsonlData);
|
||||
return response.send({ ok: true });
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const fetch = require('node-fetch').default;
|
||||
const express = require('express');
|
||||
const AIHorde = require('@zeldafan0225/ai_horde');
|
||||
const { AIHorde, ModelGenerationInputStableSamplers, ModelInterrogationFormTypes, HordeAsyncRequestStates } = require('@zeldafan0225/ai_horde');
|
||||
const { getVersion, delay, Cache } = require('../util');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
const { jsonParser } = require('../express-common');
|
||||
|
@ -191,8 +191,7 @@ router.post('/generate-text', jsonParser, async (request, response) => {
|
|||
|
||||
router.post('/sd-samplers', jsonParser, async (_, response) => {
|
||||
try {
|
||||
const ai_horde = await getHordeClient();
|
||||
const samplers = Object.values(ai_horde.ModelGenerationInputStableSamplers);
|
||||
const samplers = Object.values(ModelGenerationInputStableSamplers);
|
||||
response.send(samplers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -217,7 +216,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
|
|||
const ai_horde = await getHordeClient();
|
||||
const result = await ai_horde.postAsyncInterrogate({
|
||||
source_image: request.body.image,
|
||||
forms: [{ name: AIHorde.ModelInterrogationFormTypes.caption }],
|
||||
forms: [{ name: ModelInterrogationFormTypes.caption }],
|
||||
}, { token: api_key_horde });
|
||||
|
||||
if (!result.id) {
|
||||
|
@ -233,7 +232,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
|
|||
const status = await ai_horde.getInterrogationStatus(result.id);
|
||||
console.log(status);
|
||||
|
||||
if (status.state === AIHorde.HordeAsyncRequestStates.done) {
|
||||
if (status.state === HordeAsyncRequestStates.done) {
|
||||
|
||||
if (status.forms === undefined) {
|
||||
console.error('Image interrogation request failed: no forms found.');
|
||||
|
@ -251,7 +250,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
|
|||
return response.send({ caption });
|
||||
}
|
||||
|
||||
if (status.state === AIHorde.HordeAsyncRequestStates.faulted || status.state === AIHorde.HordeAsyncRequestStates.cancelled) {
|
||||
if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) {
|
||||
console.log('Image interrogation request is not successful.');
|
||||
return response.sendStatus(503);
|
||||
}
|
||||
|
@ -325,6 +324,8 @@ router.post('/generate-image', jsonParser, async (request, response) => {
|
|||
width: request.body.width,
|
||||
height: request.body.height,
|
||||
karras: Boolean(request.body.karras),
|
||||
clip_skip: request.body.clip_skip,
|
||||
seed: request.body.seed >= 0 ? String(request.body.seed) : undefined,
|
||||
n: 1,
|
||||
},
|
||||
r2: false,
|
||||
|
|
|
@ -255,7 +255,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
|
|||
height: request.body.height ?? 512,
|
||||
width: request.body.width ?? 512,
|
||||
scale: request.body.scale ?? 9,
|
||||
seed: Math.floor(Math.random() * 9999999999),
|
||||
seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 9999999999),
|
||||
sampler: request.body.sampler ?? 'k_dpmpp_2m',
|
||||
steps: request.body.steps ?? 28,
|
||||
n_samples: 1,
|
||||
|
@ -264,7 +264,7 @@ router.post('/generate-image', jsonParser, async (request, response) => {
|
|||
qualityToggle: false,
|
||||
add_original_image: false,
|
||||
controlnet_strength: 1,
|
||||
dynamic_thresholding: false,
|
||||
dynamic_thresholding: request.body.decrisper ?? false,
|
||||
legacy: false,
|
||||
sm: request.body.sm ?? false,
|
||||
sm_dyn: request.body.sm_dyn ?? false,
|
||||
|
|
|
@ -110,10 +110,6 @@ function readPresetsFromDirectory(directoryPath, options = {}) {
|
|||
|
||||
async function backupSettings() {
|
||||
try {
|
||||
if (!fs.existsSync(PUBLIC_DIRECTORIES.backups)) {
|
||||
fs.mkdirSync(PUBLIC_DIRECTORIES.backups);
|
||||
}
|
||||
|
||||
const userHandles = await getAllUserHandles();
|
||||
|
||||
for (const handle of userHandles) {
|
||||
|
@ -131,7 +127,7 @@ async function backupSettings() {
|
|||
*/
|
||||
function backupUserSettings(handle) {
|
||||
const userDirectories = getUserDirectories(handle);
|
||||
const backupFile = path.join(PUBLIC_DIRECTORIES.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`);
|
||||
const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`);
|
||||
const sourceFile = path.join(userDirectories.root, SETTINGS_FILE);
|
||||
|
||||
if (!fs.existsSync(sourceFile)) {
|
||||
|
@ -139,7 +135,7 @@ function backupUserSettings(handle) {
|
|||
}
|
||||
|
||||
fs.copyFileSync(sourceFile, backupFile);
|
||||
removeOldBackups(`settings_${handle}`);
|
||||
removeOldBackups(userDirectories.backups, `settings_${handle}`);
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
@ -227,12 +223,12 @@ router.post('/get', jsonParser, (request, response) => {
|
|||
|
||||
router.post('/get-snapshots', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const snapshots = fs.readdirSync(PUBLIC_DIRECTORIES.backups);
|
||||
const snapshots = fs.readdirSync(request.user.directories.backups);
|
||||
const userFilesPattern = getFilePrefix(request.user.profile.handle);
|
||||
const userSnapshots = snapshots.filter(x => x.startsWith(userFilesPattern));
|
||||
|
||||
const result = userSnapshots.map(x => {
|
||||
const stat = fs.statSync(path.join(PUBLIC_DIRECTORIES.backups, x));
|
||||
const stat = fs.statSync(path.join(request.user.directories.backups, x));
|
||||
return { date: stat.ctimeMs, name: x, size: stat.size };
|
||||
});
|
||||
|
||||
|
@ -252,7 +248,7 @@ router.post('/load-snapshot', jsonParser, async (request, response) => {
|
|||
}
|
||||
|
||||
const snapshotName = request.body.name;
|
||||
const snapshotPath = path.join(PUBLIC_DIRECTORIES.backups, snapshotName);
|
||||
const snapshotPath = path.join(request.user.directories.backups, snapshotName);
|
||||
|
||||
if (!fs.existsSync(snapshotPath)) {
|
||||
return response.sendStatus(404);
|
||||
|
@ -286,7 +282,7 @@ router.post('/restore-snapshot', jsonParser, async (request, response) => {
|
|||
}
|
||||
|
||||
const snapshotName = request.body.name;
|
||||
const snapshotPath = path.join(PUBLIC_DIRECTORIES.backups, snapshotName);
|
||||
const snapshotPath = path.join(request.user.directories.backups, snapshotName);
|
||||
|
||||
if (!fs.existsSync(snapshotPath)) {
|
||||
return response.sendStatus(404);
|
||||
|
|
|
@ -160,6 +160,31 @@ router.post('/samplers', jsonParser, async (request, response) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.post('/schedulers', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.body.url);
|
||||
url.pathname = '/sdapi/v1/schedulers';
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': getBasicAuthHeader(request.body.auth),
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error('SD WebUI returned an error.');
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
const names = data.map(x => x.name);
|
||||
return response.send(names);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/models', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.body.url);
|
||||
|
@ -608,8 +633,10 @@ together.post('/generate', jsonParser, async (request, response) => {
|
|||
model: request.body.model,
|
||||
steps: request.body.steps,
|
||||
n: 1,
|
||||
seed: Math.floor(Math.random() * 10_000_000), // Limited to 10000 on playground, works fine with more.
|
||||
sessionKey: getHexString(40), // Don't know if that's supposed to be random or not. It works either way.
|
||||
// Limited to 10000 on playground, works fine with more.
|
||||
seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000),
|
||||
// Don't know if that's supposed to be random or not. It works either way.
|
||||
sessionKey: getHexString(40),
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -676,6 +703,23 @@ drawthings.post('/get-model', jsonParser, async (request, response) => {
|
|||
}
|
||||
});
|
||||
|
||||
drawthings.post('/get-upscaler', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.body.url);
|
||||
url.pathname = '/';
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
const data = await result.json();
|
||||
|
||||
return response.send(data['upscaler']);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
drawthings.post('/generate', jsonParser, async (request, response) => {
|
||||
try {
|
||||
console.log('SD DrawThings API request:', request.body);
|
||||
|
@ -719,7 +763,7 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
|
|||
const params = new URLSearchParams({
|
||||
model: String(request.body.model),
|
||||
negative_prompt: String(request.body.negative_prompt),
|
||||
seed: String(Math.floor(Math.random() * 10_000_000)),
|
||||
seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)),
|
||||
enhance: String(request.body.enhance ?? false),
|
||||
refine: String(request.body.refine ?? false),
|
||||
width: String(request.body.width ?? 1024),
|
||||
|
|
|
@ -5,7 +5,18 @@ const sanitize = require('sanitize-filename');
|
|||
const { jsonParser } = require('../express-common');
|
||||
|
||||
// Don't forget to add new sources to the SOURCES array
|
||||
const SOURCES = ['transformers', 'mistral', 'openai', 'extras', 'palm', 'togetherai', 'nomicai', 'cohere'];
|
||||
const SOURCES = [
|
||||
'transformers',
|
||||
'mistral',
|
||||
'openai',
|
||||
'extras',
|
||||
'palm',
|
||||
'togetherai',
|
||||
'nomicai',
|
||||
'cohere',
|
||||
'ollama',
|
||||
'llamacpp',
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from the given source.
|
||||
|
@ -32,6 +43,10 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
|
|||
return require('../vectors/makersuite-vectors').getMakerSuiteVector(text, directories);
|
||||
case 'cohere':
|
||||
return require('../vectors/cohere-vectors').getCohereVector(text, isQuery, directories, sourceSettings.model);
|
||||
case 'llamacpp':
|
||||
return require('../vectors/llamacpp-vectors').getLlamaCppVector(text, sourceSettings.apiUrl, directories);
|
||||
case 'ollama':
|
||||
return require('../vectors/ollama-vectors').getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown vector source ${source}`);
|
||||
|
@ -73,6 +88,12 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
|
|||
case 'cohere':
|
||||
results.push(...await require('../vectors/cohere-vectors').getCohereBatchVector(batch, isQuery, directories, sourceSettings.model));
|
||||
break;
|
||||
case 'llamacpp':
|
||||
results.push(...await require('../vectors/llamacpp-vectors').getLlamaCppBatchVector(batch, sourceSettings.apiUrl, directories));
|
||||
break;
|
||||
case 'ollama':
|
||||
results.push(...await require('../vectors/ollama-vectors').getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown vector source ${source}`);
|
||||
}
|
||||
|
@ -251,7 +272,23 @@ function getSourceSettings(source, request) {
|
|||
return {
|
||||
model: model,
|
||||
};
|
||||
}else {
|
||||
} else if (source === 'llamacpp') {
|
||||
const apiUrl = String(request.headers['x-llamacpp-url']);
|
||||
|
||||
return {
|
||||
apiUrl: apiUrl,
|
||||
};
|
||||
} else if (source === 'ollama') {
|
||||
const apiUrl = String(request.headers['x-ollama-url']);
|
||||
const model = String(request.headers['x-ollama-model']);
|
||||
const keep = Boolean(request.headers['x-ollama-keep']);
|
||||
|
||||
return {
|
||||
apiUrl: apiUrl,
|
||||
model: model,
|
||||
keep: keep,
|
||||
};
|
||||
} else {
|
||||
// Extras API settings to connect to the Extras embeddings provider
|
||||
let extrasUrl = '';
|
||||
let extrasKey = '';
|
||||
|
@ -267,6 +304,35 @@ function getSourceSettings(source, request) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a request to regenerate the index if it is corrupted.
|
||||
* @param {import('express').Request} req Express request object
|
||||
* @param {import('express').Response} res Express response object
|
||||
* @param {Error} error Error object
|
||||
* @returns {Promise<any>} Promise
|
||||
*/
|
||||
async function regenerateCorruptedIndexErrorHandler(req, res, error) {
|
||||
if (error instanceof SyntaxError && !req.query.regenerated) {
|
||||
const collectionId = String(req.body.collectionId);
|
||||
const source = String(req.body.source) || 'transformers';
|
||||
|
||||
if (collectionId && source) {
|
||||
const index = await getIndex(req.user.directories, collectionId, source, false);
|
||||
const exists = await index.isIndexCreated();
|
||||
|
||||
if (exists) {
|
||||
const path = index.folderPath;
|
||||
console.error(`Corrupted index detected at ${path}, regenerating...`);
|
||||
await index.deleteIndex();
|
||||
return res.redirect(307, req.originalUrl + '?regenerated=true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/query', jsonParser, async (req, res) => {
|
||||
|
@ -285,8 +351,7 @@ router.post('/query', jsonParser, async (req, res) => {
|
|||
const results = await queryCollection(req.user.directories, collectionId, source, sourceSettings, searchText, topK, threshold);
|
||||
return res.json(results);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
return regenerateCorruptedIndexErrorHandler(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -306,8 +371,7 @@ router.post('/query-multi', jsonParser, async (req, res) => {
|
|||
const results = await multiQueryCollection(req.user.directories, collectionIds, source, sourceSettings, searchText, topK, threshold);
|
||||
return res.json(results);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
return regenerateCorruptedIndexErrorHandler(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -325,8 +389,7 @@ router.post('/insert', jsonParser, async (req, res) => {
|
|||
await insertVectorItems(req.user.directories, collectionId, source, sourceSettings, items);
|
||||
return res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
return regenerateCorruptedIndexErrorHandler(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -342,8 +405,7 @@ router.post('/list', jsonParser, async (req, res) => {
|
|||
const hashes = await getSavedHashes(req.user.directories, collectionId, source);
|
||||
return res.json(hashes);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
return regenerateCorruptedIndexErrorHandler(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -360,8 +422,7 @@ router.post('/delete', jsonParser, async (req, res) => {
|
|||
await deleteVectorItems(req.user.directories, collectionId, source, hashes);
|
||||
return res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
return regenerateCorruptedIndexErrorHandler(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ const STORAGE_KEYS = {
|
|||
* @property {string} comfyWorkflows - The directory where the ComfyUI workflows are stored
|
||||
* @property {string} files - The directory where the uploaded files are stored
|
||||
* @property {string} vectors - The directory where the vectors are stored
|
||||
* @property {string} backups - The directory where the backups are stored
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
12
src/util.js
12
src/util.js
|
@ -9,8 +9,6 @@ const yaml = require('yaml');
|
|||
const { default: simpleGit } = require('simple-git');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const { PUBLIC_DIRECTORIES } = require('./constants');
|
||||
|
||||
/**
|
||||
* Parsed config object.
|
||||
*/
|
||||
|
@ -360,14 +358,16 @@ function generateTimestamp() {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} prefix
|
||||
* Remove old backups with the given prefix from a specified directory.
|
||||
* @param {string} directory The root directory to remove backups from.
|
||||
* @param {string} prefix File prefix to filter backups by.
|
||||
*/
|
||||
function removeOldBackups(prefix) {
|
||||
function removeOldBackups(directory, prefix) {
|
||||
const MAX_BACKUPS = 50;
|
||||
|
||||
let files = fs.readdirSync(PUBLIC_DIRECTORIES.backups).filter(f => f.startsWith(prefix));
|
||||
let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
|
||||
if (files.length > MAX_BACKUPS) {
|
||||
files = files.map(f => path.join(PUBLIC_DIRECTORIES.backups, f));
|
||||
files = files.map(f => path.join(directory, f));
|
||||
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
|
||||
|
||||
fs.rmSync(files[0]);
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
const fetch = require('node-fetch').default;
|
||||
const { setAdditionalHeadersByType } = require('../additional-headers');
|
||||
const { TEXTGEN_TYPES } = require('../constants');
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from LlamaCpp
|
||||
* @param {string[]} texts - The array of texts to get the vectors for
|
||||
* @param {string} apiUrl - The API URL
|
||||
* @param {import('../users').UserDirectoryList} directories - The directories object for the user
|
||||
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||
*/
|
||||
async function getLlamaCppBatchVector(texts, apiUrl, directories) {
|
||||
const url = new URL(apiUrl);
|
||||
url.pathname = '/v1/embeddings';
|
||||
|
||||
const headers = {};
|
||||
setAdditionalHeadersByType(headers, TEXTGEN_TYPES.LLAMACPP, apiUrl, directories);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify({ input: texts }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(`LlamaCpp: Failed to get vector for text: ${response.statusText} ${responseText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data?.data)) {
|
||||
throw new Error('API response was not an array');
|
||||
}
|
||||
|
||||
// Sort data by x.index to ensure the order is correct
|
||||
data.data.sort((a, b) => a.index - b.index);
|
||||
|
||||
const vectors = data.data.map(x => x.embedding);
|
||||
return vectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from LlamaCpp
|
||||
* @param {string} text - The text to get the vector for
|
||||
* @param {string} apiUrl - The API URL
|
||||
* @param {import('../users').UserDirectoryList} directories - The directories object for the user
|
||||
* @returns {Promise<number[]>} - The vector for the text
|
||||
*/
|
||||
async function getLlamaCppVector(text, apiUrl, directories) {
|
||||
const vectors = await getLlamaCppBatchVector([text], apiUrl, directories);
|
||||
return vectors[0];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLlamaCppBatchVector,
|
||||
getLlamaCppVector,
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
const fetch = require('node-fetch').default;
|
||||
const { setAdditionalHeadersByType } = require('../additional-headers');
|
||||
const { TEXTGEN_TYPES } = require('../constants');
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from Ollama
|
||||
* @param {string[]} texts - The array of texts to get the vectors for
|
||||
* @param {string} apiUrl - The API URL
|
||||
* @param {string} model - The model to use
|
||||
* @param {boolean} keep - Keep the model loaded in memory
|
||||
* @param {import('../users').UserDirectoryList} directories - The directories object for the user
|
||||
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||
*/
|
||||
async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {
|
||||
const result = [];
|
||||
for (const text of texts) {
|
||||
const vector = await getOllamaVector(text, apiUrl, model, keep, directories);
|
||||
result.push(vector);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vector for the given text from Ollama
|
||||
* @param {string} text - The text to get the vector for
|
||||
* @param {string} apiUrl - The API URL
|
||||
* @param {string} model - The model to use
|
||||
* @param {boolean} keep - Keep the model loaded in memory
|
||||
* @param {import('../users').UserDirectoryList} directories - The directories object for the user
|
||||
* @returns {Promise<number[]>} - The vector for the text
|
||||
*/
|
||||
async function getOllamaVector(text, apiUrl, model, keep, directories) {
|
||||
const url = new URL(apiUrl);
|
||||
url.pathname = '/api/embeddings';
|
||||
|
||||
const headers = {};
|
||||
setAdditionalHeadersByType(headers, TEXTGEN_TYPES.OLLAMA, apiUrl, directories);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: text,
|
||||
model: model,
|
||||
keep_alive: keep ? -1 : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(`Ollama: Failed to get vector for text: ${response.statusText} ${responseText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data?.embedding)) {
|
||||
throw new Error('API response was not an array');
|
||||
}
|
||||
|
||||
return data.embedding;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOllamaBatchVector,
|
||||
getOllamaVector,
|
||||
};
|
Loading…
Reference in New Issue