UNPKG

@lenml/char-card-reader

Version:

SillyTavern character card info reader

393 lines (359 loc) 13.3 kB
import { CharacterBook } from "./CharacterBook"; import { extractUserCommentFromWebPChunk, parseImageMetadata, } from "./MetadataReader"; import { SpecV1 } from "./spec_types/spec_v1"; import { SpecV2 } from "./spec_types/spec_v2"; import { SpecV3 } from "./spec_types/spec_v3"; import { CharacterCardParserError, CharRawData, ParsedMetadata } from "./types"; import { CharacterSpec } from "./types"; import { Base64, deepClone, isValidImageUrl, mergeObjects, toBase64, } from "./utils"; export class CharacterCard { static async from_file(file: ArrayBuffer | Uint8Array) { const exif_data = parseImageMetadata(file); const image_b64: string = await toBase64(file); const fallback_avatar = `data:image/${exif_data.format};base64,${image_b64}`; const raw_data = this.parse_char_info(file, exif_data); return new CharacterCard( { // default as v1 spec: "chara_card_v1", spec_version: "1.0", data: {}, ...raw_data, }, fallback_avatar ); } static from_json(raw_data: CharRawData, fallback_avatar = "") { return new CharacterCard(raw_data, fallback_avatar); } static parse_char_info(file: ArrayBuffer | Uint8Array, exif: ParsedMetadata) { let encoded_text: string | undefined; // NOTE: About CCV3 keyword checkout this // https://github.com/kwaroran/character-card-spec-v3/blob/f3a86af019fbd99f788f7a1155f399655b34ab35/SPEC_V3.md?plain=1#L22-L30 if (exif.format === "png") { const encoded_ccv_1_2 = exif.chunks.find((x) => x.keyword === "chara"); const encoded_ccv_3 = exif.chunks.find((x) => x.keyword === "ccv3"); encoded_text = encoded_ccv_3?.text ?? encoded_ccv_1_2?.text; } else if (exif.format === "jpeg") { const encoded_ccv_1_2 = exif.segments.find((x) => x.marker === "chara"); const encoded_ccv_3 = exif.segments.find((x) => x.marker === "ccv3"); encoded_text = encoded_ccv_3?.comment ?? encoded_ccv_1_2?.comment; } else if (exif.format === "webp") { const exifChunk = exif.chunks.find((x) => x.type === "EXIF"); if (exifChunk) { const exifData = extractUserCommentFromWebPChunk( file instanceof Uint8Array ? file : new Uint8Array(file), exifChunk ); encoded_text = exifData; } } if (!encoded_text) { throw new CharacterCardParserError( "Failed to extract chara card data from image" ); } const json_str = Base64.decode(encoded_text); const json = JSON.parse(json_str); return json; } constructor(readonly raw_data: CharRawData, readonly fallback_avatar = "") {} async get_avatar(without_fallback = false): Promise<string> { const fallback = without_fallback ? "" : this.fallback_avatar; return [this.raw_data.avatar, this.raw_data.data?.avatar, fallback].filter( isValidImageUrl )[0]; } get avatar(): CharacterSpec.Root["avatar"] { return [ this.raw_data.avatar, this.raw_data.data?.avatar, this.fallback_avatar, ].filter(isValidImageUrl)[0]; } get spec(): CharacterSpec.Root["spec"] { return this.raw_data.spec || "unknown"; } get spec_version(): CharacterSpec.Root["spec_version"] { return this.raw_data.spec_version || "unknown"; } get name(): CharacterSpec.Data["name"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.name ?? this.raw_data.name; case "chara_card_v3": return this.raw_data.data?.name ?? this.raw_data.name; default: return this.raw_data.char_name ?? this.raw_data.name ?? "unknown"; } } get description(): CharacterSpec.Data["description"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.description ?? this.raw_data.description; case "chara_card_v3": return this.raw_data.data?.description ?? this.raw_data.description; default: return this.raw_data.description ?? "unknown"; } } get first_message(): CharacterSpec.Data["first_mes"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.first_mes ?? this.raw_data.first_mes; case "chara_card_v3": return this.raw_data.data?.first_mes ?? this.raw_data.first_mes; default: return this.raw_data.first_mes ?? "unknown"; } } get message_example(): CharacterSpec.Root["mes_example"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.mes_example ?? this.raw_data.mes_example; case "chara_card_v3": return this.raw_data.data?.mes_example ?? this.raw_data.mes_example; default: return this.raw_data.mes_example ?? "unknown"; } } get create_date(): CharacterSpec.Root["create_date"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.create_date ?? this.raw_data.create_date; case "chara_card_v3": return this.raw_data.data?.create_date ?? this.raw_data.create_date; default: return this.raw_data.create_date ?? "unknown"; } } get personality(): CharacterSpec.Data["personality"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.personality ?? this.raw_data.personality; case "chara_card_v3": return this.raw_data.data?.personality ?? this.raw_data.personality; default: return this.raw_data.personality ?? "unknown"; } } get scenario(): CharacterSpec.Data["scenario"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.scenario ?? this.raw_data.scenario; case "chara_card_v3": return this.raw_data.data?.scenario ?? this.raw_data.scenario; default: return this.raw_data.scenario ?? "unknown"; } } get alternate_greetings(): CharacterSpec.Data["alternate_greetings"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.alternate_greetings; case "chara_card_v3": return this.raw_data.data?.alternate_greetings; default: return []; } } get character_book(): CharacterSpec.CharacterBook { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.character_book; case "chara_card_v3": return this.raw_data.data?.character_book; default: return { entries: [], name: this.name, extensions: {}, }; } } get tags(): CharacterSpec.Data["tags"] { switch (this.spec) { case "chara_card_v2": return this.raw_data.data?.tags; case "chara_card_v3": return this.raw_data.data?.tags; default: return []; } } /** * Converts the current character card data to the SpecV1 format. * * This method constructs a SpecV1.TavernCard object by extracting the necessary * fields from the current instance's raw data using a getter function. The function * retrieves data from multiple sources, including instance properties, the raw data * object, and its nested data object. The resulting object contains fields defined * in the chara_card_v1 specification, such as name, description, personality, scenario, * first message, and example messages. * * @returns A SpecV1.TavernCard object representing the character card data in SpecV1 format. */ public toSpecV1(): SpecV1.TavernCard { const getter = (key: string) => (this as any)[key] ?? this.raw_data[key] ?? this.raw_data.data?.[key]; return { name: getter("name") ?? getter("char_name"), description: getter("description"), personality: getter("personality"), scenario: getter("scenario"), first_mes: getter("first_mes"), mes_example: getter("mes_example"), }; } /** * Converts the current character card data to the SpecV2 format. * * This method constructs a SpecV2.TavernCardV2 object by extracting the necessary * fields from the current instance's raw data using a getter function. The function * retrieves data from multiple sources, including instance properties, the raw data * object, and its nested data object. The resulting object contains fields defined * in the chara_card_v2 specification, including additional fields introduced in * later updates. * * @returns A deep-cloned SpecV2.TavernCardV2 object representing the character card * data in SpecV2 format. */ public toSpecV2(): SpecV2.TavernCardV2 { const getter = (key: string) => (this as any)[key] ?? this.raw_data[key] ?? this.raw_data.data?.[key]; return deepClone({ spec: "chara_card_v2", spec_version: "2.0", data: { // fields from CCV2 name: getter("name") ?? getter("char_name"), description: getter("description"), mes_example: getter("mes_example"), first_mes: getter("first_mes"), personality: getter("personality"), scenario: getter("scenario"), // New fields start here creator_notes: getter("creator_notes"), system_prompt: getter("system_prompt"), post_history_instructions: getter("post_history_instructions"), alternate_greetings: getter("alternate_greetings"), character_book: getter("character_book"), // May 8th additions tags: getter("tags"), creator: getter("creator"), character_version: getter("character_version"), extensions: getter("extensions"), }, }); } /** * Converts the current character card data to the SpecV3 format. * * This function utilizes a getter to retrieve properties from the * character card's raw data and returns a deep-cloned object * conforming to the SpecV3.CharacterCardV3 structure. It includes * fields from the CCV2 specification, changes specific to CCV3, * and new fields introduced in CCV3. * * @returns A deep-cloned object representing the character card data * in SpecV3 format. */ public toSpecV3(): SpecV3.CharacterCardV3 { const getter = (key: string) => (this as any)[key] ?? this.raw_data[key] ?? this.raw_data.data?.[key]; return deepClone({ spec: "chara_card_v3", spec_version: "3.0", data: { // fields from CCV2 name: getter("name") ?? getter("char_name"), description: getter("description"), tags: getter("tags"), creator: getter("creator"), character_version: getter("character_version"), mes_example: getter("mes_example"), extensions: getter("extensions"), system_prompt: getter("system_prompt"), post_history_instructions: getter("post_history_instructions"), first_mes: getter("first_mes"), alternate_greetings: getter("alternate_greetings"), personality: getter("personality"), scenario: getter("scenario"), //Changes from CCV2 creator_notes: getter("creator_notes"), character_book: getter("character_book"), //New fields in CCV3 assets: getter("assets"), nickname: getter("nickname"), creator_notes_multilingual: getter("creator_notes_multilingual"), source: getter("source"), group_only_greetings: getter("group_only_greetings"), creation_date: getter("create_date") ?? getter("creation_date"), modification_date: getter("modify_date") ?? getter("modification_date"), }, }); } /** * Returns the maximum compatible version of the character card * * this card => merge(v1,v2,v3); */ public toMaxCompatibleSpec(): | SpecV3.CharacterCardV3 | SpecV2.TavernCardV2 | SpecV1.TavernCard { return mergeObjects(this.toSpecV1(), this.toSpecV2(), this.toSpecV3()); } /** * Creates a clone of the current CharacterCard instance in the specified version format. * * This method generates a new CharacterCard object with the data formatted to match * the specified version's specification. It supports conversion to SpecV1, SpecV2, and * SpecV3 formats by utilizing the respective `toSpecV1`, `toSpecV2`, and `toSpecV3` methods. * * @param version - The specification version ("v1", "v2", or "v3") to clone the character card into. * Defaults to "v3" if not specified. * * @returns A new CharacterCard instance formatted according to the specified version. * * @throws Will throw an error if the specified version is unsupported. */ public clone(version = "v3" as "v1" | "v2" | "v3") { let new_raw_data = null; switch (version) { case "v1": { new_raw_data = this.toSpecV1(); break; } case "v2": { new_raw_data = this.toSpecV2(); break; } case "v3": { new_raw_data = this.toSpecV3(); break; } default: { throw new Error(`Unsupported version ${version}`); } } return CharacterCard.from_json({ spec: "chara_card_v1", spec_version: "1.0", data: {}, ...new_raw_data, }); } public get_book() { return CharacterBook.from_json(this.raw_data); } }