Spaces:
Runtime error
Runtime error
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}
|