oceansweep
commited on
Commit
•
69937e1
1
Parent(s):
cb782bd
Upload 13 files
Browse files- App_Function_Libraries/Personas/Character_Chat.py +18 -0
- App_Function_Libraries/Personas/__init__.py +0 -0
- App_Function_Libraries/Personas/__pycache__/__init__.cpython-312.pyc +0 -0
- App_Function_Libraries/Personas/__pycache__/cbs_handlers.cpython-312.pyc +0 -0
- App_Function_Libraries/Personas/__pycache__/ccv3_parser.cpython-312.pyc +0 -0
- App_Function_Libraries/Personas/__pycache__/models.cpython-312.pyc +0 -0
- App_Function_Libraries/Personas/__pycache__/utils.cpython-312.pyc +0 -0
- App_Function_Libraries/Personas/cbs_handlers.py +67 -0
- App_Function_Libraries/Personas/ccv3_parser.py +326 -0
- App_Function_Libraries/Personas/decorators.py +48 -0
- App_Function_Libraries/Personas/errors.py +11 -0
- App_Function_Libraries/Personas/models.py +75 -0
- App_Function_Libraries/Personas/utils.py +72 -0
App_Function_Libraries/Personas/Character_Chat.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Character_Chat.py
|
2 |
+
# Description: Functions for character chat
|
3 |
+
#
|
4 |
+
# Imports
|
5 |
+
#
|
6 |
+
# External Imports
|
7 |
+
#
|
8 |
+
# Local Imports
|
9 |
+
#
|
10 |
+
# ############################################################################################################
|
11 |
+
#
|
12 |
+
# Functions:
|
13 |
+
|
14 |
+
# FIXME - migrate functions from character_chat_tab to here
|
15 |
+
|
16 |
+
#
|
17 |
+
# End of Character_Chat.py
|
18 |
+
############################################################################################################
|
App_Function_Libraries/Personas/__init__.py
ADDED
File without changes
|
App_Function_Libraries/Personas/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (173 Bytes). View file
|
|
App_Function_Libraries/Personas/__pycache__/cbs_handlers.cpython-312.pyc
ADDED
Binary file (4.21 kB). View file
|
|
App_Function_Libraries/Personas/__pycache__/ccv3_parser.cpython-312.pyc
ADDED
Binary file (14.9 kB). View file
|
|
App_Function_Libraries/Personas/__pycache__/models.cpython-312.pyc
ADDED
Binary file (3.98 kB). View file
|
|
App_Function_Libraries/Personas/__pycache__/utils.cpython-312.pyc
ADDED
Binary file (4.21 kB). View file
|
|
App_Function_Libraries/Personas/cbs_handlers.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# cbs_handler.py
|
2 |
+
import re
|
3 |
+
import random
|
4 |
+
from typing import List
|
5 |
+
|
6 |
+
from App_Function_Libraries.Personas.models import CharacterCardV3
|
7 |
+
|
8 |
+
|
9 |
+
class CBSHandler:
|
10 |
+
"""Handles Curly Braced Syntaxes (CBS) in strings."""
|
11 |
+
|
12 |
+
CBS_PATTERN = re.compile(r'\{\{(.*?)\}\}')
|
13 |
+
|
14 |
+
def __init__(self, character_card: CharacterCardV3, user_display_name: str):
|
15 |
+
self.character_card = character_card
|
16 |
+
self.user_display_name = user_display_name
|
17 |
+
|
18 |
+
def replace_cbs(self, text: str) -> str:
|
19 |
+
"""Replaces CBS in the given text with appropriate values."""
|
20 |
+
def replacer(match):
|
21 |
+
cbs_content = match.group(1).strip()
|
22 |
+
if cbs_content.lower() == 'char':
|
23 |
+
return self.character_card.data.nickname or self.character_card.data.name
|
24 |
+
elif cbs_content.lower() == 'user':
|
25 |
+
return self.user_display_name
|
26 |
+
elif cbs_content.lower().startswith('random:'):
|
27 |
+
options = self._split_escaped(cbs_content[7:])
|
28 |
+
return random.choice(options) if options else ''
|
29 |
+
elif cbs_content.lower().startswith('pick:'):
|
30 |
+
options = self._split_escaped(cbs_content[5:])
|
31 |
+
return random.choice(options) if options else ''
|
32 |
+
elif cbs_content.lower().startswith('roll:'):
|
33 |
+
return self._handle_roll(cbs_content[5:])
|
34 |
+
elif cbs_content.lower().startswith('//'):
|
35 |
+
return ''
|
36 |
+
elif cbs_content.lower().startswith('hidden_key:'):
|
37 |
+
# Placeholder for hidden_key logic
|
38 |
+
return ''
|
39 |
+
elif cbs_content.lower().startswith('comment:'):
|
40 |
+
# Placeholder for comment logic
|
41 |
+
return ''
|
42 |
+
elif cbs_content.lower().startswith('reverse:'):
|
43 |
+
return cbs_content[8:][::-1]
|
44 |
+
else:
|
45 |
+
# Unknown CBS; return as is or empty
|
46 |
+
return ''
|
47 |
+
|
48 |
+
return self.CBS_PATTERN.sub(replacer, text)
|
49 |
+
|
50 |
+
def _split_escaped(self, text: str) -> List[str]:
|
51 |
+
"""Splits a string by commas, considering escaped commas."""
|
52 |
+
return [s.replace('\\,', ',') for s in re.split(r'(?<!\\),', text)]
|
53 |
+
|
54 |
+
def _handle_roll(self, value: str) -> str:
|
55 |
+
"""Handles the roll:N CBS."""
|
56 |
+
value = value.lower()
|
57 |
+
if value.startswith('d'):
|
58 |
+
value = value[1:]
|
59 |
+
if value.isdigit():
|
60 |
+
return str(random.randint(1, int(value)))
|
61 |
+
return ''
|
62 |
+
|
63 |
+
def handle_comments(self, text: str) -> str:
|
64 |
+
"""Handles comments in CBS."""
|
65 |
+
# Implementation depends on how comments should be displayed
|
66 |
+
# For simplicity, remove comments
|
67 |
+
return re.sub(r'\{\{comment:.*?\}\}', '', text)
|
App_Function_Libraries/Personas/ccv3_parser.py
ADDED
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ccv3_parser.py
|
2 |
+
#
|
3 |
+
#
|
4 |
+
# Imports
|
5 |
+
from typing import Any, Dict, List, Optional, Union
|
6 |
+
import re
|
7 |
+
#
|
8 |
+
# External Imports
|
9 |
+
#
|
10 |
+
# Local Imports
|
11 |
+
from App_Function_Libraries.Personas.models import Lorebook, Asset, CharacterCardV3, CharacterCardV3Data, Decorator, \
|
12 |
+
LorebookEntry
|
13 |
+
from App_Function_Libraries.Personas.utils import validate_iso_639_1, extract_json_from_charx, parse_json_file, \
|
14 |
+
extract_text_chunks_from_png, decode_base64
|
15 |
+
#
|
16 |
+
############################################################################################################
|
17 |
+
#
|
18 |
+
# Functions:
|
19 |
+
|
20 |
+
class CCv3ParserError(Exception):
|
21 |
+
"""Custom exception for CCv3 Parser errors."""
|
22 |
+
pass
|
23 |
+
|
24 |
+
|
25 |
+
class CharacterCardV3Parser:
|
26 |
+
REQUIRED_SPEC = 'chara_card_v3'
|
27 |
+
REQUIRED_VERSION = '3.0'
|
28 |
+
|
29 |
+
def __init__(self, input_data: Union[str, bytes], input_type: str):
|
30 |
+
"""
|
31 |
+
Initialize the parser with input data.
|
32 |
+
|
33 |
+
:param input_data: The input data as a string or bytes.
|
34 |
+
:param input_type: The type of the input data: 'json', 'png', 'apng', 'charx'.
|
35 |
+
"""
|
36 |
+
self.input_data = input_data
|
37 |
+
self.input_type = input_type.lower()
|
38 |
+
self.character_card: Optional[CharacterCardV3] = None
|
39 |
+
|
40 |
+
def parse(self):
|
41 |
+
"""Main method to parse the input data based on its type."""
|
42 |
+
if self.input_type == 'json':
|
43 |
+
self.parse_json_input()
|
44 |
+
elif self.input_type in ['png', 'apng']:
|
45 |
+
self.parse_png_apng_input()
|
46 |
+
elif self.input_type == 'charx':
|
47 |
+
self.parse_charx_input()
|
48 |
+
else:
|
49 |
+
raise CCv3ParserError(f"Unsupported input type: {self.input_type}")
|
50 |
+
|
51 |
+
def parse_json_input(self):
|
52 |
+
"""Parse JSON input directly."""
|
53 |
+
try:
|
54 |
+
data = parse_json_file(
|
55 |
+
self.input_data.encode('utf-8') if isinstance(self.input_data, str) else self.input_data)
|
56 |
+
self.character_card = self._build_character_card(data)
|
57 |
+
except Exception as e:
|
58 |
+
raise CCv3ParserError(f"Failed to parse JSON input: {e}")
|
59 |
+
|
60 |
+
def parse_png_apng_input(self):
|
61 |
+
"""Parse PNG or APNG input by extracting 'ccv3' tEXt chunk."""
|
62 |
+
try:
|
63 |
+
text_chunks = extract_text_chunks_from_png(self.input_data)
|
64 |
+
if 'ccv3' not in text_chunks:
|
65 |
+
raise CCv3ParserError("PNG/APNG does not contain 'ccv3' tEXt chunk.")
|
66 |
+
ccv3_base64 = text_chunks['ccv3']
|
67 |
+
ccv3_json_bytes = decode_base64(ccv3_base64)
|
68 |
+
data = parse_json_file(ccv3_json_bytes)
|
69 |
+
self.character_card = self._build_character_card(data)
|
70 |
+
except Exception as e:
|
71 |
+
raise CCv3ParserError(f"Failed to parse PNG/APNG input: {e}")
|
72 |
+
|
73 |
+
def parse_charx_input(self):
|
74 |
+
"""Parse CHARX input by extracting 'card.json' from the ZIP archive."""
|
75 |
+
try:
|
76 |
+
data = extract_json_from_charx(self.input_data)
|
77 |
+
self.character_card = self._build_character_card(data)
|
78 |
+
except Exception as e:
|
79 |
+
raise CCv3ParserError(f"Failed to parse CHARX input: {e}")
|
80 |
+
|
81 |
+
def _build_character_card(self, data: Dict[str, Any]) -> CharacterCardV3:
|
82 |
+
"""Build the CharacterCardV3 object from parsed data."""
|
83 |
+
# Validate required fields
|
84 |
+
spec = data.get('spec')
|
85 |
+
spec_version = data.get('spec_version')
|
86 |
+
if spec != self.REQUIRED_SPEC:
|
87 |
+
raise CCv3ParserError(f"Invalid spec: Expected '{self.REQUIRED_SPEC}', got '{spec}'")
|
88 |
+
if spec_version != self.REQUIRED_VERSION:
|
89 |
+
# As per spec, should not reject but handle versions
|
90 |
+
# For now, proceed if version is >=3.0
|
91 |
+
try:
|
92 |
+
version_float = float(spec_version)
|
93 |
+
if version_float < 3.0:
|
94 |
+
raise CCv3ParserError(f"Unsupported spec_version: '{spec_version}' (must be >= '3.0')")
|
95 |
+
except ValueError:
|
96 |
+
raise CCv3ParserError(f"Invalid spec_version format: '{spec_version}'")
|
97 |
+
|
98 |
+
data_field = data.get('data')
|
99 |
+
if not data_field:
|
100 |
+
raise CCv3ParserError("Missing 'data' field in CharacterCardV3 object.")
|
101 |
+
|
102 |
+
# Extract required fields
|
103 |
+
required_fields = ['name', 'description', 'tags', 'creator', 'character_version',
|
104 |
+
'mes_example', 'extensions', 'system_prompt',
|
105 |
+
'post_history_instructions', 'first_mes',
|
106 |
+
'alternate_greetings', 'personality', 'scenario',
|
107 |
+
'creator_notes', 'group_only_greetings']
|
108 |
+
for field_name in required_fields:
|
109 |
+
if field_name not in data_field:
|
110 |
+
raise CCv3ParserError(f"Missing required field in data: '{field_name}'")
|
111 |
+
|
112 |
+
# Parse assets
|
113 |
+
assets_data = data_field.get('assets', [{
|
114 |
+
'type': 'icon',
|
115 |
+
'uri': 'ccdefault:',
|
116 |
+
'name': 'main',
|
117 |
+
'ext': 'png'
|
118 |
+
}])
|
119 |
+
assets = self._parse_assets(assets_data)
|
120 |
+
|
121 |
+
# Parse creator_notes_multilingual
|
122 |
+
creator_notes_multilingual = data_field.get('creator_notes_multilingual')
|
123 |
+
if creator_notes_multilingual:
|
124 |
+
if not isinstance(creator_notes_multilingual, dict):
|
125 |
+
raise CCv3ParserError("'creator_notes_multilingual' must be a dictionary.")
|
126 |
+
# Validate ISO 639-1 codes
|
127 |
+
for lang_code in creator_notes_multilingual.keys():
|
128 |
+
if not validate_iso_639_1(lang_code):
|
129 |
+
raise CCv3ParserError(f"Invalid language code in 'creator_notes_multilingual': '{lang_code}'")
|
130 |
+
|
131 |
+
# Parse character_book
|
132 |
+
character_book_data = data_field.get('character_book')
|
133 |
+
character_book = self._parse_lorebook(character_book_data) if character_book_data else None
|
134 |
+
|
135 |
+
# Build CharacterCardV3Data
|
136 |
+
character_card_data = CharacterCardV3Data(
|
137 |
+
name=data_field['name'],
|
138 |
+
description=data_field['description'],
|
139 |
+
tags=data_field['tags'],
|
140 |
+
creator=data_field['creator'],
|
141 |
+
character_version=data_field['character_version'],
|
142 |
+
mes_example=data_field['mes_example'],
|
143 |
+
extensions=data_field['extensions'],
|
144 |
+
system_prompt=data_field['system_prompt'],
|
145 |
+
post_history_instructions=data_field['post_history_instructions'],
|
146 |
+
first_mes=data_field['first_mes'],
|
147 |
+
alternate_greetings=data_field['alternate_greetings'],
|
148 |
+
personality=data_field['personality'],
|
149 |
+
scenario=data_field['scenario'],
|
150 |
+
creator_notes=data_field['creator_notes'],
|
151 |
+
character_book=character_book,
|
152 |
+
assets=assets,
|
153 |
+
nickname=data_field.get('nickname'),
|
154 |
+
creator_notes_multilingual=creator_notes_multilingual,
|
155 |
+
source=data_field.get('source'),
|
156 |
+
group_only_greetings=data_field['group_only_greetings'],
|
157 |
+
creation_date=data_field.get('creation_date'),
|
158 |
+
modification_date=data_field.get('modification_date')
|
159 |
+
)
|
160 |
+
|
161 |
+
return CharacterCardV3(
|
162 |
+
spec=spec,
|
163 |
+
spec_version=spec_version,
|
164 |
+
data=character_card_data
|
165 |
+
)
|
166 |
+
|
167 |
+
def _parse_assets(self, assets_data: List[Dict[str, Any]]) -> List[Asset]:
|
168 |
+
"""Parse and validate assets."""
|
169 |
+
assets = []
|
170 |
+
for asset_data in assets_data:
|
171 |
+
# Validate required fields
|
172 |
+
for field in ['type', 'uri', 'ext']:
|
173 |
+
if field not in asset_data:
|
174 |
+
raise CCv3ParserError(f"Asset missing required field: '{field}'")
|
175 |
+
if not isinstance(asset_data[field], str):
|
176 |
+
raise CCv3ParserError(f"Asset field '{field}' must be a string.")
|
177 |
+
# Optional 'name'
|
178 |
+
name = asset_data.get('name', '')
|
179 |
+
# Validate 'ext'
|
180 |
+
ext = asset_data['ext'].lower()
|
181 |
+
if not re.match(r'^[a-z0-9]+$', ext):
|
182 |
+
raise CCv3ParserError(f"Invalid file extension in asset: '{ext}'")
|
183 |
+
# Append to assets list
|
184 |
+
assets.append(Asset(
|
185 |
+
type=asset_data['type'],
|
186 |
+
uri=asset_data['uri'],
|
187 |
+
name=name,
|
188 |
+
ext=ext
|
189 |
+
))
|
190 |
+
return assets
|
191 |
+
|
192 |
+
def _parse_lorebook(self, lorebook_data: Dict[str, Any]) -> Lorebook:
|
193 |
+
"""Parse and validate Lorebook object."""
|
194 |
+
# Validate Lorebook fields
|
195 |
+
if not isinstance(lorebook_data, dict):
|
196 |
+
raise CCv3ParserError("Lorebook must be a JSON object.")
|
197 |
+
|
198 |
+
# Extract fields with defaults
|
199 |
+
name = lorebook_data.get('name')
|
200 |
+
description = lorebook_data.get('description')
|
201 |
+
scan_depth = lorebook_data.get('scan_depth')
|
202 |
+
token_budget = lorebook_data.get('token_budget')
|
203 |
+
recursive_scanning = lorebook_data.get('recursive_scanning')
|
204 |
+
extensions = lorebook_data.get('extensions', {})
|
205 |
+
entries_data = lorebook_data.get('entries', [])
|
206 |
+
|
207 |
+
# Parse entries
|
208 |
+
entries = self._parse_lorebook_entries(entries_data)
|
209 |
+
|
210 |
+
return Lorebook(
|
211 |
+
name=name,
|
212 |
+
description=description,
|
213 |
+
scan_depth=scan_depth,
|
214 |
+
token_budget=token_budget,
|
215 |
+
recursive_scanning=recursive_scanning,
|
216 |
+
extensions=extensions,
|
217 |
+
entries=entries
|
218 |
+
)
|
219 |
+
|
220 |
+
def _parse_lorebook_entries(self, entries_data: List[Dict[str, Any]]) -> List[LorebookEntry]:
|
221 |
+
"""Parse and validate Lorebook entries."""
|
222 |
+
entries = []
|
223 |
+
for entry_data in entries_data:
|
224 |
+
# Validate required fields
|
225 |
+
for field in ['keys', 'content', 'enabled', 'insertion_order']:
|
226 |
+
if field not in entry_data:
|
227 |
+
raise CCv3ParserError(f"Lorebook entry missing required field: '{field}'")
|
228 |
+
if not isinstance(entry_data['keys'], list) or not all(isinstance(k, str) for k in entry_data['keys']):
|
229 |
+
raise CCv3ParserError("'keys' field in Lorebook entry must be a list of strings.")
|
230 |
+
if not isinstance(entry_data['content'], str):
|
231 |
+
raise CCv3ParserError("'content' field in Lorebook entry must be a string.")
|
232 |
+
if not isinstance(entry_data['enabled'], bool):
|
233 |
+
raise CCv3ParserError("'enabled' field in Lorebook entry must be a boolean.")
|
234 |
+
if not isinstance(entry_data['insertion_order'], (int, float)):
|
235 |
+
raise CCv3ParserError("'insertion_order' field in Lorebook entry must be a number.")
|
236 |
+
|
237 |
+
# Optional fields
|
238 |
+
use_regex = entry_data.get('use_regex', False)
|
239 |
+
constant = entry_data.get('constant')
|
240 |
+
selective = entry_data.get('selective')
|
241 |
+
secondary_keys = entry_data.get('secondary_keys')
|
242 |
+
position = entry_data.get('position')
|
243 |
+
name = entry_data.get('name')
|
244 |
+
priority = entry_data.get('priority')
|
245 |
+
entry_id = entry_data.get('id')
|
246 |
+
comment = entry_data.get('comment')
|
247 |
+
|
248 |
+
if selective and not isinstance(selective, bool):
|
249 |
+
raise CCv3ParserError("'selective' field in Lorebook entry must be a boolean.")
|
250 |
+
if secondary_keys:
|
251 |
+
if not isinstance(secondary_keys, list) or not all(isinstance(k, str) for k in secondary_keys):
|
252 |
+
raise CCv3ParserError("'secondary_keys' field in Lorebook entry must be a list of strings.")
|
253 |
+
if position and not isinstance(position, str):
|
254 |
+
raise CCv3ParserError("'position' field in Lorebook entry must be a string.")
|
255 |
+
|
256 |
+
# Parse decorators from content
|
257 |
+
decorators = self._extract_decorators(entry_data['content'])
|
258 |
+
|
259 |
+
# Create LorebookEntry
|
260 |
+
entries.append(LorebookEntry(
|
261 |
+
keys=entry_data['keys'],
|
262 |
+
content=entry_data['content'],
|
263 |
+
enabled=entry_data['enabled'],
|
264 |
+
insertion_order=int(entry_data['insertion_order']),
|
265 |
+
use_regex=use_regex,
|
266 |
+
constant=constant,
|
267 |
+
selective=selective,
|
268 |
+
secondary_keys=secondary_keys,
|
269 |
+
position=position,
|
270 |
+
decorators=decorators,
|
271 |
+
name=name,
|
272 |
+
priority=priority,
|
273 |
+
id=entry_id,
|
274 |
+
comment=comment
|
275 |
+
))
|
276 |
+
return entries
|
277 |
+
|
278 |
+
def _extract_decorators(self, content: str) -> List[Decorator]:
|
279 |
+
"""Extract decorators from the content field."""
|
280 |
+
decorators = []
|
281 |
+
lines = content.splitlines()
|
282 |
+
for line in lines:
|
283 |
+
if line.startswith('@@'):
|
284 |
+
decorator = self._parse_decorator_line(line)
|
285 |
+
if decorator:
|
286 |
+
decorators.append(decorator)
|
287 |
+
return decorators
|
288 |
+
|
289 |
+
def _parse_decorator_line(self, line: str) -> Optional[Decorator]:
|
290 |
+
"""
|
291 |
+
Parses a single decorator line.
|
292 |
+
|
293 |
+
Example:
|
294 |
+
@@decorator_name value
|
295 |
+
@@@fallback_decorator value
|
296 |
+
"""
|
297 |
+
fallback = None
|
298 |
+
if line.startswith('@@@'):
|
299 |
+
# Fallback decorator
|
300 |
+
name_value = line.lstrip('@').strip()
|
301 |
+
parts = name_value.split(' ', 1)
|
302 |
+
name = parts[0]
|
303 |
+
value = parts[1] if len(parts) > 1 else None
|
304 |
+
fallback = Decorator(name=name, value=value)
|
305 |
+
return fallback
|
306 |
+
elif line.startswith('@@'):
|
307 |
+
# Primary decorator
|
308 |
+
name_value = line.lstrip('@').strip()
|
309 |
+
parts = name_value.split(' ', 1)
|
310 |
+
name = parts[0]
|
311 |
+
value = parts[1] if len(parts) > 1 else None
|
312 |
+
# Check for fallback decorators in subsequent lines
|
313 |
+
# This assumes that fallback decorators follow immediately after the primary
|
314 |
+
# decorator in the content
|
315 |
+
# For simplicity, not implemented here. You can enhance this based on your needs.
|
316 |
+
return Decorator(name=name, value=value)
|
317 |
+
else:
|
318 |
+
return None
|
319 |
+
|
320 |
+
def get_character_card(self) -> Optional[CharacterCardV3]:
|
321 |
+
"""Returns the parsed CharacterCardV3 object."""
|
322 |
+
return self.character_card
|
323 |
+
|
324 |
+
#
|
325 |
+
# End of ccv3_parser.py
|
326 |
+
############################################################################################################
|
App_Function_Libraries/Personas/decorators.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# decorators.py
|
2 |
+
from typing import List, Optional
|
3 |
+
|
4 |
+
from App_Function_Libraries.Personas.models import Decorator
|
5 |
+
|
6 |
+
|
7 |
+
# Assume Decorator class is already defined in models.py
|
8 |
+
|
9 |
+
class DecoratorProcessor:
|
10 |
+
"""Processes decorators for Lorebook entries."""
|
11 |
+
|
12 |
+
def __init__(self, decorators: List[Decorator]):
|
13 |
+
self.decorators = decorators
|
14 |
+
|
15 |
+
def process(self):
|
16 |
+
"""Process decorators based on their definitions."""
|
17 |
+
for decorator in self.decorators:
|
18 |
+
# Implement processing logic based on decorator.name
|
19 |
+
if decorator.name == 'activate_only_after':
|
20 |
+
self._activate_only_after(decorator.value)
|
21 |
+
elif decorator.name == 'activate_only_every':
|
22 |
+
self._activate_only_every(decorator.value)
|
23 |
+
# Add more decorator handling as needed
|
24 |
+
else:
|
25 |
+
# Handle unknown decorators or ignore
|
26 |
+
pass
|
27 |
+
|
28 |
+
def _activate_only_after(self, value: Optional[str]):
|
29 |
+
"""Handle @@activate_only_after decorator."""
|
30 |
+
if value and value.isdigit():
|
31 |
+
count = int(value)
|
32 |
+
# Implement logic to activate only after 'count' messages
|
33 |
+
pass
|
34 |
+
else:
|
35 |
+
# Invalid value; ignore or raise error
|
36 |
+
pass
|
37 |
+
|
38 |
+
def _activate_only_every(self, value: Optional[str]):
|
39 |
+
"""Handle @@activate_only_every decorator."""
|
40 |
+
if value and value.isdigit():
|
41 |
+
frequency = int(value)
|
42 |
+
# Implement logic to activate every 'frequency' messages
|
43 |
+
pass
|
44 |
+
else:
|
45 |
+
# Invalid value; ignore or raise error
|
46 |
+
pass
|
47 |
+
|
48 |
+
# Implement other decorator handlers as needed
|
App_Function_Libraries/Personas/errors.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# errors.py
|
2 |
+
# Description: Custom Exceptions for Personas
|
3 |
+
#
|
4 |
+
# Imports
|
5 |
+
from typing import Any, Dict, List, Optional, Union
|
6 |
+
#
|
7 |
+
# Custom Exceptions
|
8 |
+
|
9 |
+
class CCv3ParserError(Exception):
|
10 |
+
"""Custom exception for CCv3 Parser errors."""
|
11 |
+
pass
|
App_Function_Libraries/Personas/models.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# models.py
|
2 |
+
from dataclasses import dataclass, field
|
3 |
+
from typing import Any, Dict, List, Optional, Union
|
4 |
+
|
5 |
+
@dataclass
|
6 |
+
class Asset:
|
7 |
+
type: str
|
8 |
+
uri: str
|
9 |
+
name: str = ""
|
10 |
+
ext: str = "unknown"
|
11 |
+
|
12 |
+
@dataclass
|
13 |
+
class Decorator:
|
14 |
+
name: str
|
15 |
+
value: Optional[str] = None
|
16 |
+
fallback: Optional['Decorator'] = None
|
17 |
+
|
18 |
+
@dataclass
|
19 |
+
class LorebookEntry:
|
20 |
+
keys: List[str]
|
21 |
+
content: str
|
22 |
+
enabled: bool
|
23 |
+
insertion_order: int
|
24 |
+
use_regex: bool = False
|
25 |
+
constant: Optional[bool] = None
|
26 |
+
selective: Optional[bool] = None
|
27 |
+
secondary_keys: Optional[List[str]] = None
|
28 |
+
position: Optional[str] = None
|
29 |
+
decorators: List[Decorator] = field(default_factory=list)
|
30 |
+
# Optional Fields
|
31 |
+
name: Optional[str] = None
|
32 |
+
priority: Optional[int] = None
|
33 |
+
id: Optional[Union[int, str]] = None
|
34 |
+
comment: Optional[str] = None
|
35 |
+
|
36 |
+
@dataclass
|
37 |
+
class Lorebook:
|
38 |
+
name: Optional[str] = None
|
39 |
+
description: Optional[str] = None
|
40 |
+
scan_depth: Optional[int] = None
|
41 |
+
token_budget: Optional[int] = None
|
42 |
+
recursive_scanning: Optional[bool] = None
|
43 |
+
extensions: Dict[str, Any] = field(default_factory=dict)
|
44 |
+
entries: List[LorebookEntry] = field(default_factory=list)
|
45 |
+
|
46 |
+
@dataclass
|
47 |
+
class CharacterCardV3Data:
|
48 |
+
name: str
|
49 |
+
description: str
|
50 |
+
tags: List[str]
|
51 |
+
creator: str
|
52 |
+
character_version: str
|
53 |
+
mes_example: str
|
54 |
+
extensions: Dict[str, Any]
|
55 |
+
system_prompt: str
|
56 |
+
post_history_instructions: str
|
57 |
+
first_mes: str
|
58 |
+
alternate_greetings: List[str]
|
59 |
+
personality: str
|
60 |
+
scenario: str
|
61 |
+
creator_notes: str
|
62 |
+
character_book: Optional[Lorebook] = None
|
63 |
+
assets: List[Asset] = field(default_factory=list)
|
64 |
+
nickname: Optional[str] = None
|
65 |
+
creator_notes_multilingual: Optional[Dict[str, str]] = None
|
66 |
+
source: Optional[List[str]] = None
|
67 |
+
group_only_greetings: List[str] = field(default_factory=list)
|
68 |
+
creation_date: Optional[int] = None
|
69 |
+
modification_date: Optional[int] = None
|
70 |
+
|
71 |
+
@dataclass
|
72 |
+
class CharacterCardV3:
|
73 |
+
spec: str
|
74 |
+
spec_version: str
|
75 |
+
data: CharacterCardV3Data
|
App_Function_Libraries/Personas/utils.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# utils.py
|
2 |
+
import base64
|
3 |
+
import json
|
4 |
+
import re
|
5 |
+
from typing import Any, Dict, List, Optional
|
6 |
+
from zipfile import ZipFile, BadZipFile
|
7 |
+
from io import BytesIO
|
8 |
+
from PIL import Image, PngImagePlugin
|
9 |
+
|
10 |
+
|
11 |
+
def decode_base64(data: str) -> bytes:
|
12 |
+
"""Decodes a Base64 encoded string."""
|
13 |
+
try:
|
14 |
+
return base64.b64decode(data)
|
15 |
+
except base64.binascii.Error as e:
|
16 |
+
raise ValueError(f"Invalid Base64 data: {e}")
|
17 |
+
|
18 |
+
|
19 |
+
def extract_text_chunks_from_png(png_bytes: bytes) -> Dict[str, str]:
|
20 |
+
"""Extracts tEXt chunks from a PNG/APNG file."""
|
21 |
+
try:
|
22 |
+
with Image.open(BytesIO(png_bytes)) as img:
|
23 |
+
info = img.info
|
24 |
+
return info
|
25 |
+
except Exception as e:
|
26 |
+
raise ValueError(f"Failed to extract text chunks: {e}")
|
27 |
+
|
28 |
+
|
29 |
+
def extract_json_from_charx(charx_bytes: bytes) -> Dict[str, Any]:
|
30 |
+
"""Extracts and parses card.json from a CHARX file."""
|
31 |
+
try:
|
32 |
+
with ZipFile(BytesIO(charx_bytes)) as zip_file:
|
33 |
+
if 'card.json' not in zip_file.namelist():
|
34 |
+
raise ValueError("CHARX file does not contain card.json")
|
35 |
+
with zip_file.open('card.json') as json_file:
|
36 |
+
return json.load(json_file)
|
37 |
+
except BadZipFile:
|
38 |
+
raise ValueError("Invalid CHARX file: Not a valid zip archive")
|
39 |
+
except Exception as e:
|
40 |
+
raise ValueError(f"Failed to extract JSON from CHARX: {e}")
|
41 |
+
|
42 |
+
|
43 |
+
def parse_json_file(json_bytes: bytes) -> Dict[str, Any]:
|
44 |
+
"""Parses a JSON byte stream."""
|
45 |
+
try:
|
46 |
+
return json.loads(json_bytes.decode('utf-8'))
|
47 |
+
except json.JSONDecodeError as e:
|
48 |
+
raise ValueError(f"Invalid JSON data: {e}")
|
49 |
+
|
50 |
+
|
51 |
+
def validate_iso_639_1(code: str) -> bool:
|
52 |
+
"""Validates if the code is a valid ISO 639-1 language code."""
|
53 |
+
# For brevity, a small subset of ISO 639-1 codes
|
54 |
+
valid_codes = {
|
55 |
+
'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'zh', 'ja', 'ko',
|
56 |
+
# Add more as needed
|
57 |
+
}
|
58 |
+
return code in valid_codes
|
59 |
+
|
60 |
+
|
61 |
+
def parse_uri(uri: str) -> Dict[str, Any]:
|
62 |
+
"""Parses the URI field and categorizes its type."""
|
63 |
+
if uri.startswith('http://') or uri.startswith('https://'):
|
64 |
+
return {'scheme': 'http', 'value': uri}
|
65 |
+
elif uri.startswith('embeded://'):
|
66 |
+
return {'scheme': 'embeded', 'value': uri.replace('embeded://', '')}
|
67 |
+
elif uri.startswith('ccdefault:'):
|
68 |
+
return {'scheme': 'ccdefault', 'value': None}
|
69 |
+
elif uri.startswith('data:'):
|
70 |
+
return {'scheme': 'data', 'value': uri}
|
71 |
+
else:
|
72 |
+
return {'scheme': 'unknown', 'value': uri}
|