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(npm run tauri:dev:*)",
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"WebSearch"
|
"WebSearch",
|
||||||
|
"Bash(python3:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"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,
|
extensions: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2 character card specification structs
|
// V2/V3 character card specification structs
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct CharacterCardV2 {
|
struct CharacterCardV2 {
|
||||||
spec: String,
|
spec: String,
|
||||||
@@ -61,6 +61,55 @@ struct CharacterCardV2 {
|
|||||||
data: CharacterCardV2Data,
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct CharacterCardV2Data {
|
struct CharacterCardV2Data {
|
||||||
name: String,
|
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)
|
let json_str = String::from_utf8(json_bytes)
|
||||||
.map_err(|e| format!("Invalid UTF-8 in character data: {}", e))?;
|
.map_err(|e| format!("Invalid UTF-8 in character data: {}", e))?;
|
||||||
|
|
||||||
// Parse as V2 card
|
// First try to parse as generic value to check spec version
|
||||||
let card: CharacterCardV2 = serde_json::from_str(&json_str)
|
let generic: serde_json::Value = serde_json::from_str(&json_str)
|
||||||
.map_err(|e| format!("Failed to parse character card JSON: {}", e))?;
|
.map_err(|e| format!("Failed to parse character card JSON: {}", e))?;
|
||||||
|
|
||||||
// Validate spec
|
let spec = generic.get("spec")
|
||||||
if card.spec != "chara_card_v2" {
|
.and_then(|v| v.as_str())
|
||||||
return Err(format!("Unsupported character card spec: {}", card.spec));
|
.ok_or_else(|| "No spec field in character card".to_string())?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(card.data)
|
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(
|
fn write_character_card_to_png(
|
||||||
|
|||||||
Reference in New Issue
Block a user