| import json
|
| import chardet
|
| from loguru import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
| class Live2dModel:
|
| """
|
| A class to represent a Live2D model. This class only prepares and stores the information of the Live2D model. It does not send anything to the frontend or server or anything.
|
|
|
| Attributes:
|
| model_dict_path (str): The path to the model dictionary file.
|
| live2d_model_name (str): The name of the Live2D model.
|
| model_info (dict): The information of the Live2D model.
|
| emo_map (dict): The emotion map of the Live2D model.
|
| emo_str (str): The string representation of the emotion map of the Live2D model.
|
| """
|
|
|
| model_dict_path: str
|
| live2d_model_name: str
|
| model_info: dict
|
| emo_map: dict
|
| emo_str: str
|
|
|
| def __init__(
|
| self, live2d_model_name: str, model_dict_path: str = "model_dict.json"
|
| ):
|
| self.model_dict_path: str = model_dict_path
|
| self.live2d_model_name: str = live2d_model_name
|
| self.set_model(live2d_model_name)
|
|
|
| def set_model(self, model_name: str) -> None:
|
| """
|
| Set the model with its name and load the model information. This method will initialize the `self.model_info`, `self.emo_map`, and `self.emo_str` attributes.
|
| This method is called in the constructor.
|
|
|
| Parameters:
|
| model_name (str): The name of the live2d model.
|
|
|
| Returns:
|
| None
|
| """
|
|
|
| self.model_info: dict = self._lookup_model_info(model_name)
|
| self.emo_map: dict = {
|
| k.lower(): v for k, v in self.model_info["emotionMap"].items()
|
| }
|
| self.emo_str: str = " ".join([f"[{key}]," for key in self.emo_map.keys()])
|
|
|
|
|
|
|
| def _load_file_content(self, file_path: str) -> str:
|
| """Load the content of a file with robust encoding handling."""
|
|
|
| encodings = ["utf-8", "utf-8-sig", "gbk", "gb2312", "ascii"]
|
|
|
| for encoding in encodings:
|
| try:
|
| with open(file_path, "r", encoding=encoding) as file:
|
| return file.read()
|
| except UnicodeDecodeError:
|
| continue
|
|
|
|
|
| try:
|
| with open(file_path, "rb") as file:
|
| raw_data = file.read()
|
| detected = chardet.detect(raw_data)
|
| detected_encoding = detected["encoding"]
|
|
|
| if detected_encoding:
|
| try:
|
| return raw_data.decode(detected_encoding)
|
| except UnicodeDecodeError:
|
| pass
|
| except Exception as e:
|
| logger.error(f"Error detecting encoding for {file_path}: {e}")
|
|
|
| raise UnicodeError(f"Failed to decode {file_path} with any encoding")
|
|
|
| def _lookup_model_info(self, model_name: str) -> dict:
|
| """
|
| Find the model information from the model dictionary and return the information about the matched model.
|
|
|
| Parameters:
|
| model_name (str): The name of the live2d model.
|
|
|
| Returns:
|
| dict: The dictionary with the information of the matched model.
|
|
|
| Raises:
|
| FileNotFoundError if the model dictionary file is not found.
|
|
|
| json.JSONDecodeError if the model dictionary file is not a valid JSON file.
|
|
|
| KeyError if the model name is not found in the model dictionary.
|
|
|
| """
|
|
|
| self.live2d_model_name = model_name
|
|
|
| try:
|
| file_content = self._load_file_content(self.model_dict_path)
|
| model_dict = json.loads(file_content)
|
| except FileNotFoundError as file_e:
|
| logger.critical(
|
| f"Model dictionary file not found at {self.model_dict_path}."
|
| )
|
| raise file_e
|
| except json.JSONDecodeError as json_e:
|
| logger.critical(
|
| f"Error decoding JSON from model dictionary file at {self.model_dict_path}."
|
| )
|
| raise json_e
|
| except UnicodeError as uni_e:
|
| logger.critical(
|
| f"Error reading model dictionary file at {self.model_dict_path}."
|
| )
|
| raise uni_e
|
| except Exception as e:
|
| logger.critical(
|
| f"Error occurred while reading model dictionary file at {self.model_dict_path}."
|
| )
|
| raise e
|
|
|
|
|
| matched_model = next(
|
| (model for model in model_dict if model["name"] == model_name), None
|
| )
|
|
|
| if matched_model is None:
|
| logger.critical(f"Unable to find {model_name} in {self.model_dict_path}.")
|
| raise KeyError(
|
| f"{model_name} not found in model dictionary {self.model_dict_path}."
|
| )
|
|
|
|
|
|
|
| logger.info("Model Information Loaded.")
|
|
|
| return matched_model
|
|
|
| def extract_emotion(self, str_to_check: str) -> list:
|
| """
|
| Check the input string for any emotion keywords and return a list of values (the expression index) of the emotions found in the string.
|
|
|
| Parameters:
|
| str_to_check (str): The string to check for emotions.
|
|
|
| Returns:
|
| list: A list of values of the emotions found in the string. An empty list is returned if no emotions are found.
|
| """
|
|
|
| expression_list = []
|
| str_to_check = str_to_check.lower()
|
|
|
| i = 0
|
| while i < len(str_to_check):
|
| if str_to_check[i] != "[":
|
| i += 1
|
| continue
|
| for key in self.emo_map.keys():
|
| emo_tag = f"[{key}]"
|
| if str_to_check[i : i + len(emo_tag)] == emo_tag:
|
| expression_list.append(self.emo_map[key])
|
| i += len(emo_tag) - 1
|
| break
|
| i += 1
|
| return expression_list
|
|
|
| def remove_emotion_keywords(self, target_str: str) -> str:
|
| """
|
| Remove the emotion keywords from the input string and return the cleaned string.
|
|
|
| Parameters:
|
| str_to_check (str): The string to check for emotions.
|
|
|
| Returns:
|
| str: The cleaned string with the emotion keywords removed.
|
| """
|
|
|
| lower_str = target_str.lower()
|
|
|
| for key in self.emo_map.keys():
|
| lower_key = f"[{key}]".lower()
|
| while lower_key in lower_str:
|
| start_index = lower_str.find(lower_key)
|
| end_index = start_index + len(lower_key)
|
| target_str = target_str[:start_index] + target_str[end_index:]
|
| lower_str = lower_str[:start_index] + lower_str[end_index:]
|
| return target_str
|
|
|