Files
SillyTavern_replica/backend/models/converters.py

380 lines
13 KiB
Python

"""
数据模型转换器
提供 SillyTavern 格式与内部格式之间的双向转换功能。
所有导入/导出操作都应该通过转换器进行,确保数据格式的一致性。
"""
import uuid
from typing import Dict, Any, List, Optional
from datetime import datetime
from models.internal import (
WorldInfo,
WorldInfoEntry,
ActivationType,
)
class WorldBookConverter:
"""世界书数据转换器
负责 SillyTavern 格式和项目内部格式之间的转换。
SillyTavern 格式特点:
- entries 是 dict (key 为 uid)
- 使用 constant 字段表示常驻激活
- position 是字符串 (如 "after_char")
项目内部格式特点:
- entries 是 list
- 使用 activationType 枚举
- position 是数字 (0-5)
- 包含 trigger_config 结构(前端需要)
"""
@staticmethod
def detect_format(data: Dict[str, Any]) -> str:
"""
智能检测世界书数据格式
Args:
data: 世界书数据
Returns:
'sillytavern' | 'internal' | 'unknown'
"""
# 检查 entries 类型
entries = data.get("entries")
if not entries:
return "unknown"
# SillyTavern 特征: entries 是 dict
if isinstance(entries, dict):
return "sillytavern"
# 内部格式特征: entries 是 list
if isinstance(entries, list):
# 进一步检查是否有 trigger_config
if len(entries) > 0 and isinstance(entries[0], dict):
first_entry = entries[0]
if "trigger_config" in first_entry:
return "internal"
# 也可能是简化的内部格式
if "activationType" in first_entry or "position" in first_entry:
return "internal"
return "unknown"
# 位置映射: SillyTavern 字符串 -> 内部数字
POSITION_MAP_ST_TO_INTERNAL = {
"after_char": 0,
"before_char": 1,
"before_example": 2,
"after_example": 3,
"author_note": 4,
"system_prompt": 5,
}
# 位置映射: 内部数字 -> SillyTavern 字符串
POSITION_MAP_INTERNAL_TO_ST = {
0: "after_char",
1: "before_char",
2: "before_example",
3: "after_example",
4: "author_note",
5: "system_prompt",
}
@staticmethod
def st_to_internal(st_data: Dict[str, Any], name: str = None) -> Dict[str, Any]:
"""
将 SillyTavern 格式的世界书转换为内部格式
Args:
st_data: SillyTavern 格式的世界书数据
name: 世界书名称(可选,优先使用 st_data 中的 name)
Returns:
内部格式的世界书字典(包含 trigger_config)
"""
now = int(datetime.now().timestamp())
# 转换条目
entries = []
st_entries = st_data.get("entries", {})
# SillyTavern 的 entries 可能是 dict 或 list
if isinstance(st_entries, dict):
entries_list = list(st_entries.values())
elif isinstance(st_entries, list):
entries_list = st_entries
else:
entries_list = []
for st_entry in entries_list:
if not isinstance(st_entry, dict):
continue
# 判断激活类型
is_constant = st_entry.get("constant", False)
activation_type = ActivationType.PERMANENT if is_constant else ActivationType.KEYWORD
# 转换位置
st_position = st_entry.get("position", "after_char")
internal_position = WorldBookConverter.POSITION_MAP_ST_TO_INTERNAL.get(st_position, 0)
# 构建 trigger_config (前端期望的格式)
trigger_config = WorldBookConverter._build_trigger_config(
is_constant=is_constant,
key=st_entry.get("key", []),
keysecondary=st_entry.get("keysecondary", []),
selective=st_entry.get("selective", True)
)
# 创建内部格式的条目
entry_dict = {
"uid": st_entry.get("uid", str(uuid.uuid4())),
"key": st_entry.get("key", []),
"keysecondary": st_entry.get("keysecondary", []),
"content": st_entry.get("content", ""),
"comment": st_entry.get("comment", ""),
"activationType": activation_type.value,
"trigger_config": trigger_config,
"order": st_entry.get("order", 100),
"position": internal_position,
"depth": st_entry.get("depth", 4),
"role": st_entry.get("role", 0),
"probability": st_entry.get("probability", 100),
"group": st_entry.get("group", []),
"disable": st_entry.get("disable", False),
"createdAt": now,
"updatedAt": now
}
entries.append(entry_dict)
# 创建内部格式的世界书
worldbook_data = {
"id": str(uuid.uuid4()),
"name": name or st_data.get("name", "Unnamed"),
"description": st_data.get("description", ""),
"entries": entries,
"createdAt": now,
"updatedAt": now,
"version": 1
}
return worldbook_data
@staticmethod
def internal_to_st(worldbook_data: Dict[str, Any]) -> Dict[str, Any]:
"""
将内部格式的世界书转换为 SillyTavern 格式
Args:
worldbook_data: 内部格式的世界书字典
Returns:
SillyTavern 格式的世界书数据
"""
# 转换条目
st_entries = {}
for entry_data in worldbook_data.get("entries", []):
if not isinstance(entry_data, dict):
continue
uid = entry_data.get("uid", str(uuid.uuid4()))
# 从 trigger_config 或 activationType 判断是否常驻
is_constant = WorldBookConverter._is_constant_entry(entry_data)
# 提取关键词
key, keysecondary = WorldBookConverter._extract_keywords(entry_data)
# 转换位置
internal_position = entry_data.get("position", 0)
st_position = WorldBookConverter.POSITION_MAP_INTERNAL_TO_ST.get(internal_position, "after_char")
# 创建 SillyTavern 格式的条目
st_entry = {
"uid": uid,
"key": key,
"keysecondary": keysecondary,
"content": entry_data.get("content", ""),
"comment": entry_data.get("comment", ""),
"constant": is_constant,
"selective": not is_constant,
"order": entry_data.get("order", 100),
"position": st_position,
"depth": entry_data.get("depth", 4),
"probability": entry_data.get("probability", 100),
"group": entry_data.get("group", []),
"disable": entry_data.get("disable", False)
}
st_entries[uid] = st_entry
# 创建 SillyTavern 格式的世界书
st_data = {
"name": worldbook_data.get("name", ""),
"description": worldbook_data.get("description", ""),
"entries": st_entries
}
return st_data
@staticmethod
def normalize_entry(entry_data: Dict[str, Any]) -> Dict[str, Any]:
"""
规范化条目数据,确保包含所有必需字段和 trigger_config
Args:
entry_data: 条目数据(可能来自不同来源)
Returns:
规范化后的条目数据
"""
now = int(datetime.now().timestamp())
# 如果已经有 trigger_config,直接返回
if "trigger_config" in entry_data and entry_data["trigger_config"]:
return entry_data
# 否则从其他字段构建 trigger_config
is_constant = WorldBookConverter._is_constant_entry(entry_data)
key, keysecondary = WorldBookConverter._extract_keywords(entry_data)
trigger_config = WorldBookConverter._build_trigger_config(
is_constant=is_constant,
key=key,
keysecondary=keysecondary,
selective=entry_data.get("selective", True)
)
# 添加缺失的字段
normalized = {
"uid": entry_data.get("uid", str(uuid.uuid4())),
"key": key,
"keysecondary": keysecondary,
"content": entry_data.get("content", ""),
"comment": entry_data.get("comment", ""),
"activationType": entry_data.get("activationType",
ActivationType.PERMANENT.value if is_constant
else ActivationType.KEYWORD.value),
"trigger_config": trigger_config,
"order": entry_data.get("order", 100),
"position": entry_data.get("position", 0),
"depth": entry_data.get("depth", 4),
"role": entry_data.get("role", 0),
"probability": entry_data.get("probability", 100),
"group": entry_data.get("group", []),
"disable": entry_data.get("disable", False),
"createdAt": entry_data.get("createdAt", now),
"updatedAt": entry_data.get("updatedAt", now)
}
return normalized
@staticmethod
def _build_trigger_config(
is_constant: bool,
key: List[str],
keysecondary: List[str],
selective: bool = True
) -> Dict[str, Any]:
"""
构建 trigger_config 结构
Args:
is_constant: 是否常驻激活
key: 主关键词列表
keysecondary: 次要关键词列表
selective: 是否选择性匹配
Returns:
trigger_config 字典
"""
return {
"triggers": {
"constant": [is_constant, None],
"keyword": [
not is_constant,
{
"key": key,
"keysecondary": keysecondary,
"selective": selective,
"selectiveLogic": 0,
"matchWholeWords": False,
"caseSensitive": False
}
],
"rag": [False, {
"threshold": 0.75,
"top_k": 5,
"query_template": None
}],
"condition": [False, {
"variable_a": "",
"operator": "=",
"variable_b": ""
}]
}
}
@staticmethod
def _is_constant_entry(entry_data: Dict[str, Any]) -> bool:
"""
判断条目是否为常驻激活
Args:
entry_data: 条目数据
Returns:
是否常驻激活
"""
# 优先从 trigger_config 判断
if "trigger_config" in entry_data and entry_data["trigger_config"]:
try:
return entry_data["trigger_config"]["triggers"]["constant"][0]
except (KeyError, IndexError, TypeError):
pass
# 其次从 activationType 判断
if "activationType" in entry_data:
return entry_data["activationType"] == ActivationType.PERMANENT.value
# 最后从 constant 字段判断
if "constant" in entry_data:
return entry_data["constant"]
return False
@staticmethod
def _extract_keywords(entry_data: Dict[str, Any]) -> tuple:
"""
从条目数据中提取关键词
Args:
entry_data: 条目数据
Returns:
(key, keysecondary) 元组
"""
# 优先从 trigger_config 提取
if "trigger_config" in entry_data and entry_data["trigger_config"]:
try:
keyword_config = entry_data["trigger_config"]["triggers"]["keyword"][1]
if keyword_config:
key = keyword_config.get("key", [])
keysecondary = keyword_config.get("keysecondary", [])
return key, keysecondary
except (KeyError, IndexError, TypeError):
pass
# 否则从顶层字段提取
key = entry_data.get("key", [])
keysecondary = entry_data.get("keysecondary", [])
return key, keysecondary