sillytavern / src /validator /TavernCardValidator.js
Nocigar's picture
Upload 72 files
1307964 verified
/**
* Validates the data structure of character cards.
* Supported specs: V1, V2
* Up to: 8083fb3
*
* @link https://github.com/malfoyslastname/character-card-spec-v2
*/
class TavernCardValidator {
/**
* @type {string|null}
*/
#lastValidationError = null;
constructor(card) {
this.card = card;
}
/**
* Field that caused the validation to fail
*
* @returns {null|string}
*/
get lastValidationError() {
return this.#lastValidationError;
}
/**
* Validate against V1 or V2 spec.
*
* @returns {number|boolean} - false when neither V1 nor V2 spec were matched. Specification version number otherwise.
*/
validate() {
this.#lastValidationError = null;
if (this.validateV1()) {
return 1;
}
if (this.validateV2()) {
return 2;
}
if (this.validateV3()) {
return 3;
}
return false;
}
/**
* Validate against V1 specification
*
* @returns {this is string[]}
*/
validateV1() {
const requiredFields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example'];
return requiredFields.every(field => {
if (!Object.hasOwn(this.card, field)) {
this.#lastValidationError = field;
return false;
}
return true;
});
}
/**
* Validate against V2 specification
*
* @returns {false|boolean|*}
*/
validateV2() {
return this.#validateSpecV2()
&& this.#validateSpecVersionV2()
&& this.#validateDataV2()
&& this.#validateCharacterBookV2();
}
/**
* 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;
}
return true;
}
#validateSpecVersionV2() {
if (this.card.spec_version !== '2.0') {
this.#lastValidationError = 'spec_version';
return false;
}
return true;
}
#validateDataV2() {
const data = this.card.data;
if (!data) {
this.#lastValidationError = 'No tavern card data found';
return false;
}
const requiredFields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example', 'creator_notes', 'system_prompt', 'post_history_instructions', 'alternate_greetings', 'tags', 'creator', 'character_version', 'extensions'];
const isAllRequiredFieldsPresent = requiredFields.every(field => {
if (!Object.hasOwn(data, field)) {
this.#lastValidationError = `data.${field}`;
return false;
}
return true;
});
return isAllRequiredFieldsPresent && Array.isArray(data.alternate_greetings) && Array.isArray(data.tags) && typeof data.extensions === 'object';
}
#validateCharacterBookV2() {
const characterBook = this.card.data.character_book;
if (!characterBook) {
return true;
}
const requiredFields = ['extensions', 'entries'];
const isAllRequiredFieldsPresent = requiredFields.every(field => {
if (!Object.hasOwn(characterBook, field)) {
this.#lastValidationError = `data.character_book.${field}`;
return false;
}
return true;
});
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 };