V3 spec (IMPORT ONLY)
This commit is contained in:
parent
2e23e78937
commit
b559f2f559
|
@ -296,6 +296,7 @@ export function evaluateMacros(content, env) {
|
|||
// Legacy non-macro substitutions
|
||||
content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user);
|
||||
content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char);
|
||||
content = content.replace(/<CHAR>/gi, typeof env.char === 'function' ? env.char() : env.char);
|
||||
content = content.replace(/<CHARIFNOTGROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
|
||||
content = content.replace(/<GROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
|
||||
|
||||
|
|
|
@ -633,6 +633,9 @@ function parseTimestamp(timestamp) {
|
|||
|
||||
// Unix time (legacy TAI / tags)
|
||||
if (typeof timestamp === 'number') {
|
||||
if (isNaN(timestamp) || !isFinite(timestamp) || timestamp < 0) {
|
||||
return moment.invalid();
|
||||
}
|
||||
return moment(timestamp);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ const PNGtext = require('png-chunk-text');
|
|||
|
||||
/**
|
||||
* Writes Character metadata to a PNG image buffer.
|
||||
* Writes only 'chara', 'ccv3' is not supported and removed not to create a mismatch.
|
||||
* @param {Buffer} image PNG image buffer
|
||||
* @param {string} data Character data to write
|
||||
* @returns {Buffer} PNG image buffer with metadata
|
||||
|
@ -14,10 +15,14 @@ const write = (image, data) => {
|
|||
const chunks = extract(image);
|
||||
const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
|
||||
|
||||
// Remove all existing tEXt chunks
|
||||
for (let tEXtChunk of tEXtChunks) {
|
||||
chunks.splice(chunks.indexOf(tEXtChunk), 1);
|
||||
// Remove existing tEXt chunks
|
||||
for (const tEXtChunk of tEXtChunks) {
|
||||
const data = PNGtext.decode(tEXtChunk.data);
|
||||
if (data.keyword.toLowerCase() === 'chara' || data.keyword.toLowerCase() === 'ccv3') {
|
||||
chunks.splice(chunks.indexOf(tEXtChunk), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new chunks before the IEND chunk
|
||||
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
|
||||
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
|
||||
|
@ -27,31 +32,34 @@ const write = (image, data) => {
|
|||
|
||||
/**
|
||||
* Reads Character metadata from a PNG image buffer.
|
||||
* Supports both V2 (chara) and V3 (ccv3). V3 (ccv3) takes precedence.
|
||||
* @param {Buffer} image PNG image buffer
|
||||
* @returns {string} Character data
|
||||
*/
|
||||
const read = (image) => {
|
||||
const chunks = extract(image);
|
||||
|
||||
const textChunks = chunks.filter(function (chunk) {
|
||||
return chunk.name === 'tEXt';
|
||||
}).map(function (chunk) {
|
||||
return PNGtext.decode(chunk.data);
|
||||
});
|
||||
const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
|
||||
|
||||
if (textChunks.length === 0) {
|
||||
console.error('PNG metadata does not contain any text chunks.');
|
||||
throw new Error('No PNG metadata.');
|
||||
}
|
||||
|
||||
let index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() == 'chara');
|
||||
const ccv3Index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'ccv3');
|
||||
|
||||
if (index === -1) {
|
||||
console.error('PNG metadata does not contain any character data.');
|
||||
throw new Error('No PNG metadata.');
|
||||
if (ccv3Index > -1) {
|
||||
return Buffer.from(textChunks[ccv3Index].text, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
return Buffer.from(textChunks[index].text, 'base64').toString('utf8');
|
||||
const charaIndex = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'chara');
|
||||
|
||||
if (charaIndex > -1) {
|
||||
return Buffer.from(textChunks[charaIndex].text, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
console.error('PNG metadata does not contain any character data.');
|
||||
throw new Error('No PNG metadata.');
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -424,6 +424,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
|||
insertion_order: entry.order,
|
||||
enabled: !entry.disable,
|
||||
position: entry.position == 0 ? 'before_char' : 'after_char',
|
||||
use_regex: true, // ST keys are always regex
|
||||
extensions: {
|
||||
position: entry.position,
|
||||
exclude_recursion: entry.excludeRecursion,
|
||||
|
@ -498,7 +499,7 @@ async function importFromJson(uploadPath, { request }) {
|
|||
let jsonData = JSON.parse(data);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('Importing from v2 json');
|
||||
console.log(`Importing from ${jsonData.spec} json`);
|
||||
importRisuSprites(request.user.directories, jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
|
@ -581,7 +582,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
|
|||
const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('Found a v2 character file.');
|
||||
console.log(`Found a ${jsonData.spec} character file.`);
|
||||
importRisuSprites(request.user.directories, jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
* @link https://github.com/malfoyslastname/character-card-spec-v2
|
||||
*/
|
||||
class TavernCardValidator {
|
||||
/**
|
||||
* @type {string|null}
|
||||
*/
|
||||
#lastValidationError = null;
|
||||
|
||||
constructor(card) {
|
||||
|
@ -37,6 +40,10 @@ class TavernCardValidator {
|
|||
return 2;
|
||||
}
|
||||
|
||||
if (this.validateV3()) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -62,13 +69,23 @@ class TavernCardValidator {
|
|||
* @returns {false|boolean|*}
|
||||
*/
|
||||
validateV2() {
|
||||
return this.#validateSpec()
|
||||
&& this.#validateSpecVersion()
|
||||
&& this.#validateData()
|
||||
&& this.#validateCharacterBook();
|
||||
return this.#validateSpecV2()
|
||||
&& this.#validateSpecVersionV2()
|
||||
&& this.#validateDataV2()
|
||||
&& this.#validateCharacterBookV2();
|
||||
}
|
||||
|
||||
#validateSpec() {
|
||||
/**
|
||||
* Validate against V3 specification
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateV3() {
|
||||
return this.#validateSpecV3()
|
||||
&& this.#validateSpecVersionV3()
|
||||
&& this.#validateDataV3();
|
||||
}
|
||||
|
||||
#validateSpecV2() {
|
||||
if (this.card.spec !== 'chara_card_v2') {
|
||||
this.#lastValidationError = 'spec';
|
||||
return false;
|
||||
|
@ -76,7 +93,7 @@ class TavernCardValidator {
|
|||
return true;
|
||||
}
|
||||
|
||||
#validateSpecVersion() {
|
||||
#validateSpecVersionV2() {
|
||||
if (this.card.spec_version !== '2.0') {
|
||||
this.#lastValidationError = 'spec_version';
|
||||
return false;
|
||||
|
@ -84,7 +101,7 @@ class TavernCardValidator {
|
|||
return true;
|
||||
}
|
||||
|
||||
#validateData() {
|
||||
#validateDataV2() {
|
||||
const data = this.card.data;
|
||||
|
||||
if (!data) {
|
||||
|
@ -104,7 +121,7 @@ class TavernCardValidator {
|
|||
return isAllRequiredFieldsPresent && Array.isArray(data.alternate_greetings) && Array.isArray(data.tags) && typeof data.extensions === 'object';
|
||||
}
|
||||
|
||||
#validateCharacterBook() {
|
||||
#validateCharacterBookV2() {
|
||||
const characterBook = this.card.data.character_book;
|
||||
|
||||
if (!characterBook) {
|
||||
|
@ -122,6 +139,33 @@ class TavernCardValidator {
|
|||
|
||||
return isAllRequiredFieldsPresent && Array.isArray(characterBook.entries) && typeof characterBook.extensions === 'object';
|
||||
}
|
||||
|
||||
#validateSpecV3() {
|
||||
if (this.card.spec !== 'chara_card_v3') {
|
||||
this.#lastValidationError = 'spec';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#validateSpecVersionV3() {
|
||||
if (Number(this.card.spec_version) < 3.0 || Number(this.card.spec_version) >= 4.0) {
|
||||
this.#lastValidationError = 'spec_version';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#validateDataV3() {
|
||||
const data = this.card.data;
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
this.#lastValidationError = 'No tavern card data found';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TavernCardValidator };
|
||||
|
|
Loading…
Reference in New Issue