Compare commits

...

8 Commits

Author SHA1 Message Date
Blueprint Coding ea414fed4d
Merge 305afb3713 into be7eb8b2b5 2024-04-26 15:36:48 +00:00
Cohee be7eb8b2b5
Merge pull request #2143 from aisu-wata0/style_mes_block_overflow_y
style: `.mes_block { overflow-y: clip; }`
2024-04-26 18:36:17 +03:00
Cohee 3b6372431a
Merge pull request #2144 from sirius422/fix-json-export-extension
Add json extension to exported oai and LogitBias presets
2024-04-26 18:30:55 +03:00
sirius422 389ee7917f Add json extension to exported oai and LogitBias presets 2024-04-26 23:07:25 +08:00
Cohee 212e61d2a1 Lazy initialization of Claude tokenizer. Add JSDoc for tokenizer handlers 2024-04-26 15:17:02 +03:00
Cohee 1b60e4a013 Init user storage module before server listening 2024-04-26 14:09:40 +03:00
Aisu Wata 93cd93ada3 style: `.mes_block { overflow-y: clip; }` 2024-04-25 21:49:12 -03:00
Blueprint Coding 305afb3713 Added import function for AICharacterCards.com cards
Added ability to import cards directly from aicharactercards.com via it's api like Chub and Janny.
Video of it in action: https://streamable.com/gbfdtw

Just pass the last two slash vars from the url (the author and card title) from a page. EX: aicharcards/the-game-master to:
https://aicharactercards.com/wp-json/pngapi/v1/image/

In this example: https://aicharactercards.com/wp-json/pngapi/v1/image/aicharcards/the-game-master
2024-04-24 18:04:17 -06:00
13 changed files with 220 additions and 73 deletions

View File

