f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
170 lines
5.9 KiB
JavaScript
170 lines
5.9 KiB
JavaScript
/**
|
|
* Character Card & World Info Parser Tool
|
|
*
|
|
* This tool extracts character card or world info data from PNG images.
|
|
* Compatible with SillyTavern format.
|
|
*
|
|
* Usage: node parse-character-card.js <image-path> [output-path] [--world-info]
|
|
* Example: node parse-character-card.js character.png character.json
|
|
* Example: node parse-character-card.js world-info.png world-info.json --world-info
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { Buffer } = require('buffer');
|
|
|
|
// Check if dependencies are installed
|
|
let extract, PNGtext;
|
|
try {
|
|
extract = require('png-chunks-extract');
|
|
PNGtext = require('png-chunk-text');
|
|
} catch (error) {
|
|
console.error('Error: Required dependencies not found.');
|
|
console.error('Please run: npm install png-chunks-extract png-chunk-text');
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Extract character card data from PNG image
|
|
* Based on SillyTavern's implementation: src/character-card-parser.js
|
|
* @param {string} imagePath - Path to PNG image
|
|
* @returns {string} - Character card JSON string
|
|
*/
|
|
function extractFromPng(imagePath) {
|
|
try {
|
|
const buffer = fs.readFileSync(imagePath);
|
|
const chunks = extract(new Uint8Array(buffer));
|
|
|
|
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('PNG metadata does not contain any text chunks.');
|
|
}
|
|
|
|
// Try ccv3 first (v3 format) - V3 takes precedence as per SillyTavern
|
|
const ccv3Index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'ccv3');
|
|
|
|
if (ccv3Index > -1) {
|
|
return Buffer.from(textChunks[ccv3Index].text, 'base64').toString('utf8');
|
|
}
|
|
|
|
// Fallback to chara (v2 format)
|
|
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 card data (chara or ccv3).');
|
|
throw new Error('PNG metadata does not contain any character card data (chara or ccv3).');
|
|
} catch (error) {
|
|
if (error.message.includes('PNG metadata')) {
|
|
throw error; // Re-throw PNG metadata errors as-is
|
|
}
|
|
throw new Error(`Failed to extract from PNG: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract world info data from PNG image
|
|
* @param {string} imagePath - Path to PNG image
|
|
* @returns {string} - World info JSON string
|
|
*/
|
|
function extractWorldInfoFromPng(imagePath) {
|
|
try {
|
|
const buffer = fs.readFileSync(imagePath);
|
|
const chunks = extract(new Uint8Array(buffer));
|
|
|
|
const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
|
|
|
|
if (textChunks.length === 0) {
|
|
throw new Error('PNG metadata does not contain any text chunks.');
|
|
}
|
|
|
|
// Look for naidata (world info keyword)
|
|
const naidataIndex = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'naidata');
|
|
|
|
if (naidataIndex > -1) {
|
|
return Buffer.from(textChunks[naidataIndex].text, 'base64').toString('utf8');
|
|
}
|
|
|
|
throw new Error('PNG metadata does not contain world info data (naidata).');
|
|
} catch (error) {
|
|
throw new Error(`Failed to extract world info from PNG: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length < 1) {
|
|
console.error('Usage: node parse-character-card.js <image-path> [output-path] [--world-info]');
|
|
console.error('Example: node parse-character-card.js character.png character.json');
|
|
console.error('Example: node parse-character-card.js world-info.png world-info.json --world-info');
|
|
process.exit(1);
|
|
}
|
|
|
|
const imagePath = args[0];
|
|
// Important: outputPath must be provided as second argument, not via stdout redirection
|
|
const outputPath = args[1] || imagePath.replace(/\.(png|webp)$/i, '.json');
|
|
const isWorldInfo = args.includes('--world-info');
|
|
|
|
if (!fs.existsSync(imagePath)) {
|
|
console.error(`Error: Image file not found: ${imagePath}`);
|
|
console.error('Please check if the file path is correct and the file exists.');
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const ext = path.extname(imagePath).toLowerCase();
|
|
let jsonData;
|
|
|
|
if (ext === '.png') {
|
|
if (isWorldInfo) {
|
|
jsonData = extractWorldInfoFromPng(imagePath);
|
|
} else {
|
|
jsonData = extractFromPng(imagePath);
|
|
}
|
|
} else if (ext === '.webp') {
|
|
// WebP support would require additional library
|
|
// For now, suggest converting to PNG or using JSON format
|
|
console.error('Error: WebP format parsing requires additional setup.');
|
|
console.error('Please convert to PNG or use JSON format.');
|
|
process.exit(1);
|
|
} else {
|
|
console.error(`Error: Unsupported file format: ${ext}`);
|
|
console.error('Supported formats: .png');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Validate JSON
|
|
try {
|
|
JSON.parse(jsonData);
|
|
} catch (error) {
|
|
console.error('Error: Extracted data is not valid JSON.');
|
|
console.error('This might not be a valid character card or world info image.');
|
|
console.error('The image may not contain embedded character data, or the data format is corrupted.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Save to file (do not use stdout redirection, use file path argument)
|
|
fs.writeFileSync(outputPath, jsonData, 'utf8');
|
|
console.log(`Successfully extracted data to: ${outputPath}`);
|
|
} catch (error) {
|
|
console.error(`Error: ${error.message}`);
|
|
if (error.message.includes('PNG metadata')) {
|
|
console.error('This image may not be a valid SillyTavern character card.');
|
|
console.error('Please ensure the image was exported from SillyTavern with character data embedded.');
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Export for use as module
|
|
module.exports = {
|
|
extractFromPng,
|
|
extractWorldInfoFromPng,
|
|
};
|