Files
st/server/utils/character_card.go

286 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package utils
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"image"
"image/png"
"io"
)
// CharacterCardV2 SillyTavern 角色卡 V2 格式
type CharacterCardV2 struct {
Spec string `json:"spec"`
SpecVersion string `json:"spec_version"`
Data CharacterCardV2Data `json:"data"`
}
type CharacterCardV2Data struct {
Name string `json:"name"`
Description string `json:"description"`
Personality string `json:"personality"`
Scenario string `json:"scenario"`
FirstMes string `json:"first_mes"`
MesExample string `json:"mes_example"`
CreatorNotes string `json:"creator_notes"`
SystemPrompt string `json:"system_prompt"`
PostHistoryInstructions string `json:"post_history_instructions"`
Tags []string `json:"tags"`
Creator string `json:"creator"`
CharacterVersion string `json:"character_version"`
AlternateGreetings []string `json:"alternate_greetings"`
CharacterBook map[string]interface{} `json:"character_book,omitempty"`
Extensions map[string]interface{} `json:"extensions"`
}
// ExtractCharacterFromPNG 从 PNG 图片中提取角色卡数据
func ExtractCharacterFromPNG(pngData []byte) (*CharacterCardV2, error) {
reader := bytes.NewReader(pngData)
// 验证 PNG 格式(解码但不保存图片)
_, err := png.Decode(reader)
if err != nil {
return nil, errors.New("无效的 PNG 文件")
}
// 重新读取以获取 tEXt chunks
reader.Seek(0, 0)
// 查找 tEXt chunk 中的 "chara" 字段
charaJSON, err := extractTextChunk(reader, "chara")
if err != nil {
return nil, errors.New("PNG 中没有找到角色卡数据")
}
// 尝试 Base64 解码
decodedJSON, err := base64.StdEncoding.DecodeString(charaJSON)
if err != nil {
// 如果不是 Base64直接使用原始 JSON
decodedJSON = []byte(charaJSON)
}
// 解析 JSON
var card CharacterCardV2
err = json.Unmarshal(decodedJSON, &card)
if err != nil {
return nil, errors.New("解析角色卡数据失败: " + err.Error())
}
return &card, nil
}
// extractTextChunk 从 PNG 中提取指定 key 的 tEXt chunk
func extractTextChunk(r io.Reader, key string) (string, error) {
// 跳过 PNG signature (8 bytes)
signature := make([]byte, 8)
if _, err := io.ReadFull(r, signature); err != nil {
return "", err
}
// 验证 PNG signature
expectedSig := []byte{137, 80, 78, 71, 13, 10, 26, 10}
if !bytes.Equal(signature, expectedSig) {
return "", errors.New("invalid PNG signature")
}
// 读取所有 chunks
for {
// 读取 chunk length (4 bytes)
lengthBytes := make([]byte, 4)
if _, err := io.ReadFull(r, lengthBytes); err != nil {
if err == io.EOF {
break
}
return "", err
}
length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
// 读取 chunk type (4 bytes)
chunkType := make([]byte, 4)
if _, err := io.ReadFull(r, chunkType); err != nil {
return "", err
}
// 读取 chunk data
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return "", err
}
// 读取 CRC (4 bytes)
crc := make([]byte, 4)
if _, err := io.ReadFull(r, crc); err != nil {
return "", err
}
// 检查是否是 tEXt chunk
if string(chunkType) == "tEXt" {
// tEXt chunk 格式: keyword\0text
nullIndex := bytes.IndexByte(data, 0)
if nullIndex == -1 {
continue
}
keyword := string(data[:nullIndex])
text := string(data[nullIndex+1:])
if keyword == key {
return text, nil
}
}
// IEND chunk 表示结束
if string(chunkType) == "IEND" {
break
}
}
return "", errors.New("text chunk not found")
}
// EmbedCharacterToPNG 将角色卡数据嵌入到 PNG 图片中
func EmbedCharacterToPNG(img image.Image, card *CharacterCardV2) ([]byte, error) {
// 序列化角色卡数据
cardJSON, err := json.Marshal(card)
if err != nil {
return nil, err
}
// Base64 编码
encodedJSON := base64.StdEncoding.EncodeToString(cardJSON)
// 创建一个 buffer 来写入 PNG
var buf bytes.Buffer
// 写入 PNG signature
buf.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
// 编码原始图片到临时 buffer
var imgBuf bytes.Buffer
if err := png.Encode(&imgBuf, img); err != nil {
return nil, err
}
// 跳过原始 PNG 的 signature
imgData := imgBuf.Bytes()[8:]
// 将原始图片的 chunks 复制到输出,在 IEND 之前插入 tEXt chunk
r := bytes.NewReader(imgData)
for {
// 读取 chunk length
lengthBytes := make([]byte, 4)
if _, err := io.ReadFull(r, lengthBytes); err != nil {
if err == io.EOF {
break
}
return nil, err
}
length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
// 读取 chunk type
chunkType := make([]byte, 4)
if _, err := io.ReadFull(r, chunkType); err != nil {
return nil, err
}
// 读取 chunk data
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
// 读取 CRC
crc := make([]byte, 4)
if _, err := io.ReadFull(r, crc); err != nil {
return nil, err
}
// 如果是 IEND chunk先写入 tEXt chunk
if string(chunkType) == "IEND" {
// 写入 tEXt chunk
writeTextChunk(&buf, "chara", encodedJSON)
}
// 写入原始 chunk
buf.Write(lengthBytes)
buf.Write(chunkType)
buf.Write(data)
buf.Write(crc)
if string(chunkType) == "IEND" {
break
}
}
return buf.Bytes(), nil
}
// writeTextChunk 写入 tEXt chunk
func writeTextChunk(w io.Writer, keyword, text string) error {
data := append([]byte(keyword), 0)
data = append(data, []byte(text)...)
// 写入 length
length := uint32(len(data))
lengthBytes := []byte{
byte(length >> 24),
byte(length >> 16),
byte(length >> 8),
byte(length),
}
w.Write(lengthBytes)
// 写入 type
w.Write([]byte("tEXt"))
// 写入 data
w.Write(data)
// 计算并写入 CRC
crcData := append([]byte("tEXt"), data...)
crc := calculateCRC(crcData)
crcBytes := []byte{
byte(crc >> 24),
byte(crc >> 16),
byte(crc >> 8),
byte(crc),
}
w.Write(crcBytes)
return nil
}
// calculateCRC 计算 CRC32
func calculateCRC(data []byte) uint32 {
crc := uint32(0xFFFFFFFF)
for _, b := range data {
crc ^= uint32(b)
for i := 0; i < 8; i++ {
if crc&1 != 0 {
crc = (crc >> 1) ^ 0xEDB88320
} else {
crc >>= 1
}
}
}
return crc ^ 0xFFFFFFFF
}
// ParseCharacterCardJSON 解析 JSON 格式的角色卡
func ParseCharacterCardJSON(jsonData []byte) (*CharacterCardV2, error) {
var card CharacterCardV2
err := json.Unmarshal(jsonData, &card)
if err != nil {
return nil, errors.New("解析角色卡 JSON 失败: " + err.Error())
}
return &card, nil
}