feat: add v3 character card support
- Added CharacterCardV3 struct to handle v3 format - Created From<CharacterCardV3> for CharacterCardV2Data conversion - Updated read_character_card_from_png() to detect and parse both v2 and v3 specs - V3 cards have top-level fields + nested data object - Maintains full backward compatibility with v2 cards - Export still uses v2 format for maximum compatibility
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
"Bash(npm run tauri:dev:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"WebSearch"
|
||||
"WebSearch",
|
||||
"Bash(python3:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
43
Mia Nakamura - The Working Girl.json
Normal file
43
Mia Nakamura - The Working Girl.json
Normal file
File diff suppressed because one or more lines are too long
BIN
Mia Nakamura - The Working Girl.png
Normal file
BIN
Mia Nakamura - The Working Girl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 747 KiB |
73
check_png_chunks.py
Normal file
73
check_png_chunks.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import struct
|
||||
import sys
|
||||
|
||||
def read_png_chunks(filename):
|
||||
with open(filename, 'rb') as f:
|
||||
# Check PNG signature
|
||||
signature = f.read(8)
|
||||
if signature != b'\x89PNG\r\n\x1a\n':
|
||||
print("Not a valid PNG file!")
|
||||
return
|
||||
|
||||
print("Valid PNG file")
|
||||
print("\nChunks found:")
|
||||
print("-" * 60)
|
||||
|
||||
chunk_num = 0
|
||||
while True:
|
||||
# Read chunk length
|
||||
length_data = f.read(4)
|
||||
if len(length_data) < 4:
|
||||
break
|
||||
|
||||
length = struct.unpack('>I', length_data)[0]
|
||||
|
||||
# Read chunk type
|
||||
chunk_type = f.read(4)
|
||||
if len(chunk_type) < 4:
|
||||
break
|
||||
|
||||
chunk_type_str = chunk_type.decode('ascii', errors='ignore')
|
||||
|
||||
# Read chunk data
|
||||
data = f.read(length)
|
||||
|
||||
# Read CRC
|
||||
crc = f.read(4)
|
||||
|
||||
chunk_num += 1
|
||||
|
||||
# Print chunk info
|
||||
print(f"{chunk_num}. Type: {chunk_type_str:8s} | Length: {length:8d} bytes", end='')
|
||||
|
||||
# For text chunks, show the keyword
|
||||
if chunk_type_str in ['tEXt', 'iTXt', 'zTXt']:
|
||||
try:
|
||||
# Find null terminator for keyword
|
||||
null_pos = data.find(b'\x00')
|
||||
if null_pos > 0:
|
||||
keyword = data[:null_pos].decode('latin-1', errors='ignore')
|
||||
print(f" | Keyword: '{keyword}'", end='')
|
||||
|
||||
# For tEXt, show some of the text content
|
||||
if chunk_type_str == 'tEXt' and len(data) > null_pos + 1:
|
||||
text_preview = data[null_pos+1:null_pos+51]
|
||||
print(f" | Text: {text_preview[:50]}...", end='')
|
||||
except:
|
||||
pass
|
||||
|
||||
print()
|
||||
|
||||
if chunk_type_str == 'IEND':
|
||||
break
|
||||
|
||||
print("-" * 60)
|
||||
print(f"Total chunks: {chunk_num}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: check_png_chunks.py <png_file>")
|
||||
sys.exit(1)
|
||||
|
||||
read_png_chunks(sys.argv[1])
|
||||
@@ -53,7 +53,7 @@ struct Character {
|
||||
extensions: serde_json::Value,
|
||||
}
|
||||
|
||||
// V2 character card specification structs
|
||||
// V2/V3 character card specification structs
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct CharacterCardV2 {
|
||||
spec: String,
|
||||
@@ -61,6 +61,55 @@ struct CharacterCardV2 {
|
||||
data: CharacterCardV2Data,
|
||||
}
|
||||
|
||||
// V3 card format (fields at top level + data object)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct CharacterCardV3 {
|
||||
spec: String,
|
||||
spec_version: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
personality: Option<String>,
|
||||
#[serde(default)]
|
||||
scenario: Option<String>,
|
||||
#[serde(default)]
|
||||
first_mes: Option<String>,
|
||||
#[serde(default)]
|
||||
mes_example: Option<String>,
|
||||
#[serde(default)]
|
||||
data: serde_json::Value, // V3 has additional data nested here
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
extensions: serde_json::Value,
|
||||
}
|
||||
|
||||
impl From<CharacterCardV3> for CharacterCardV2Data {
|
||||
fn from(v3: CharacterCardV3) -> Self {
|
||||
Self {
|
||||
name: v3.name,
|
||||
description: v3.description,
|
||||
personality: v3.personality,
|
||||
scenario: v3.scenario,
|
||||
first_mes: v3.first_mes,
|
||||
mes_example: v3.mes_example,
|
||||
system_prompt: v3.data.get("system_prompt").and_then(|v| v.as_str()).map(String::from),
|
||||
post_history_instructions: v3.data.get("post_history_instructions").and_then(|v| v.as_str()).map(String::from),
|
||||
alternate_greetings: v3.data.get("alternate_greetings")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default(),
|
||||
character_book: v3.data.get("character_book").cloned(),
|
||||
tags: v3.tags,
|
||||
creator: v3.data.get("creator").and_then(|v| v.as_str()).map(String::from),
|
||||
character_version: v3.data.get("character_version").and_then(|v| v.as_str()).map(String::from),
|
||||
creator_notes: v3.data.get("creator_notes").and_then(|v| v.as_str()).map(String::from),
|
||||
extensions: v3.extensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct CharacterCardV2Data {
|
||||
name: String,
|
||||
@@ -314,16 +363,29 @@ fn read_character_card_from_png(png_path: &PathBuf) -> Result<CharacterCardV2Dat
|
||||
let json_str = String::from_utf8(json_bytes)
|
||||
.map_err(|e| format!("Invalid UTF-8 in character data: {}", e))?;
|
||||
|
||||
// Parse as V2 card
|
||||
let card: CharacterCardV2 = serde_json::from_str(&json_str)
|
||||
// First try to parse as generic value to check spec version
|
||||
let generic: serde_json::Value = serde_json::from_str(&json_str)
|
||||
.map_err(|e| format!("Failed to parse character card JSON: {}", e))?;
|
||||
|
||||
// Validate spec
|
||||
if card.spec != "chara_card_v2" {
|
||||
return Err(format!("Unsupported character card spec: {}", card.spec));
|
||||
}
|
||||
let spec = generic.get("spec")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "No spec field in character card".to_string())?;
|
||||
|
||||
match spec {
|
||||
"chara_card_v2" => {
|
||||
// Parse as V2 card
|
||||
let card: CharacterCardV2 = serde_json::from_value(generic)
|
||||
.map_err(|e| format!("Failed to parse V2 card: {}", e))?;
|
||||
Ok(card.data)
|
||||
}
|
||||
"chara_card_v3" => {
|
||||
// Parse as V3 card and convert to V2Data
|
||||
let card: CharacterCardV3 = serde_json::from_value(generic)
|
||||
.map_err(|e| format!("Failed to parse V3 card: {}", e))?;
|
||||
Ok(CharacterCardV2Data::from(card))
|
||||
}
|
||||
_ => Err(format!("Unsupported character card spec: {}", spec))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_character_card_to_png(
|
||||
|
||||
Reference in New Issue
Block a user