oceansweep commited on
Commit
69937e1
1 Parent(s): cb782bd

Upload 13 files

Browse files
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}