@ -10516,6 +10516,7 @@ jQuery(async function () {
<li>Chub Lorebook (Direct Link or ID)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>JanitorAI Character (Direct Link or UUID)<br>Example: <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li>Pygmalion.chat Character (Direct Link or UUID)<br>Example: <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li>AICharacterCard.com Character (Direct Link or ID)<br>Example: <tt>AICC/aicharcards/the-game-master</tt></li>
<li>More coming soon...</li>
<ul>`;
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });

View File

@ -3250,7 +3250,8 @@ async function onExportPresetClick() {
delete preset.proxy_password;
const presetJsonString = JSON.stringify(preset, null, 4);
download(presetJsonString, oai_settings.preset_settings_openai, 'application/json');
const presetFileName = `${oai_settings.preset_settings_openai}.json`;
download(presetJsonString, presetFileName, 'application/json');
}
async function onLogitBiasPresetImportFileChange(e) {
@ -3298,7 +3299,8 @@ function onLogitBiasPresetExportClick() {
}
const presetJsonString = JSON.stringify(oai_settings.bias_presets[oai_settings.bias_preset_selected], null, 4);
download(presetJsonString, oai_settings.bias_preset_selected, 'application/json');
const presetFileName = `${oai_settings.bias_preset_selected}.json`;
download(presetJsonString, presetFileName, 'application/json');
}
async function onDeletePresetClick() {

View File

@ -1000,6 +1000,7 @@ body .panelControlBar {
padding-left: 10px;
width: 100%;
overflow-x: hidden;
overflow-y: clip;
}
.mes_text {

View File

@ -45,7 +45,6 @@ const {
forwardFetchResponse,
} = require('./src/util');
const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');
const { loadTokenizers } = require('./src/endpoints/tokenizers');
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
@ -543,22 +542,12 @@ const setupTasks = async function () {
}
console.log();
// TODO: do endpoint init functions depend on certain directories existing or not existing? They should be callable
// in any order for encapsulation reasons, but right now it's unknown if that would break anything.
await userModule.initUserStorage(dataRoot);
if (listen && !basicAuthMode && enableAccounts) {
await userModule.checkAccountsProtection();
}
await settingsEndpoint.init();
const directories = await userModule.ensurePublicDirectoriesExist();
await userModule.migrateUserData();
const directories = await userModule.getUserDirectoriesList();
await contentManager.checkForNewContent(directories);
await ensureThumbnailCache();
cleanUploads();
await loadTokenizers();
await settingsEndpoint.init();
await statsEndpoint.init();
const cleanupPlugins = await loadPlugins();
@ -581,7 +570,6 @@ const setupTasks = async function () {
exitProcess();
});
console.log('Launching...');
if (autorun) open(autorunUrl.toString());
@ -601,6 +589,9 @@ const setupTasks = async function () {
}
}
if (listen && !basicAuthMode && enableAccounts) {
await userModule.checkAccountsProtection();
}
};
/**
@ -642,21 +633,27 @@ function setWindowTitle(title) {
}
}
if (cliArguments.ssl) {
https.createServer(
{
cert: fs.readFileSync(cliArguments.certPath),
key: fs.readFileSync(cliArguments.keyPath),
}, app)
.listen(
Number(tavernUrl.port) || 443,
tavernUrl.hostname,
setupTasks,
);
} else {
http.createServer(app).listen(
Number(tavernUrl.port) || 80,
tavernUrl.hostname,
setupTasks,
);
}
// User storage module needs to be initialized before starting the server
userModule.initUserStorage(dataRoot)
.then(userModule.ensurePublicDirectoriesExist)
.then(userModule.migrateUserData)
.finally(() => {
if (cliArguments.ssl) {
https.createServer(
{
cert: fs.readFileSync(cliArguments.certPath),
key: fs.readFileSync(cliArguments.keyPath),
}, app)
.listen(
Number(tavernUrl.port) || 443,
tavernUrl.hostname,
setupTasks,
);
} else {
http.createServer(app).listen(
Number(tavernUrl.port) || 80,
tavernUrl.hostname,
setupTasks,
);
}
});

View File

@ -386,6 +386,40 @@ async function downloadJannyCharacter(uuid) {
throw new Error('Failed to download character');
}
//Download Character Cards from AICharactersCards.com (AICC) API.
async function downloadAICCCharacter(id) {
const apiURL = `https://aicharactercards.com/wp-json/pngapi/v1/image/${id}`;
try {
const response = await fetch(apiURL);
if (!response.ok) {
throw new Error(`Failed to download character: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || 'image/png'; // Default to 'image/png' if header is missing
const buffer = await response.buffer();
const fileName = `${sanitize(id)}.png`; // Assuming PNG, but adjust based on actual content or headers
return {
buffer: buffer,
fileName: fileName,
fileType: contentType
};
} catch (error) {
console.error('Error downloading character:', error);
throw error;
}
}
function parseAICC(url) {
const pattern = /^https?:\/\/aicharactercards\.com\/character-cards\/([^\/]+)\/([^\/]+)\/?$|([^\/]+)\/([^\/]+)$/;
const match = url.match(pattern);
if (match) {
// Match group 1 & 2 for full URL, 3 & 4 for relative path
return match[1] && match[2] ? `${match[1]}/${match[2]}` : `${match[3]}/${match[4]}`;
}
return null;
}
/**
* @param {String} url
* @returns {String | null } UUID of the character
@ -414,6 +448,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
const isJannnyContent = url.includes('janitorai');
const isPygmalionContent = url.includes('pygmalion.chat');
const isAICharacterCardsContent = url.includes('aicharactercards.com');
if (isPygmalionContent) {
const uuid = getUuidFromUrl(url);
@ -431,6 +466,13 @@ router.post('/importURL', jsonParser, async (request, response) => {
type = 'character';
result = await downloadJannyCharacter(uuid);
} else if (isAICharacterCardsContent) {
const AICCParsed = parseAICC(url);
if (!AICCParsed) {
return response.sendStatus(404);
}
type = 'character';
result = await downloadAICCCharacter(AICCParsed);
} else {
const chubParsed = parseChubUrl(url);
type = chubParsed?.type;
@ -469,6 +511,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
const isJannny = uuid.includes('_character');
const isPygmalion = (!isJannny && uuid.length == 36);
const isAICC = uuid.startsWith('AICC/');
const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
if (isPygmalion) {
@ -477,6 +520,10 @@ router.post('/importUUID', jsonParser, async (request, response) => {
} else if (isJannny) {
console.log('Downloading Janitor character:', uuid.split('_')[0]);
result = await downloadJannyCharacter(uuid.split('_')[0]);
} else if (isAICC) {
const [, author, card] = uuid.split('/');
console.log('Downloading AICC character:', `${author}/${card}`);
result = await downloadAICCCharacter(`${author}/${card}`);
} else {
if (uuidType === 'character') {
console.log('Downloading chub character:', uuid);

View File

@ -10,6 +10,10 @@ const { TEXTGEN_TYPES } = require('../constants');
const { jsonParser } = require('../express-common');
const { setAdditionalHeaders } = require('../additional-headers');
/**
* @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler
*/
/**
* @type {{[key: string]: import("@dqbd/tiktoken").Tiktoken}} Tokenizers cache
*/
@ -48,16 +52,30 @@ const TEXT_COMPLETION_MODELS = [
const CHARS_PER_TOKEN = 3.35;
/**
* Sentencepiece tokenizer for tokenizing text.
*/
class SentencePieceTokenizer {
/**
* @type {import('@agnai/sentencepiece-js').SentencePieceProcessor} Sentencepiece tokenizer instance
*/
#instance;
/**
* @type {string} Path to the tokenizer model
*/
#model;
/**
* Creates a new Sentencepiece tokenizer.
* @param {string} model Path to the tokenizer model
*/
constructor(model) {
this.#model = model;
}
/**
* Gets the Sentencepiece tokenizer instance.
* @returns {Promise<import('@agnai/sentencepiece-js').SentencePieceProcessor|null>} Sentencepiece tokenizer instance
*/
async get() {
if (this.#instance) {
@ -76,18 +94,61 @@ class SentencePieceTokenizer {
}
}
const spp_llama = new SentencePieceTokenizer('src/sentencepiece/llama.model');
const spp_nerd = new SentencePieceTokenizer('src/sentencepiece/nerdstash.model');
const spp_nerd_v2 = new SentencePieceTokenizer('src/sentencepiece/nerdstash_v2.model');
const spp_mistral = new SentencePieceTokenizer('src/sentencepiece/mistral.model');
const spp_yi = new SentencePieceTokenizer('src/sentencepiece/yi.model');
let claude_tokenizer;
/**
* Web tokenizer for tokenizing text.
*/
class WebTokenizer {
/**
* @type {Tokenizer} Web tokenizer instance
*/
#instance;
/**
* @type {string} Path to the tokenizer model
*/
#model;
/**
* Creates a new Web tokenizer.
* @param {string} model Path to the tokenizer model
*/
constructor(model) {
this.#model = model;
}
/**
* Gets the Web tokenizer instance.
* @returns {Promise<Tokenizer|null>} Web tokenizer instance
*/
async get() {
if (this.#instance) {
return this.#instance;
}
try {
const arrayBuffer = fs.readFileSync(this.#model).buffer;
this.#instance = await Tokenizer.fromJSON(arrayBuffer);
console.log('Instantiated the tokenizer for', path.parse(this.#model).name);
return this.#instance;
} catch (error) {
console.error('Web tokenizer failed to load: ' + this.#model, error);
return null;
}
}
}
const spp_llama = new SentencePieceTokenizer('src/tokenizers/llama.model');
const spp_nerd = new SentencePieceTokenizer('src/tokenizers/nerdstash.model');
const spp_nerd_v2 = new SentencePieceTokenizer('src/tokenizers/nerdstash_v2.model');
const spp_mistral = new SentencePieceTokenizer('src/tokenizers/mistral.model');
const spp_yi = new SentencePieceTokenizer('src/tokenizers/yi.model');
const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
const sentencepieceTokenizers = [
'llama',
'nerdstash',
'nerdstash_v2',
'mistral',
'yi',
];
/**
@ -112,6 +173,10 @@ function getSentencepiceTokenizer(model) {
return spp_nerd_v2;
}
if (model.includes('yi')) {
return spp_yi;
}
return null;
}
@ -168,13 +233,23 @@ async function getTiktokenChunks(tokenizer, ids) {
return chunks;
}
async function getWebTokenizersChunks(tokenizer, ids) {
/**
* Gets the token chunks for the given token IDs using the Web tokenizer.
* @param {Tokenizer} tokenizer Web tokenizer instance
* @param {number[]} ids Token IDs
* @returns {string[]} Token chunks
*/
function getWebTokenizersChunks(tokenizer, ids) {
const chunks = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const chunkText = await tokenizer.decode(new Uint32Array([id]));
for (let i = 0, lastProcessed = 0; i < ids.length; i++) {
const chunkIds = ids.slice(lastProcessed, i + 1);
const chunkText = tokenizer.decode(new Int32Array(chunkIds));
if (chunkText === '<27>') {
continue;
}
chunks.push(chunkText);
lastProcessed = i + 1;
}
return chunks;
@ -237,17 +312,12 @@ function getTiktokenTokenizer(model) {
return tokenizer;
}
async function loadClaudeTokenizer(modelPath) {
try {
const arrayBuffer = fs.readFileSync(modelPath).buffer;
const instance = await Tokenizer.fromJSON(arrayBuffer);
return instance;
} catch (error) {
console.error('Claude tokenizer failed to load: ' + modelPath, error);
return null;
}
}
/**
* Counts the tokens for the given messages using the Claude tokenizer.
* @param {Tokenizer} tokenizer Web tokenizer
* @param {object[]} messages Array of messages
* @returns {number} Number of tokens
*/
function countClaudeTokens(tokenizer, messages) {
// Should be fine if we use the old conversion method instead of the messages API one i think?
const convertedPrompt = convertClaudePrompt(messages, false, '', false, false, '', false);
@ -264,9 +334,14 @@ function countClaudeTokens(tokenizer, messages) {
/**
* Creates an API handler for encoding Sentencepiece tokens.
* @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
* @returns {any} Handler function
* @returns {TokenizationHandler} Handler function
*/
function createSentencepieceEncodingHandler(tokenizer) {
/**
* Request handler for encoding Sentencepiece tokens.
* @param {import('express').Request} request
* @param {import('express').Response} response
*/
return async function (request, response) {
try {
if (!request.body) {
@ -276,7 +351,7 @@ function createSentencepieceEncodingHandler(tokenizer) {
const text = request.body.text || '';
const instance = await tokenizer?.get();
const { ids, count } = await countSentencepieceTokens(tokenizer, text);
const chunks = await instance?.encodePieces(text);
const chunks = instance?.encodePieces(text);
return response.send({ ids, count, chunks });
} catch (error) {
console.log(error);
@ -288,9 +363,14 @@ function createSentencepieceEncodingHandler(tokenizer) {
/**
* Creates an API handler for decoding Sentencepiece tokens.
* @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer
* @returns {any} Handler function
* @returns {TokenizationHandler} Handler function
*/
function createSentencepieceDecodingHandler(tokenizer) {
/**
* Request handler for decoding Sentencepiece tokens.
* @param {import('express').Request} request
* @param {import('express').Response} response
*/
return async function (request, response) {
try {
if (!request.body) {
@ -299,6 +379,7 @@ function createSentencepieceDecodingHandler(tokenizer) {
const ids = request.body.ids || [];
const instance = await tokenizer?.get();
if (!instance) throw new Error('Failed to load the Sentencepiece tokenizer');
const ops = ids.map(id => instance.decodeIds([id]));
const chunks = await Promise.all(ops);
const text = chunks.join('');
@ -313,9 +394,14 @@ function createSentencepieceDecodingHandler(tokenizer) {
/**
* Creates an API handler for encoding Tiktoken tokens.
* @param {string} modelId Tiktoken model ID
* @returns {any} Handler function
* @returns {TokenizationHandler} Handler function
*/
function createTiktokenEncodingHandler(modelId) {
/**
* Request handler for encoding Tiktoken tokens.
* @param {import('express').Request} request
* @param {import('express').Response} response
*/
return async function (request, response) {
try {
if (!request.body) {
@ -337,9 +423,14 @@ function createTiktokenEncodingHandler(modelId) {
/**
* Creates an API handler for decoding Tiktoken tokens.
* @param {string} modelId Tiktoken model ID
* @returns {any} Handler function
* @returns {TokenizationHandler} Handler function
*/
function createTiktokenDecodingHandler(modelId) {
/**
* Request handler for decoding Tiktoken tokens.
* @param {import('express').Request} request
* @param {import('express').Response} response
*/
return async function (request, response) {
try {
if (!request.body) {
@ -358,14 +449,6 @@ function createTiktokenDecodingHandler(modelId) {
};
}
/**
* Loads the model tokenizers.
* @returns {Promise<void>} Promise that resolves when the tokenizers are loaded
*/
async function loadTokenizers() {
claude_tokenizer = await loadClaudeTokenizer('src/claude.json');
}
const router = express.Router();
router.post('/ai21/count', jsonParser, async function (req, res) {
@ -446,8 +529,10 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
if (queryModel.includes('claude')) {
const text = req.body.text || '';
const tokens = Object.values(claude_tokenizer.encode(text));
const chunks = await getWebTokenizersChunks(claude_tokenizer, tokens);
const instance = await claude_tokenizer.get();
if (!instance) throw new Error('Failed to load the Claude tokenizer');
const tokens = Object.values(instance.encode(text));
const chunks = getWebTokenizersChunks(instance, tokens);
return res.send({ ids: tokens, count: tokens.length, chunks });
}
@ -481,7 +566,9 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
if (queryModel.includes('claude')) {
const ids = req.body.ids || [];
const chunkText = await claude_tokenizer.decode(new Uint32Array(ids));
const instance = await claude_tokenizer.get();
if (!instance) throw new Error('Failed to load the Claude tokenizer');
const chunkText = instance.decode(new Int32Array(ids));
return res.send({ text: chunkText });
}
@ -503,7 +590,9 @@ router.post('/openai/count', jsonParser, async function (req, res) {
const model = getTokenizerModel(queryModel);
if (model === 'claude') {
num_tokens = countClaudeTokens(claude_tokenizer, req.body);
const instance = await claude_tokenizer.get();
if (!instance) throw new Error('Failed to load the Claude tokenizer');
num_tokens = countClaudeTokens(instance, req.body);
return res.send({ 'token_count': num_tokens });
}
@ -665,7 +754,6 @@ module.exports = {
getTokenizerModel,
getTiktokenTokenizer,
countClaudeTokens,
loadTokenizers,
getSentencepiceTokenizer,
sentencepieceTokenizers,
router,

View File

@ -112,6 +112,16 @@ async function ensurePublicDirectoriesExist() {
return directoriesList;
}
/**
* Gets a list of all user directories.
* @returns {Promise<import('./users').UserDirectoryList[]>} - The list of user directories
*/
async function getUserDirectoriesList() {
const userHandles = await getAllUserHandles();
const directoriesList = userHandles.map(handle => getUserDirectories(handle));
return directoriesList;
}
/**
* Perform migration from the old user data format to the new one.
*/
@ -707,6 +717,7 @@ module.exports = {
toAvatarKey,
initUserStorage,
ensurePublicDirectoriesExist,
getUserDirectoriesList,
getAllUserHandles,
getUserDirectories,
setUserDataMiddleware,