SillyTavern/src/character-card-parser.js

101 lines
3.1 KiB
JavaScript
Raw Normal View History

2023-07-20 20:32:15 +03:00
const fs = require('fs');
2024-02-16 18:03:56 +02:00
const encode = require('png-chunks-encode');
2023-07-20 20:32:15 +03:00
const extract = require('png-chunks-extract');
const PNGtext = require('png-chunk-text');
2024-02-16 18:03:56 +02:00
/**
* Writes Character metadata to a PNG image buffer.
2024-06-01 22:07:57 +03:00
* Writes only 'chara', 'ccv3' is not supported and removed not to create a mismatch.
2024-02-16 18:03:56 +02:00
* @param {Buffer} image PNG image buffer
* @param {string} data Character data to write
* @returns {Buffer} PNG image buffer with metadata
*/
const write = (image, data) => {
const chunks = extract(image);
const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
2023-07-20 20:32:15 +03:00
2024-06-01 22:07:57 +03:00
// 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);
}
2024-02-16 18:03:56 +02:00
}
2024-06-01 22:07:57 +03:00
2024-07-11 15:11:35 +09:00
// Add new v2 chunk before the IEND chunk
2024-02-16 18:03:56 +02:00
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
2024-07-11 15:11:35 +09:00
// Try adding v3 chunk before the IEND chunk
try {
//change v2 format to v3
const v3Data = JSON.parse(data);
2024-07-14 14:07:23 +03:00
v3Data.spec = 'chara_card_v3';
v3Data.spec_version = '3.0';
2024-07-11 15:11:35 +09:00
const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));
2024-07-14 14:07:23 +03:00
} catch (error) { }
2024-07-11 15:11:35 +09:00
2024-02-16 18:03:56 +02:00
const newBuffer = Buffer.from(encode(chunks));
return newBuffer;
};
/**
* Reads Character metadata from a PNG image buffer.
2024-06-01 22:07:57 +03:00
* Supports both V2 (chara) and V3 (ccv3). V3 (ccv3) takes precedence.
2024-02-16 18:03:56 +02:00
* @param {Buffer} image PNG image buffer
* @returns {string} Character data
*/
const read = (image) => {
const chunks = extract(image);
2023-07-20 20:32:15 +03:00
2024-06-01 22:07:57 +03:00
const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
2023-07-20 20:32:15 +03:00
2024-02-16 18:03:56 +02:00
if (textChunks.length === 0) {
console.error('PNG metadata does not contain any text chunks.');
throw new Error('No PNG metadata.');
}
2024-06-01 22:07:57 +03:00
const ccv3Index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'ccv3');
2024-06-01 22:07:57 +03:00
if (ccv3Index > -1) {
return Buffer.from(textChunks[ccv3Index].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');
2024-02-16 18:03:56 +02:00
}
2024-01-26 01:14:12 +02:00
2024-06-01 22:07:57 +03:00
console.error('PNG metadata does not contain any character data.');
throw new Error('No PNG metadata.');
2024-02-16 18:03:56 +02:00
};
/**
* Parses a card image and returns the character metadata.
* @param {string} cardUrl Path to the card image
* @param {string} format File format
* @returns {string} Character data
*/
const parse = (cardUrl, format) => {
let fileFormat = format === undefined ? 'png' : format;
switch (fileFormat) {
case 'png': {
const buffer = fs.readFileSync(cardUrl);
return read(buffer);
2023-12-02 10:14:06 -05:00
}
2023-07-20 20:32:15 +03:00
}
2024-02-16 18:03:56 +02:00
throw new Error('Unsupported format');
2023-07-20 20:32:15 +03:00
};
module.exports = {
2024-02-16 18:03:56 +02:00
parse,
write,
read,
2023-07-20 20:32:15 +03:00
};