Merge remote-tracking branch 'upstream/staging' into staging

This commit is contained in:
DonMoralez
2023-12-22 17:12:59 +02:00
12 changed files with 911 additions and 874 deletions

View File

@ -121,7 +121,7 @@
} }
/* Add the custom checkbox */ /* Add the custom checkbox */
.select2-results__option:before { .select2-results__option::before {
content: ''; content: '';
display: inline-block; display: inline-block;
position: absolute; position: absolute;
@ -141,11 +141,19 @@
} }
/* Add the custom checkbox checkmark */ /* Add the custom checkbox checkmark */
.select2-results__option--selected.select2-results__option:before { .select2-results__option--selected.select2-results__option::before {
content: '\2713'; content: '\2713';
font-weight: bold; font-weight: bold;
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
background-color: var(--SmartThemeBlurTintColor); background-color: var(--SmartThemeBlurTintColor);
text-align: center; text-align: center;
line-height: 14px; line-height: 14px;
} }
.select2-results__option.select2-results__message {
background-color: inherit;
}
.select2-results__option.select2-results__message::before {
display: none;
}

View File

@ -3847,7 +3847,7 @@
</div> </div>
<div id="rm_character_import" class="right_menu" style="display: none;"> <div id="rm_character_import" class="right_menu" style="display: none;">
<form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input multiple type="file" id="character_import_file" accept=".json, image/png" name="avatar"> <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml" name="avatar">
<input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off"> <input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">
</form> </form>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -47,8 +47,6 @@ export function saveMetadataDebounced() {
}, 1000); }, 1000);
} }
export const extensionsHandlebars = Handlebars.create();
/** /**
* Provides an ability for extensions to render HTML templates. * Provides an ability for extensions to render HTML templates.
* Templates sanitation and localization is forced. * Templates sanitation and localization is forced.
@ -61,40 +59,6 @@ export function renderExtensionTemplate(extensionName, templateId, templateData
return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true); return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
} }
/**
* Registers a Handlebars helper for use in extensions.
* @param {string} name Handlebars helper name
* @param {function} helper Handlebars helper function
*/
export function registerExtensionHelper(name, helper) {
extensionsHandlebars.registerHelper(name, helper);
}
/**
* Applies handlebars extension helpers to a message.
* @param {number} messageId Message index in the chat.
*/
export function processExtensionHelpers(messageId) {
const context = getContext();
const message = context.chat[messageId];
if (!message?.mes || typeof message.mes !== 'string') {
return;
}
// Don't waste time if there are no mustaches
if (!substituteParams(message.mes).includes('{{')) {
return;
}
try {
const template = extensionsHandlebars.compile(substituteParams(message.mes), { noEscape: true });
message.mes = template({});
} catch {
// Ignore
}
}
// Disables parallel updates // Disables parallel updates
class ModuleWorkerWrapper { class ModuleWorkerWrapper {
constructor(callback) { constructor(callback) {

View File

@ -12,6 +12,7 @@ import {
} from '../../../script.js'; } from '../../../script.js';
import { extension_settings, getContext } from '../../extensions.js'; import { extension_settings, getContext } from '../../extensions.js';
import { secret_state, writeSecret } from '../../secrets.js'; import { secret_state, writeSecret } from '../../secrets.js';
import { splitRecursive } from '../../utils.js';
export const autoModeOptions = { export const autoModeOptions = {
NONE: 'none', NONE: 'none',
@ -315,6 +316,28 @@ async function translateProviderBing(text, lang) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
/**
* Splits text into chunks and translates each chunk separately
* @param {string} text Text to translate
* @param {string} lang Target language code
* @param {(text: string, lang: string) => Promise<string>} translateFn Function to translate a single chunk (must return a Promise)
* @param {number} chunkSize Maximum chunk size
* @returns {Promise<string>} Translated text
*/
async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
if (text.length <= chunkSize) {
return await translateFn(text, lang);
}
const chunks = splitRecursive(text, chunkSize);
let result = '';
for (const chunk of chunks) {
result += await translateFn(chunk, lang);
}
return result;
}
/** /**
* Translates text using the selected translation provider * Translates text using the selected translation provider
* @param {string} text Text to translate * @param {string} text Text to translate
@ -331,15 +354,15 @@ async function translate(text, lang) {
case 'libre': case 'libre':
return await translateProviderLibre(text, lang); return await translateProviderLibre(text, lang);
case 'google': case 'google':
return await translateProviderGoogle(text, lang); return await chunkedTranslate(text, lang, translateProviderGoogle, 5000);
case 'deepl': case 'deepl':
return await translateProviderDeepl(text, lang); return await translateProviderDeepl(text, lang);
case 'deeplx': case 'deeplx':
return await translateProviderDeepLX(text, lang); return await chunkedTranslate(text, lang, translateProviderDeepLX, 1500);
case 'oneringtranslator': case 'oneringtranslator':
return await translateProviderOneRing(text, lang); return await translateProviderOneRing(text, lang);
case 'bing': case 'bing':
return await translateProviderBing(text, lang); return await chunkedTranslate(text, lang, translateProviderBing, 1000);
default: default:
console.error('Unknown translation provider', extension_settings.translate.provider); console.error('Unknown translation provider', extension_settings.translate.provider);
return text; return text;

View File

@ -21,7 +21,6 @@ import {
MAX_INJECTION_DEPTH, MAX_INJECTION_DEPTH,
name1, name1,
name2, name2,
replaceBiasMarkup,
replaceItemizedPromptText, replaceItemizedPromptText,
resultCheckStatus, resultCheckStatus,
saveSettingsDebounced, saveSettingsDebounced,
@ -443,8 +442,6 @@ function setOpenAIMessages(chat) {
content = `${chat[j].name}: ${content}`; content = `${chat[j].name}: ${content}`;
} }
} }
content = replaceBiasMarkup(content);
// remove caret return (waste of tokens) // remove caret return (waste of tokens)
content = content.replace(/\r/gm, ''); content = content.replace(/\r/gm, '');

View File

@ -20,7 +20,7 @@ import {
main_api, main_api,
name1, name1,
reloadCurrentChat, reloadCurrentChat,
replaceBiasMarkup, removeMacros,
saveChatConditional, saveChatConditional,
sendMessageAsUser, sendMessageAsUser,
sendSystemMessage, sendSystemMessage,
@ -1260,7 +1260,7 @@ export async function sendMessageAs(args, text) {
// Messages that do nothing but set bias will be hidden from the context // Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(mesText); const bias = extractMessageBias(mesText);
const isSystem = replaceBiasMarkup(mesText).trim().length === 0; const isSystem = bias && !removeMacros(mesText).length;
const character = characters.find(x => x.name === name); const character = characters.find(x => x.name === name);
let force_avatar, original_avatar; let force_avatar, original_avatar;
@ -1313,7 +1313,7 @@ export async function sendNarratorMessage(args, text) {
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT; const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
// Messages that do nothing but set bias will be hidden from the context // Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(text); const bias = extractMessageBias(text);
const isSystem = replaceBiasMarkup(text).trim().length === 0; const isSystem = bias && !removeMacros(text).length;
const message = { const message = {
name: name, name: name,

View File

@ -637,6 +637,9 @@ hr {
order: 2; order: 2;
padding-right: 2px; padding-right: 2px;
place-self: center; place-self: center;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
} }
#options_button { #options_button {

View File

@ -5,7 +5,7 @@ const Readable = require('stream').Readable;
const { jsonParser } = require('../../express-common'); const { jsonParser } = require('../../express-common');
const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS } = require('../../constants'); const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS } = require('../../constants');
const { forwardFetchResponse } = require('../../util'); const { forwardFetchResponse, trimV1 } = require('../../util');
const { setAdditionalHeaders } = require('../../additional-headers'); const { setAdditionalHeaders } = require('../../additional-headers');
const router = express.Router(); const router = express.Router();
@ -57,6 +57,26 @@ async function parseOllamaStream(jsonStream, request, response) {
} }
} }
/**
* Abort KoboldCpp generation request.
* @param {string} url Server base URL
* @returns {Promise<void>} Promise resolving when we are done
*/
async function abortKoboldCppRequest(url) {
try {
console.log('Aborting Kobold generation...');
const abortResponse = await fetch(`${url}/api/extra/abort`, {
method: 'POST',
});
if (!abortResponse.ok) {
console.log('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
}
} catch (error) {
console.log(error);
}
}
//************** Ooba/OpenAI text completions API //************** Ooba/OpenAI text completions API
router.post('/status', jsonParser, async function (request, response) { router.post('/status', jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400); if (!request.body) return response.sendStatus(400);
@ -67,9 +87,7 @@ router.post('/status', jsonParser, async function (request, response) {
} }
console.log('Trying to connect to API:', request.body); console.log('Trying to connect to API:', request.body);
const baseUrl = trimV1(request.body.api_server);
// Convert to string + remove trailing slash + /v1 suffix
const baseUrl = String(request.body.api_server).replace(/\/$/, '').replace(/\/v1$/, '');
const args = { const args = {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -195,12 +213,15 @@ router.post('/generate', jsonParser, async function (request, response) {
const controller = new AbortController(); const controller = new AbortController();
request.socket.removeAllListeners('close'); request.socket.removeAllListeners('close');
request.socket.on('close', function () { request.socket.on('close', async function () {
if (request.body.api_type === TEXTGEN_TYPES.KOBOLDCPP && !response.writableEnded) {
await abortKoboldCppRequest(trimV1(baseUrl));
}
controller.abort(); controller.abort();
}); });
// Convert to string + remove trailing slash + /v1 suffix let url = trimV1(baseUrl);
let url = String(baseUrl).replace(/\/$/, '').replace(/\/v1$/, '');
if (request.body.legacy_api) { if (request.body.legacy_api) {
url += '/v1/generate'; url += '/v1/generate';
@ -337,8 +358,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
} }
console.log('Ollama caption request:', request.body); console.log('Ollama caption request:', request.body);
// Convert to string + remove trailing slash + /v1 suffix const baseUrl = trimV1(request.body.server_url);
const baseUrl = String(request.body.server_url).replace(/\/$/, '').replace(/\/v1$/, '');
const fetchResponse = await fetch(`${baseUrl}/api/generate`, { const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
method: 'POST', method: 'POST',
@ -383,8 +403,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
} }
console.log('LlamaCpp caption request:', request.body); console.log('LlamaCpp caption request:', request.body);
// Convert to string + remove trailing slash + /v1 suffix const baseUrl = trimV1(request.body.server_url);
const baseUrl = String(request.body.server_url).replace(/\/$/, '').replace(/\/v1$/, '');
const fetchResponse = await fetch(`${baseUrl}/completion`, { const fetchResponse = await fetch(`${baseUrl}/completion`, {
method: 'POST', method: 'POST',

View File

@ -4,6 +4,7 @@ const readline = require('readline');
const express = require('express'); const express = require('express');
const sanitize = require('sanitize-filename'); const sanitize = require('sanitize-filename');
const writeFileAtomicSync = require('write-file-atomic').sync; const writeFileAtomicSync = require('write-file-atomic').sync;
const yaml = require('yaml');
const _ = require('lodash'); const _ = require('lodash');
const encode = require('png-chunks-encode'); const encode = require('png-chunks-encode');
@ -19,6 +20,7 @@ const characterCardParser = require('../character-card-parser.js');
const { readWorldInfoFile } = require('./worldinfo'); const { readWorldInfoFile } = require('./worldinfo');
const { invalidateThumbnail } = require('./thumbnails'); const { invalidateThumbnail } = require('./thumbnails');
const { importRisuSprites } = require('./sprites'); const { importRisuSprites } = require('./sprites');
const defaultAvatarPath = './public/img/ai4.png';
let characters = {}; let characters = {};
@ -394,6 +396,36 @@ function convertWorldInfoToCharacterBook(name, entries) {
return result; return result;
} }
/**
* Import a character from a YAML file.
* @param {string} uploadPath Path to the uploaded file
* @param {import('express').Response} response Express response object
*/
function importFromYaml(uploadPath, response) {
const fileText = fs.readFileSync(uploadPath, 'utf8');
fs.rmSync(uploadPath);
const yamlData = yaml.parse(fileText);
console.log('importing from yaml');
yamlData.name = sanitize(yamlData.name);
const fileName = getPngName(yamlData.name);
let char = convertToV2({
'name': yamlData.name,
'description': yamlData.context ?? '',
'first_mes': yamlData.greeting ?? '',
'create_date': humanizedISO8601DateTime(),
'chat': `${yamlData.name} - ${humanizedISO8601DateTime()}`,
'personality': '',
'creatorcomment': '',
'avatar': 'none',
'mes_example': '',
'scenario': '',
'talkativeness': 0.5,
'creator': '',
'tags': '',
});
charaWrite(defaultAvatarPath, JSON.stringify(char), fileName, response, { file_name: fileName });
}
const router = express.Router(); const router = express.Router();
router.post('/create', urlencodedParser, async function (request, response) { router.post('/create', urlencodedParser, async function (request, response) {
@ -760,144 +792,147 @@ function getPngName(file) {
} }
router.post('/import', urlencodedParser, async function (request, response) { router.post('/import', urlencodedParser, async function (request, response) {
if (!request.body || !request.file) return response.sendStatus(400);
if (!request.body || request.file === undefined) return response.sendStatus(400);
let png_name = ''; let png_name = '';
let filedata = request.file; let filedata = request.file;
let uploadPath = path.join(UPLOADS_PATH, filedata.filename); let uploadPath = path.join(UPLOADS_PATH, filedata.filename);
var format = request.body.file_type; let format = request.body.file_type;
const defaultAvatarPath = './public/img/ai4.png';
//console.log(format);
if (filedata) {
if (format == 'json') {
fs.readFile(uploadPath, 'utf8', async (err, data) => {
fs.unlinkSync(uploadPath);
if (err) { if (format == 'yaml' || format == 'yml') {
console.log(err); try {
response.send({ error: true }); importFromYaml(uploadPath, response);
} } catch (err) {
console.log(err);
response.send({ error: true });
}
} else if (format == 'json') {
fs.readFile(uploadPath, 'utf8', async (err, data) => {
fs.unlinkSync(uploadPath);
let jsonData = JSON.parse(data); if (err) {
if (jsonData.spec !== undefined) {
console.log('importing from v2 json');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
png_name = getPngName(jsonData.data?.name || jsonData.name);
let char = JSON.stringify(jsonData);
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.name !== undefined) {
console.log('importing from v1 json');
jsonData.name = sanitize(jsonData.name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.name);
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.char_name);
let char = {
'name': jsonData.char_name,
'description': jsonData.char_persona ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': '',
'first_mes': jsonData.char_greeting ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.example_dialogue ?? '',
'scenario': jsonData.world_scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
} else {
console.log('Incorrect character format .json');
response.send({ error: true });
}
});
} else {
try {
var img_data = await charaRead(uploadPath, format);
if (img_data === undefined) throw new Error('Failed to read character data');
let jsonData = JSON.parse(img_data);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
png_name = getPngName(jsonData.name);
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
const char = JSON.stringify(jsonData);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
const charJSON = JSON.stringify(char);
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else {
console.log('Unknown character card format');
response.send({ error: true });
}
} catch (err) {
console.log(err); console.log(err);
response.send({ error: true }); response.send({ error: true });
} }
let jsonData = JSON.parse(data);
if (jsonData.spec !== undefined) {
console.log('importing from v2 json');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
png_name = getPngName(jsonData.data?.name || jsonData.name);
let char = JSON.stringify(jsonData);
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.name !== undefined) {
console.log('importing from v1 json');
jsonData.name = sanitize(jsonData.name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.name);
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
png_name = getPngName(jsonData.char_name);
let char = {
'name': jsonData.char_name,
'description': jsonData.char_persona ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': '',
'first_mes': jsonData.char_greeting ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.example_dialogue ?? '',
'scenario': jsonData.world_scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
let charJSON = JSON.stringify(char);
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
} else {
console.log('Incorrect character format .json');
response.send({ error: true });
}
});
} else {
try {
var img_data = await charaRead(uploadPath, format);
if (img_data === undefined) throw new Error('Failed to read character data');
let jsonData = JSON.parse(img_data);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
png_name = getPngName(jsonData.name);
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
const char = JSON.stringify(jsonData);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
if (jsonData.creator_notes) {
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
}
let char = {
'name': jsonData.name,
'description': jsonData.description ?? '',
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
};
char = convertToV2(char);
const charJSON = JSON.stringify(char);
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else {
console.log('Unknown character card format');
response.send({ error: true });
}
} catch (err) {
console.log(err);
response.send({ error: true });
} }
} }
}); });

View File

@ -36,20 +36,12 @@ function sanitizeHordeImagePrompt(prompt) {
prompt = prompt.replace(/\b(boy)\b/gmi, 'man'); prompt = prompt.replace(/\b(boy)\b/gmi, 'man');
prompt = prompt.replace(/\b(girls)\b/gmi, 'women'); prompt = prompt.replace(/\b(girls)\b/gmi, 'women');
prompt = prompt.replace(/\b(boys)\b/gmi, 'men'); prompt = prompt.replace(/\b(boys)\b/gmi, 'men');
//always remove these high risk words from prompt, as they add little value to image gen while increasing the risk the prompt gets flagged //always remove these high risk words from prompt, as they add little value to image gen while increasing the risk the prompt gets flagged
prompt = prompt.replace(/\b(under.age|under.aged|underage|underaged|loli|pedo|pedophile|(\w+).year.old|(\w+).years.old|minor|prepubescent|minors|shota)\b/gmi, ''); prompt = prompt.replace(/\b(under.age|under.aged|underage|underaged|loli|pedo|pedophile|(\w+).year.old|(\w+).years.old|minor|prepubescent|minors|shota)\b/gmi, '');
//replace risky subject nouns with person
//if nsfw is detected, do not remove it but apply additional precautions prompt = prompt.replace(/\b(youngster|infant|baby|toddler|child|teen|kid|kiddie|kiddo|teenager|student|preteen|pre.teen)\b/gmi, 'person');
let isNsfw = prompt.match(/\b(cock|ahegao|hentai|uncensored|lewd|cocks|deepthroat|deepthroating|dick|dicks|cumshot|lesbian|fuck|fucked|fucking|sperm|naked|nipples|tits|boobs|breasts|boob|breast|topless|ass|butt|fingering|masturbate|masturbating|bitch|blowjob|pussy|piss|asshole|dildo|dildos|vibrator|erection|foreskin|handjob|nude|penis|porn|vibrator|virgin|vagina|vulva|threesome|orgy|bdsm|hickey|condom|testicles|anal|bareback|bukkake|creampie|stripper|strap-on|missionary|clitoris|clit|clitty|cowgirl|fleshlight|sex|buttplug|milf|oral|sucking|bondage|orgasm|scissoring|railed|slut|sluts|slutty|cumming|cunt|faggot|sissy|anal|anus|cum|semen|scat|nsfw|xxx|explicit|erotic|horny|aroused|jizz|moan|rape|raped|raping|throbbing|humping)\b/gmi); //remove risky adjectives and related words
prompt = prompt.replace(/\b(young|younger|youthful|youth|small|smaller|smallest|girly|boyish|lil|tiny|teenaged|lit[tl]le|school.aged|school|highschool|kindergarten|teens|children|kids)\b/gmi, '');
if (isNsfw) {
//replace risky subject nouns with person
prompt = prompt.replace(/\b(youngster|infant|baby|toddler|child|teen|kid|kiddie|kiddo|teenager|student|preteen|pre.teen)\b/gmi, 'person');
//remove risky adjectives and related words
prompt = prompt.replace(/\b(young|younger|youthful|youth|small|smaller|smallest|girly|boyish|lil|tiny|teenaged|lit[tl]le|school.aged|school|highschool|kindergarten|teens|children|kids)\b/gmi, '');
}
return prompt; return prompt;
} }

View File

@ -458,6 +458,15 @@ function excludeKeysByYaml(obj, yamlString) {
} }
} }
/**
* Removes trailing slash and /v1 from a string.
* @param {string} str Input string
* @returns {string} Trimmed string
*/
function trimV1(str) {
return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');
}
module.exports = { module.exports = {
getConfig, getConfig,
getConfigValue, getConfigValue,
@ -481,4 +490,5 @@ module.exports = {
getHexString, getHexString,
mergeObjectWithYaml, mergeObjectWithYaml,
excludeKeysByYaml, excludeKeysByYaml,
trimV1,
}; };