first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +5 -0
- .gitignore +9 -0
- BookWorld.py +1064 -0
- Dockerfile +20 -0
- LICENSE +201 -0
- app.py +240 -0
- bw_utils.py +466 -0
- config.json +20 -0
- convert_sillytavern_cards_to_data.py +31 -0
- data/locations/A_Song_of_Ice_and_Fire.json +45 -0
- data/locations/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.json +9 -0
- data/locations/example_locations.json +24 -0
- data/maps/A_Song_of_Ice_and_Fire.csv +5 -0
- data/maps/A_Song_of_Ice_and_Fire_bloody_wedding.csv +2 -0
- data/maps/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.csv +2 -0
- data/maps/example_map.csv +4 -0
- data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/role_info.json +27 -0
- data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/Catelyn-Stark-zh/role_info.json +35 -0
- data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/Edmure-Tully-zh/role_info.json +34 -0
- data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/role_info.json +29 -0
- data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/Robb-Stark-zh/role_info.json +37 -0
- data/roles/A_Song_of_Ice_and_Fire/RobbStark-zh/role_info.json +37 -0
- data/roles/A_Song_of_Ice_and_Fire/Roose-Bolton-zh/role_info.json +36 -0
- data/roles/A_Song_of_Ice_and_Fire/Roslin-Frey-zh/role_info.json +34 -0
- data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/icon.png +3 -0
- data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/role_info.json +28 -0
- data/roles/A_Song_of_Ice_and_Fire/Tywin-Lannister-zh/role_info.json +35 -0
- data/roles/A_Song_of_Ice_and_Fire/Walder-Frey-zh/role_info.json +35 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_info.json +9 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_lines.jsonl +102 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_info.json +9 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_lines.jsonl +6 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Cheshire_Cat-en/role_info.json +9 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_info.json +9 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_lines.jsonl +4 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_info.json +9 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_lines.jsonl +1 -0
- data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Duchess-en/role_info.json +9 -0
.gitattributes
CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
37 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
38 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
39 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
40 |
+
*.ico filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/__pycache__/**
|
2 |
+
**/chromadb_saves/
|
3 |
+
/log/
|
4 |
+
/experiment_saves/
|
5 |
+
run.sh
|
6 |
+
/config/
|
7 |
+
.vscode/
|
8 |
+
nohup.out
|
9 |
+
BookWorld_test.py
|
BookWorld.py
ADDED
@@ -0,0 +1,1064 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from tqdm import tqdm
|
3 |
+
import json
|
4 |
+
import os
|
5 |
+
import warnings
|
6 |
+
import random
|
7 |
+
from typing import Any, Dict, List, Optional, Literal
|
8 |
+
from collections import defaultdict
|
9 |
+
import uuid
|
10 |
+
|
11 |
+
from bw_utils import *
|
12 |
+
from modules.main_role_agent import RPAgent
|
13 |
+
from modules.world_agent import WorldAgent
|
14 |
+
from modules.history_manager import HistoryManager
|
15 |
+
import argparse
|
16 |
+
|
17 |
+
warnings.filterwarnings('ignore')
|
18 |
+
|
19 |
+
class Server():
|
20 |
+
def __init__(self,
|
21 |
+
preset_path: str,
|
22 |
+
world_llm_name: str,
|
23 |
+
role_llm_name: str,
|
24 |
+
embedding_name:str = "bge-m3") :
|
25 |
+
"""
|
26 |
+
The initialization function of the system.
|
27 |
+
|
28 |
+
Args:
|
29 |
+
preset_path (str): The path to config file of this experiment.
|
30 |
+
world_llm_name (str, optional): The base model of the world agent. Defaults to 'gpt-4o'.
|
31 |
+
role_llm_name (str, optional): The base model of all the role agents. Defaults to 'gpt-4o'.
|
32 |
+
mode (str, optional): If set to be 'script', the role agents will act according to the given script.
|
33 |
+
If set to be 'free', the role agents will act freely based on their backround.
|
34 |
+
Defaults to 'free'.
|
35 |
+
"""
|
36 |
+
|
37 |
+
self.role_llm_name: str = role_llm_name
|
38 |
+
self.world_llm_name: str = world_llm_name
|
39 |
+
self.embedding_name:str = embedding_name
|
40 |
+
config = load_json_file(preset_path)
|
41 |
+
self.preset_path = preset_path
|
42 |
+
self.config: Dict = config
|
43 |
+
self.experiment_name: str = os.path.basename(preset_path).replace(".json","") + "/" + config["experiment_subname"] + "_" + role_llm_name
|
44 |
+
|
45 |
+
role_agent_codes: List[str] = config['role_agent_codes']
|
46 |
+
world_file_path: str = config["world_file_path"]
|
47 |
+
map_file_path: str = config["map_file_path"] if "map_file_path" in config else ""
|
48 |
+
role_file_dir: str = config["role_file_dir"] if "role_file_dir" in config else "./data/roles/"
|
49 |
+
loc_file_path: str = config["loc_file_path"]
|
50 |
+
self.intervention: str = "" if "intervention" in config else ""
|
51 |
+
self.event = self.intervention
|
52 |
+
self.script: str = config["script"] if "script" in config else ""
|
53 |
+
self.language: str = config["language"] if "language" in config else "zh"
|
54 |
+
self.source:str = config["source"] if "source" in config else ""
|
55 |
+
|
56 |
+
self.idx: int = 0
|
57 |
+
self.cur_round: int = 0
|
58 |
+
self.progress: str = "剧本刚刚开始,还什么都没有发生" if self.language == 'zh' else "The story has just begun, nothing happens yet."
|
59 |
+
self.moving_roles_info: Dict[str, Any] = {}
|
60 |
+
self.history_manager = HistoryManager()
|
61 |
+
self.start_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
62 |
+
self.current_status = {
|
63 |
+
"location_code":"",
|
64 |
+
"group":role_agent_codes,
|
65 |
+
}
|
66 |
+
self.scene_characters = {}
|
67 |
+
self.event_history = []
|
68 |
+
|
69 |
+
self.role_llm = get_models(role_llm_name)
|
70 |
+
self.logger = get_logger(self.experiment_name)
|
71 |
+
self.init_role_agents(role_agent_codes = role_agent_codes,
|
72 |
+
role_file_dir = role_file_dir,
|
73 |
+
world_file_path=world_file_path,
|
74 |
+
llm = self.role_llm)
|
75 |
+
|
76 |
+
if world_llm_name == role_llm_name:
|
77 |
+
self.world_llm = self.role_llm
|
78 |
+
else:
|
79 |
+
self.world_llm = get_models(world_llm_name)
|
80 |
+
self.init_world_agent_from_file(world_file_path = world_file_path,
|
81 |
+
map_file_path = map_file_path,
|
82 |
+
loc_file_path = loc_file_path,
|
83 |
+
llm = self.world_llm)
|
84 |
+
|
85 |
+
# Init
|
86 |
+
def init_role_agents(self,
|
87 |
+
role_agent_codes: List[str],
|
88 |
+
role_file_dir:str,
|
89 |
+
world_file_path:str,
|
90 |
+
llm) -> None:
|
91 |
+
self.role_codes: List[str] = role_agent_codes
|
92 |
+
self.role_agents: Dict[str, RPAgent] = {}
|
93 |
+
|
94 |
+
for role_code in role_agent_codes:
|
95 |
+
if check_role_code_availability(role_code,role_file_dir):
|
96 |
+
self.role_agents[role_code] = RPAgent(role_code=role_code,
|
97 |
+
role_file_dir=role_file_dir,
|
98 |
+
world_file_path=world_file_path,
|
99 |
+
source = self.source,
|
100 |
+
language=self.language,
|
101 |
+
llm_name = self.role_llm_name,
|
102 |
+
llm = llm,
|
103 |
+
embedding_name=self.embedding_name,
|
104 |
+
)
|
105 |
+
# print(f"{role_code} Initialized.")
|
106 |
+
else:
|
107 |
+
print(f"Warning: The specified role `{role_code}` does not exist.")
|
108 |
+
|
109 |
+
def init_world_agent_from_file(self,
|
110 |
+
world_file_path: str,
|
111 |
+
map_file_path: str,
|
112 |
+
loc_file_path: str,
|
113 |
+
llm) -> None:
|
114 |
+
self.world_agent: WorldAgent = WorldAgent(world_file_path = world_file_path,
|
115 |
+
location_file_path = loc_file_path,
|
116 |
+
map_file_path = map_file_path,
|
117 |
+
llm_name=self.world_llm_name,
|
118 |
+
llm = llm,
|
119 |
+
language=self.language)
|
120 |
+
for role_code in self.role_agents:
|
121 |
+
self.role_agents[role_code].world_db = self.world_agent.db
|
122 |
+
self.role_agents[role_code].world_db_name = self.world_agent.db_name
|
123 |
+
|
124 |
+
def init_role_locations(self, random_allocate: bool = True):
|
125 |
+
"""
|
126 |
+
Set initial positions of the roles.
|
127 |
+
|
128 |
+
Args:
|
129 |
+
random_allocate (bool, optional): if set to be True, the initial positions of the roles are randomly assigned. Defaults to True.
|
130 |
+
"""
|
131 |
+
init_locations_code = random.choices(self.world_agent.locations, k = len(self.role_codes))
|
132 |
+
for i,role_code in enumerate(self.role_codes):
|
133 |
+
self.role_agents[role_code].set_location(init_locations_code[i], self.world_agent.find_location_name(init_locations_code[i]))
|
134 |
+
info_text = f"{self.role_agents[role_code].nickname} 现在位于 {self.world_agent.find_location_name(init_locations_code[i])}" \
|
135 |
+
if self.language == "zh" else f"{self.role_agents[role_code].nickname} is now located at {self.world_agent.find_location_name(init_locations_code[i])}"
|
136 |
+
self.log(info_text)
|
137 |
+
|
138 |
+
# Simulation
|
139 |
+
def simulate_generator(self,
|
140 |
+
rounds: int = 10,
|
141 |
+
save_dir: str = "",
|
142 |
+
if_save: Literal[0,1] = 0,
|
143 |
+
mode: Literal["free", "script"] = "free",
|
144 |
+
scene_mode: Literal[0,1] = 1,):
|
145 |
+
"""
|
146 |
+
The main function of the simulation.
|
147 |
+
|
148 |
+
Args:
|
149 |
+
rounds (int, optional): The max rounds of simulation. Defaults to 10.
|
150 |
+
save_dir (str, optional): _description_. Defaults to "".
|
151 |
+
if_save (Literal[0,1], optional): _description_. Defaults to 0.
|
152 |
+
"""
|
153 |
+
self.mode = mode
|
154 |
+
meta_info: Dict[str, Any] = self.continue_simulation_from_file(save_dir)
|
155 |
+
self.if_save: int = if_save
|
156 |
+
start_round: int = meta_info["round"]
|
157 |
+
sub_start_round:int = meta_info["sub_round"] if "sub_round" in meta_info else 0
|
158 |
+
if start_round == rounds: return
|
159 |
+
|
160 |
+
# Setting Locations
|
161 |
+
if not meta_info["location_setted"]:
|
162 |
+
self.log("========== Start Location Setting ==========")
|
163 |
+
self.init_role_locations()
|
164 |
+
self._save_current_simulation("location")
|
165 |
+
|
166 |
+
# Setting Goals
|
167 |
+
if not meta_info["goal_setted"]:
|
168 |
+
yield ("system","","-- Setting Goals --",None)
|
169 |
+
self.log("========== Start Goal Setting ==========")
|
170 |
+
|
171 |
+
if self.mode == "free":
|
172 |
+
self.get_event()
|
173 |
+
self.log(f"--------- Free Mode: Current Event ---------\n{self.event}\n")
|
174 |
+
self.event_history.append(self.event)
|
175 |
+
elif self.mode == "script":
|
176 |
+
self.get_script()
|
177 |
+
self.log(f"--------- Script Mode: Setted Script ---------\n{self.script}\n")
|
178 |
+
self.event_history.append(self.event)
|
179 |
+
if self.mode == "free":
|
180 |
+
for role_code in self.role_codes:
|
181 |
+
motivation = self.role_agents[role_code].set_motivation(
|
182 |
+
world_description = self.world_agent.description,
|
183 |
+
other_roles_info = self._get_group_members_info_dict(self.role_agents),
|
184 |
+
intervention = self.event,
|
185 |
+
script = self.script
|
186 |
+
)
|
187 |
+
info_text = f"{self.role_agents[role_code].nickname} 设立了动机: {motivation}" \
|
188 |
+
if self.language == "zh" else f"{self.role_agents[role_code].nickname} has set the motivation: {motivation}"
|
189 |
+
|
190 |
+
record_id = str(uuid.uuid4())
|
191 |
+
self.log(info_text)
|
192 |
+
self.record(role_code=role_code,
|
193 |
+
detail=info_text,
|
194 |
+
actor = role_code,
|
195 |
+
group = [role_code],
|
196 |
+
actor_type = 'role',
|
197 |
+
act_type="goal setting",
|
198 |
+
record_id = record_id)
|
199 |
+
yield ("role",role_code,info_text,record_id)
|
200 |
+
|
201 |
+
self._save_current_simulation("goal")
|
202 |
+
|
203 |
+
yield ("system","","-- Simulation Started --",None)
|
204 |
+
selected_role_codes = []
|
205 |
+
# Simulating
|
206 |
+
for current_round in range(start_round, rounds):
|
207 |
+
self.cur_round = current_round
|
208 |
+
self.log(f"========== Round {current_round+1} Started ==========")
|
209 |
+
if self.event and current_round >= 1:
|
210 |
+
self.log(f"--------- Current Event ---------\n{self.event}\n")
|
211 |
+
yield ("world","","-- Current Event --\n"+self.event, None)
|
212 |
+
self.event_history.append(self.event)
|
213 |
+
|
214 |
+
if len(self.moving_roles_info) == len(self.role_codes):
|
215 |
+
self.settle_movement()
|
216 |
+
continue
|
217 |
+
|
218 |
+
# Characters in next scene
|
219 |
+
if scene_mode:
|
220 |
+
group = self._name2code(
|
221 |
+
self.world_agent.decide_screen_actors(
|
222 |
+
self._get_locations_info(False),
|
223 |
+
self.history_manager.get_recent_history(5),
|
224 |
+
self.event,
|
225 |
+
list(set(selected_role_codes + list(self.moving_roles_info.keys())))))
|
226 |
+
|
227 |
+
selected_role_codes += group
|
228 |
+
if len(selected_role_codes) > len(self.role_codes):
|
229 |
+
selected_role_codes = []
|
230 |
+
else:
|
231 |
+
group = self.role_codes
|
232 |
+
self.current_status['group'] = group
|
233 |
+
self.current_status['location_code'] = self.role_agents[group[0]].location_code
|
234 |
+
self.scene_characters[str(current_round)] = group
|
235 |
+
|
236 |
+
# Prologue
|
237 |
+
# if current_round == 0 and len(group) > 0
|
238 |
+
# prologue = self.world_agent.generate_location_prologue(location_code=self.role_agents[group[0]].location_code, history_text=self._get_history_text(group),event=self.event,location_info_text=self._find_roles_at_location(self.role_agents[group[0]].location_code,name=True))
|
239 |
+
# self.log("--Prologue--: "+prologue)
|
240 |
+
# self.record(role_code="None",detail=prologue,act_type="prologue")
|
241 |
+
start_idx = len(self.history_manager)
|
242 |
+
|
243 |
+
sub_round = sub_start_round
|
244 |
+
for sub_round in range(sub_start_round,3):
|
245 |
+
if self.mode == "script":
|
246 |
+
self.script_instruct(self.progress)
|
247 |
+
else:
|
248 |
+
for role_code in group:
|
249 |
+
self.role_agents[role_code].update_goal(other_roles_status=self._get_status_text(self.role_codes))
|
250 |
+
|
251 |
+
for role_code in group:
|
252 |
+
if scene_mode:
|
253 |
+
role_code = self._name2code(self.world_agent.decide_next_actor("\n".join(self.history_manager.get_recent_history(3)),self._get_group_members_info_text(group,status=True),self.script)) if scene_mode else role_code
|
254 |
+
|
255 |
+
yield from self.implement_next_plan(role_code = role_code,
|
256 |
+
group = group)
|
257 |
+
self._save_current_simulation("action", current_round, sub_round)
|
258 |
+
|
259 |
+
if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_recent_history(len(self.history_manager)-start_idx)))
|
260 |
+
if if_end:
|
261 |
+
record_id = str(uuid.uuid4())
|
262 |
+
self.log("--Epilogue--: "+epilogue)
|
263 |
+
self.record(role_code = "None",
|
264 |
+
detail = epilogue,
|
265 |
+
actor_type="world",
|
266 |
+
act_type="epilogue",
|
267 |
+
actor = "world",
|
268 |
+
group = [],
|
269 |
+
record_id = record_id)
|
270 |
+
yield ("world","","--Epilogue--: "+epilogue, record_id)
|
271 |
+
|
272 |
+
break
|
273 |
+
|
274 |
+
|
275 |
+
for role_code in group:
|
276 |
+
yield from self.decide_whether_to_move(role_code = role_code,
|
277 |
+
group = self._find_group(role_code))
|
278 |
+
self.role_agents[role_code].update_status()
|
279 |
+
|
280 |
+
self.settle_movement()
|
281 |
+
self.update_event(group)
|
282 |
+
|
283 |
+
sub_start_round = 0
|
284 |
+
self._save_current_simulation("action", current_round + 1,sub_round + 1)
|
285 |
+
|
286 |
+
# Main functions using llm
|
287 |
+
def implement_next_plan(self,role_code: str, group: List[str]):
|
288 |
+
other_roles_info = self._get_group_members_info_dict(group)
|
289 |
+
plan = self.role_agents[role_code].plan(
|
290 |
+
other_roles_info = other_roles_info,
|
291 |
+
available_locations = self.world_agent.locations,
|
292 |
+
world_description = self.world_agent.description,
|
293 |
+
intervention = self.event,
|
294 |
+
)
|
295 |
+
|
296 |
+
info_text = plan["detail"]
|
297 |
+
if plan["target_role_codes"]:
|
298 |
+
plan["target_role_codes"] = self._name2code(plan["target_role_codes"])
|
299 |
+
|
300 |
+
|
301 |
+
record_id = str(uuid.uuid4())
|
302 |
+
self.log(f"-Action-\n{self.role_agents[role_code].role_name}: "+ info_text)
|
303 |
+
self.record(role_code = role_code,
|
304 |
+
detail = plan["detail"],
|
305 |
+
actor_type = 'role',
|
306 |
+
act_type = "plan",
|
307 |
+
actor = role_code,
|
308 |
+
group = plan["target_role_codes"] + [role_code],
|
309 |
+
plan = plan,
|
310 |
+
record_id = record_id
|
311 |
+
)
|
312 |
+
yield ("role", role_code, info_text, record_id)
|
313 |
+
|
314 |
+
if plan["interact_type"] == "single" and len(plan["target_role_codes"]) == 1 and plan["target_role_codes"][0] in group:
|
315 |
+
yield from self.start_single_role_interaction(plan, record_id)
|
316 |
+
elif plan["interact_type"] == "multi" and len(plan["target_role_codes"]) > 1 and set(plan["target_role_codes"]).issubset(set(group)) :
|
317 |
+
yield from self.start_multi_role_interaction(plan, record_id)
|
318 |
+
elif plan["interact_type"] == "enviroment":
|
319 |
+
yield from self.start_enviroment_interaction(plan,role_code, record_id)
|
320 |
+
elif plan["interact_type"] == "npc" and plan["target_npc_name"]:
|
321 |
+
yield from self.start_npc_interaction(plan,role_code,target_name=plan["target_npc_name"], record_id = record_id)
|
322 |
+
return info_text
|
323 |
+
|
324 |
+
def decide_whether_to_move(self,
|
325 |
+
role_code: str,
|
326 |
+
group: List[str]):
|
327 |
+
if len(self.world_agent.locations) <= 1:
|
328 |
+
return False
|
329 |
+
if_move, move_detail, destination_code = self.role_agents[role_code].move(locations_info_text = self._get_locations_info(),
|
330 |
+
locations_info = self.world_agent.locations_info)
|
331 |
+
if if_move:
|
332 |
+
self.log(move_detail)
|
333 |
+
print(f"角色选择移动。{self.role_agents[role_code].role_name}正在前往{self.world_agent.find_location_name(destination_code)}" if self.language == "zh" else f"The role decides to move. {self.role_agents[role_code].role_name} is heading to {self.world_agent.find_location_name(destination_code)}.")
|
334 |
+
self.record(role_code = role_code,
|
335 |
+
detail = move_detail,
|
336 |
+
actor_type = 'role',
|
337 |
+
act_type = "move",
|
338 |
+
actor = role_code,
|
339 |
+
group = [role_code],
|
340 |
+
destinatiion_code = destination_code
|
341 |
+
)
|
342 |
+
yield ("role",role_code,move_detail,None)
|
343 |
+
|
344 |
+
distance = self.world_agent.get_distance(self.role_agents[role_code].location_code, destination_code)
|
345 |
+
self.role_agents[role_code].set_location(location_code=None, location_name=None)
|
346 |
+
self.moving_roles_info[role_code] = {
|
347 |
+
"location_code":destination_code,
|
348 |
+
"distance":distance
|
349 |
+
}
|
350 |
+
return if_move
|
351 |
+
|
352 |
+
def start_enviroment_interaction(self,
|
353 |
+
plan: Dict[str, Any],
|
354 |
+
role_code: str,
|
355 |
+
record_id: str):
|
356 |
+
"""
|
357 |
+
Handles the role's interaction with the environment.
|
358 |
+
It gets interaction results from agents in the world, record the result and update the status of the role.
|
359 |
+
|
360 |
+
Args:
|
361 |
+
plan (Dict[str, Any]): The details of the action.
|
362 |
+
role_code (str): The action maker.
|
363 |
+
|
364 |
+
Returns:
|
365 |
+
(str): The enviroment response.
|
366 |
+
"""
|
367 |
+
if "action" not in plan:
|
368 |
+
plan["action"] = ""
|
369 |
+
self.current_status['group'] = [role_code]
|
370 |
+
location_code = self.role_agents[role_code].location_code
|
371 |
+
result = self.world_agent.enviroment_interact(action_maker_name = self.role_agents[role_code].role_name,
|
372 |
+
action = plan["action"],
|
373 |
+
action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
|
374 |
+
location_code = location_code)
|
375 |
+
env_record_id = str(uuid.uuid4())
|
376 |
+
self.log(f"(Enviroment):{result}")
|
377 |
+
self.record(role_code = role_code,
|
378 |
+
detail = result,
|
379 |
+
actor_type = 'world',
|
380 |
+
act_type = "enviroment",
|
381 |
+
initiator = role_code,
|
382 |
+
actor = "world",
|
383 |
+
group = [role_code],
|
384 |
+
record_id = env_record_id)
|
385 |
+
yield ("world","","(Enviroment):" + result, env_record_id)
|
386 |
+
|
387 |
+
return conceal_thoughts(self.history_manager.search_record_detail(record_id)) + self.history_manager.search_record_detail(env_record_id)
|
388 |
+
|
389 |
+
def start_npc_interaction(self,
|
390 |
+
plan: Dict[str, Any],
|
391 |
+
role_code: str,
|
392 |
+
target_name: str,
|
393 |
+
record_id: str,
|
394 |
+
max_rounds: int = 3):
|
395 |
+
"""
|
396 |
+
Handles the role's interaction with the environment.
|
397 |
+
It gets interaction results from agents in the world, record the result and update the status of the role.
|
398 |
+
|
399 |
+
Args:
|
400 |
+
plan (Dict[str, Any]): The details of the action.
|
401 |
+
role_code (str): The action maker.
|
402 |
+
target_name (str): The target npc.
|
403 |
+
|
404 |
+
Returns:
|
405 |
+
(str): The enviroment response.
|
406 |
+
"""
|
407 |
+
interaction = plan
|
408 |
+
start_idx = len(self.history_manager)
|
409 |
+
|
410 |
+
self.log(f"----------NPC Interaction----------\n")
|
411 |
+
self.current_status['group'] = [role_code,target_name]
|
412 |
+
for round in range(max_rounds):
|
413 |
+
npc_interaction = self.world_agent.npc_interact(action_maker_name=self.role_agents[role_code].role_name,
|
414 |
+
action_detail=self.history_manager.search_record_detail(record_id),
|
415 |
+
location_name=self.role_agents[role_code].location_name,
|
416 |
+
target_name=target_name)
|
417 |
+
npc_detail = npc_interaction["detail"]
|
418 |
+
|
419 |
+
npc_record_id = str(uuid.uuid4())
|
420 |
+
self.log(f"{target_name}: " + npc_detail)
|
421 |
+
self.record(role_code = role_code,
|
422 |
+
detail = npc_detail,
|
423 |
+
actor_type = 'world',
|
424 |
+
act_type = "npc",
|
425 |
+
actor = "world",
|
426 |
+
group = [role_code],
|
427 |
+
npc_name = target_name,
|
428 |
+
record_id = npc_record_id
|
429 |
+
)
|
430 |
+
yield ("world","",f"(NPC-{target_name}):" + npc_detail, npc_record_id)
|
431 |
+
|
432 |
+
if npc_interaction["if_end_interaction"]:
|
433 |
+
break
|
434 |
+
|
435 |
+
interaction = self.role_agents[role_code].npc_interact(
|
436 |
+
npc_name = target_name,
|
437 |
+
npc_response = self.history_manager.search_record_detail(npc_record_id),
|
438 |
+
history = self.history_manager.get_subsequent_history(start_idx = start_idx),
|
439 |
+
intervention = self.event
|
440 |
+
)
|
441 |
+
detail = interaction["detail"]
|
442 |
+
|
443 |
+
record_id = str(uuid.uuid4())
|
444 |
+
self.log(f"{self.role_agents[role_code].role_name}: " + detail)
|
445 |
+
self.record(role_code = role_code,
|
446 |
+
detail = detail,
|
447 |
+
actor_type = 'role',
|
448 |
+
act_type = "npc",
|
449 |
+
actor = role_code,
|
450 |
+
group = [role_code],
|
451 |
+
npc_name = target_name,
|
452 |
+
record_id = record_id)
|
453 |
+
yield ("role",role_code,detail,record_id)
|
454 |
+
|
455 |
+
if interaction["if_end_interaction"]:
|
456 |
+
break
|
457 |
+
if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
|
458 |
+
if if_end:
|
459 |
+
break
|
460 |
+
|
461 |
+
return "\n".join(self.history_manager.get_subsequent_history(start_idx = start_idx))
|
462 |
+
|
463 |
+
def start_single_role_interaction(self,
|
464 |
+
plan: Dict[str, Any],
|
465 |
+
record_id: str,
|
466 |
+
max_rounds: int = 8):
|
467 |
+
interaction = plan
|
468 |
+
acted_role_code = interaction["role_code"]
|
469 |
+
acting_role_code = interaction["target_role_codes"][0]
|
470 |
+
if acting_role_code not in self.role_codes:
|
471 |
+
print(f"Warning: Role {acting_role_code} does not exist.")
|
472 |
+
return
|
473 |
+
self.current_status['group'] = [acted_role_code,acting_role_code]
|
474 |
+
|
475 |
+
start_idx = len(self.history_manager)
|
476 |
+
for round in range(max_rounds):
|
477 |
+
interaction = self.role_agents[acting_role_code].single_role_interact(
|
478 |
+
action_maker_code = acted_role_code,
|
479 |
+
action_maker_name = self.role_agents[acted_role_code].role_name,
|
480 |
+
action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
|
481 |
+
action_maker_profile = self.role_agents[acted_role_code].role_profile,
|
482 |
+
intervention = self.event
|
483 |
+
)
|
484 |
+
|
485 |
+
detail = interaction["detail"]
|
486 |
+
|
487 |
+
record_id = str(uuid.uuid4())
|
488 |
+
self.log(f"{self.role_agents[acting_role_code].role_name}: " + detail)
|
489 |
+
self.record(role_code = acting_role_code,
|
490 |
+
detail = detail,
|
491 |
+
actor_type = 'role',
|
492 |
+
act_type = "single",
|
493 |
+
group = [acted_role_code,acting_role_code],
|
494 |
+
target_role_code = acting_role_code,
|
495 |
+
planning_role_code = plan["role_code"],
|
496 |
+
round = round,
|
497 |
+
record_id = record_id
|
498 |
+
)
|
499 |
+
yield ("role",acting_role_code,detail,record_id)
|
500 |
+
|
501 |
+
if interaction["if_end_interaction"]:
|
502 |
+
return
|
503 |
+
if interaction["extra_interact_type"] == "npc":
|
504 |
+
print("---Extra NPC Interact---")
|
505 |
+
result = yield from self.start_npc_interaction(plan=interaction,
|
506 |
+
role_code=acted_role_code,
|
507 |
+
target_name=interaction["target_npc_name"],
|
508 |
+
record_id=record_id)
|
509 |
+
interaction["detail"] = result
|
510 |
+
|
511 |
+
elif interaction["extra_interact_type"] == "enviroment":
|
512 |
+
print("---Extra Env Interact---")
|
513 |
+
result = yield from self.start_enviroment_interaction(plan=interaction,role_code=acted_role_code,record_id=record_id)
|
514 |
+
interaction["detail"] = result
|
515 |
+
|
516 |
+
if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
|
517 |
+
if if_end:
|
518 |
+
break
|
519 |
+
acted_role_code,acting_role_code = acting_role_code,acted_role_code
|
520 |
+
return
|
521 |
+
|
522 |
+
def start_multi_role_interaction(self,
|
523 |
+
plan: Dict[str, Any],
|
524 |
+
record_id: str,
|
525 |
+
max_rounds: int = 8):
|
526 |
+
|
527 |
+
interaction = plan
|
528 |
+
acted_role_code = interaction["role_code"]
|
529 |
+
group = interaction["target_role_codes"]
|
530 |
+
group.append(acted_role_code)
|
531 |
+
|
532 |
+
for code in group:
|
533 |
+
if code not in self.role_codes:
|
534 |
+
print(f"Warning: Role {code} does not exist.")
|
535 |
+
return
|
536 |
+
self.current_status['group'] = group
|
537 |
+
|
538 |
+
start_idx = len(self.history_manager)
|
539 |
+
other_roles_info = self._get_group_members_info_dict(group)
|
540 |
+
|
541 |
+
for round in range(max_rounds):
|
542 |
+
acting_role_code = self._name2code(self.world_agent.decide_next_actor(history_text = "\n".join(self.history_manager.get_recent_history(3)),
|
543 |
+
roles_info_text = self._get_group_members_info_text(remove_list_elements(group,acted_role_code),status=True)))
|
544 |
+
|
545 |
+
|
546 |
+
interaction = self.role_agents[acting_role_code].multi_role_interact(
|
547 |
+
action_maker_code = acted_role_code,
|
548 |
+
action_maker_name = self.role_agents[acted_role_code].role_name,
|
549 |
+
action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
|
550 |
+
action_maker_profile = self.role_agents[acted_role_code].role_profile,
|
551 |
+
other_roles_info = other_roles_info,
|
552 |
+
intervention = self.event
|
553 |
+
)
|
554 |
+
|
555 |
+
detail = interaction["detail"]
|
556 |
+
|
557 |
+
record_id = str(uuid.uuid4())
|
558 |
+
self.log(f"{self.role_agents[acting_role_code].role_name}: "+ detail)
|
559 |
+
self.record(role_code = acting_role_code,
|
560 |
+
detail = detail,
|
561 |
+
actor_type = 'role',
|
562 |
+
act_type = "multi",
|
563 |
+
group = group,
|
564 |
+
actor = acting_role_code,
|
565 |
+
planning_role_code = plan["role_code"],
|
566 |
+
round = round,
|
567 |
+
record_id = record_id
|
568 |
+
)
|
569 |
+
yield ("role",acting_role_code,detail,record_id)
|
570 |
+
|
571 |
+
|
572 |
+
if interaction["if_end_interaction"]:
|
573 |
+
break
|
574 |
+
result = ""
|
575 |
+
if interaction["extra_interact_type"] == "npc":
|
576 |
+
print("---Extra NPC Interact---")
|
577 |
+
result = yield from self.start_npc_interaction(plan=interaction,role_code=acting_role_code,target_name=interaction["target_npc_name"],record_id = record_id)
|
578 |
+
elif interaction["extra_interact_type"] == "enviroment":
|
579 |
+
print("---Extra Env Interact---")
|
580 |
+
result = yield from self.start_enviroment_interaction(plan=interaction,role_code=acting_role_code,record_id = record_id)
|
581 |
+
interaction["detail"] = self.history_manager.search_record_detail(record_id) + result
|
582 |
+
acted_role_code = acting_role_code
|
583 |
+
if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
|
584 |
+
if if_end:
|
585 |
+
break
|
586 |
+
|
587 |
+
return
|
588 |
+
|
589 |
+
# Sub functions using llm
|
590 |
+
def script_instruct(self,
|
591 |
+
last_progress: str,
|
592 |
+
top_k: int = 5):
|
593 |
+
"""
|
594 |
+
Under the script mode, generate instruction for the roles at the beginning of each round.
|
595 |
+
|
596 |
+
Args:
|
597 |
+
last_progress (str): Where the script went in the last round.
|
598 |
+
top_k (int, optional): The number of action history of each role to refer. Defaults to 1.
|
599 |
+
|
600 |
+
Returns:
|
601 |
+
Dict[str, Any]: Instruction for each role.
|
602 |
+
"""
|
603 |
+
roles_info_text = self._get_group_members_info_text(self.role_codes,status=True)
|
604 |
+
history_text = "\n".join([self.role_agents[role_code].history_manager.get_recent_history(1)[0] for role_code in self.role_codes])
|
605 |
+
|
606 |
+
instruction = self.world_agent.get_script_instruction(
|
607 |
+
roles_info_text=roles_info_text,
|
608 |
+
event = self.event,
|
609 |
+
history_text=history_text,
|
610 |
+
script=self.script,
|
611 |
+
last_progress = last_progress)
|
612 |
+
|
613 |
+
for code in instruction:
|
614 |
+
if code == "progress":
|
615 |
+
self.log("剧本进度:"+ instruction["progress"]) if self.language == "zh" else self.log("Current Stage:"+ instruction["progress"])
|
616 |
+
elif code in self.role_codes:
|
617 |
+
# self.role_agents[code].update_goal(instruction = instruction[code])
|
618 |
+
self.role_agents[code].goal = instruction[code]
|
619 |
+
else:
|
620 |
+
print("Instruction failed, role code:",code)
|
621 |
+
return instruction
|
622 |
+
|
623 |
+
def get_event(self,):
|
624 |
+
if self.intervention == "" and not self.script:
|
625 |
+
roles_info_text = self._get_group_members_info_text(self.role_codes,profile=True)
|
626 |
+
status_text = self._get_status_text(self.role_codes)
|
627 |
+
event = self.world_agent.generate_event(roles_info_text=roles_info_text,event=self.intervention,history_text=status_text)
|
628 |
+
self.intervention = event
|
629 |
+
elif self.intervention == "" and self.script:
|
630 |
+
self.intervention = self.script
|
631 |
+
self.event = self.intervention
|
632 |
+
return self.intervention
|
633 |
+
|
634 |
+
def get_script(self,):
|
635 |
+
if self.script == "":
|
636 |
+
roles_info_text = self._get_group_members_info_text(self.role_codes,profile=True)
|
637 |
+
status = "\n".join([self.role_agents[role_code].status for role_code in self.role_codes])
|
638 |
+
script = self.world_agent.generate_script(roles_info_text=roles_info_text,event=self.intervention,history_text=status)
|
639 |
+
self.script = script
|
640 |
+
# self.event = self.script
|
641 |
+
return self.script
|
642 |
+
|
643 |
+
def update_event(self, group: List[str], top_k: int = 1):
|
644 |
+
if self.intervention == "":
|
645 |
+
self.event = ""
|
646 |
+
else:
|
647 |
+
status_text = self._get_status_text(group)
|
648 |
+
self.event = self.world_agent.update_event(self.event, self.intervention, status_text, script = self.script)
|
649 |
+
|
650 |
+
# other
|
651 |
+
def record(self,
|
652 |
+
role_code: str,
|
653 |
+
detail: str,
|
654 |
+
actor_type: str,
|
655 |
+
act_type: str,
|
656 |
+
group: List[str] = [],
|
657 |
+
actor: str = "",
|
658 |
+
record_id = None,
|
659 |
+
**kwargs):
|
660 |
+
if act_type == "plan" and "plan" in kwargs:
|
661 |
+
detail = f"{self.role_agents[role_code].nickname}: {detail}"
|
662 |
+
interact_type = kwargs["plan"]["interact_type"]
|
663 |
+
target = ", ".join(kwargs["plan"]["target_role_codes"])
|
664 |
+
other_info = f"Interact type: {interact_type}, Target: {target}"
|
665 |
+
elif act_type == "move" and "destination_code" in kwargs:
|
666 |
+
destination = kwargs["destination_code"]
|
667 |
+
other_info = f"Desitination:{destination}"
|
668 |
+
elif act_type == "single":
|
669 |
+
detail = f"{self.role_agents[role_code].nickname}: {detail}"
|
670 |
+
target, planning_role, round = kwargs["target_role_code"],kwargs["planning_role_code"],kwargs["round"]
|
671 |
+
other_info = f"Target: {target}, Planning Role: {planning_role}, Round: {round}"
|
672 |
+
elif act_type == "multi":
|
673 |
+
detail = f"{self.role_agents[role_code].nickname}: {detail}"
|
674 |
+
planning_role, round = kwargs["planning_role_code"],kwargs["round"]
|
675 |
+
other_info = f"Group member:{group}, Planning Role: {planning_role}, Round:{round},"
|
676 |
+
elif act_type == "npc":
|
677 |
+
name = kwargs["npc_name"]
|
678 |
+
other_info = f"Target: {name}"
|
679 |
+
elif act_type == "enviroment":
|
680 |
+
other_info = ""
|
681 |
+
else:
|
682 |
+
other_info = ""
|
683 |
+
record = {
|
684 |
+
"cur_round":self.cur_round,
|
685 |
+
"role_code":role_code,
|
686 |
+
"detail":detail,
|
687 |
+
"actor":actor,
|
688 |
+
"group":group, # visible group
|
689 |
+
"actor_type":actor_type,
|
690 |
+
"act_type":act_type,
|
691 |
+
"other_info":other_info,
|
692 |
+
"record_id":record_id
|
693 |
+
}
|
694 |
+
self.history_manager.add_record(record)
|
695 |
+
for code in group:
|
696 |
+
self.role_agents[code].record(record)
|
697 |
+
|
698 |
+
def settle_movement(self,):
|
699 |
+
for role_code in self.moving_roles_info.copy():
|
700 |
+
if not self.moving_roles_info[role_code]["distance"]:
|
701 |
+
location_code = self.moving_roles_info[role_code]["location_code"]
|
702 |
+
self.role_agents[role_code].set_location(location_code, self.world_agent.find_location_name(location_code))
|
703 |
+
self.log(f"{self.role_agents[role_code].role_name} 已到达 【{self.world_agent.find_location_name(location_code)}】" if self.language == "zh" else
|
704 |
+
f"{self.role_agents[role_code].role_name} has reached [{self.world_agent.find_location_name(location_code)}]")
|
705 |
+
del self.moving_roles_info[role_code]
|
706 |
+
else:
|
707 |
+
self.moving_roles_info[role_code]["distance"] -= 1
|
708 |
+
|
709 |
+
def _find_group(self,role_code):
|
710 |
+
return [code for code in self.role_codes if self.role_agents[code].location_code==self.role_agents[role_code].location_code]
|
711 |
+
|
712 |
+
def _find_roles_at_location(self,location_code,name = False):
|
713 |
+
if name:
|
714 |
+
return [self.role_agents[code].nickname for code in self.role_codes if self.role_agents[code].location_code==location_code]
|
715 |
+
else:
|
716 |
+
return [code for code in self.role_codes if self.role_agents[code].location_code==location_code]
|
717 |
+
|
718 |
+
def _get_status_text(self,group):
|
719 |
+
return "\n".join([self.role_agents[role_code].status for role_code in group])
|
720 |
+
|
721 |
+
def _get_group_members_info_text(self,group, profile = False,status = False):
|
722 |
+
roles_info_text = ""
|
723 |
+
for i, role_code in enumerate(group):
|
724 |
+
name = self.role_agents[role_code].role_name
|
725 |
+
roles_info_text += f"{i+1}. {name}\n(role_code:{role_code})\n"
|
726 |
+
if profile:
|
727 |
+
profile = self.role_agents[role_code].role_profile
|
728 |
+
roles_info_text += f"{profile}\n"
|
729 |
+
if status:
|
730 |
+
status = self.role_agents[role_code].status
|
731 |
+
roles_info_text += f"{status}\n"
|
732 |
+
return roles_info_text
|
733 |
+
|
734 |
+
def _get_group_members_info_dict(self,group: List[str]):
|
735 |
+
info = {
|
736 |
+
role_code: {
|
737 |
+
"nickname": self.role_agents[role_code].nickname,
|
738 |
+
"profile": self.role_agents[role_code].role_profile
|
739 |
+
}
|
740 |
+
for role_code in group
|
741 |
+
}
|
742 |
+
return info
|
743 |
+
|
744 |
+
def _get_locations_info(self,detailed = True):
|
745 |
+
location_info_text = "---当前各角色位置---\n" if self.language == "zh" else "---Current Location of Roles---\n"
|
746 |
+
if detailed:
|
747 |
+
for i,location_code in enumerate(self.world_agent.locations_info):
|
748 |
+
location_name = self.world_agent.find_location_name(location_code)
|
749 |
+
description = self.world_agent.locations_info[location_code]["description"]
|
750 |
+
location_info_text += f"\n{i+1}. {location_name}\nlocation_code:{location_code}\n{description}\n\n"
|
751 |
+
role_names = [f"{self.role_agents[code].role_name}({code})" for code in self.role_codes if self.role_agents[code].location_code == location_code]
|
752 |
+
role_names = ", ".join(role_names)
|
753 |
+
location_info_text += "目前在这里的角色有:" + role_names if self.language == "zh" else "Roles located here: " + role_names
|
754 |
+
else:
|
755 |
+
for i,location_code in enumerate(self.world_agent.locations_info):
|
756 |
+
location_name = self.world_agent.find_location_name(location_code)
|
757 |
+
role_names = [f"{self.role_agents[code].role_name}({code})" for code in self.role_codes if self.role_agents[code].location_code == location_code]
|
758 |
+
if len(role_names) == 0:continue
|
759 |
+
role_names = ", ".join(role_names)
|
760 |
+
location_info_text += f"【{location_name}】:" + role_names +";"
|
761 |
+
return location_info_text
|
762 |
+
|
763 |
+
def _name2code(self,roles):
|
764 |
+
name_dic = {self.role_agents[code].role_name:code for code in self.role_codes}
|
765 |
+
name_dic.update({self.role_agents[code].nickname:code for code in self.role_codes})
|
766 |
+
if isinstance(roles, list):
|
767 |
+
processed_roles = []
|
768 |
+
for role in roles:
|
769 |
+
if role in self.role_codes:
|
770 |
+
processed_roles.append(role)
|
771 |
+
elif role in name_dic:
|
772 |
+
processed_roles.append(name_dic[role])
|
773 |
+
elif "-" in role and role.split("-")[0] in name_dic:
|
774 |
+
processed_roles.append(name_dic[role.split("-")[0]])
|
775 |
+
elif role.replace("_","·") in self.role_codes:
|
776 |
+
processed_roles.append(role.replace("_","·"))
|
777 |
+
else:
|
778 |
+
processed_roles.append(role)
|
779 |
+
return processed_roles
|
780 |
+
elif isinstance(roles, str) :
|
781 |
+
roles = roles.replace("\n","")
|
782 |
+
if roles in self.role_codes:
|
783 |
+
return roles
|
784 |
+
elif roles in name_dic:
|
785 |
+
return name_dic[roles]
|
786 |
+
elif f"{roles}-{self.language}" in self.role_codes:
|
787 |
+
return f"{roles}-{self.language}"
|
788 |
+
elif "-" in roles and roles.split("-")[0] in name_dic:
|
789 |
+
return name_dic[roles.split("-")[0]]
|
790 |
+
elif roles.replace("_","·") in self.role_codes:
|
791 |
+
return roles.replace("_","·")
|
792 |
+
return roles
|
793 |
+
|
794 |
+
def log(self,text):
|
795 |
+
self.logger.info(text)
|
796 |
+
print(text)
|
797 |
+
|
798 |
+
def _save_current_simulation(self,
|
799 |
+
stage: Literal["location", "goal", "action"],
|
800 |
+
current_round: int = 0,
|
801 |
+
sub_round:int = 0):
|
802 |
+
"""
|
803 |
+
Save the current simulation progress.
|
804 |
+
|
805 |
+
Args:
|
806 |
+
stage (Literal["location", "goal", "action"]): The stage in which the simulation has been carried out
|
807 |
+
current_round (int, optional): If the stage is "action", specify the number of rounds that have been completed. Defaults to 0.
|
808 |
+
"""
|
809 |
+
if not self.if_save:
|
810 |
+
return
|
811 |
+
save_dir = f"./experiment_saves/{self.experiment_name}/{self.role_llm_name}/{self.start_time}"
|
812 |
+
create_dir(save_dir)
|
813 |
+
location_setted, goal_setted = False,False
|
814 |
+
if stage in ["location","goal","action"]:
|
815 |
+
location_setted = True
|
816 |
+
if stage in ["goal","action"]:
|
817 |
+
goal_setted = True
|
818 |
+
meta_info = {
|
819 |
+
"location_setted":location_setted,
|
820 |
+
"goal_setted": goal_setted,
|
821 |
+
"round": current_round,
|
822 |
+
"sub_round": sub_round,
|
823 |
+
}
|
824 |
+
|
825 |
+
save_json_file(os.path.join(save_dir, "meta_info.json"), meta_info)
|
826 |
+
name = self.experiment_name.split("/")[0]
|
827 |
+
save_json_file(os.path.join(save_dir, f"{name}.json"), self.config)
|
828 |
+
|
829 |
+
filename = os.path.join(save_dir, f"./server_info.json")
|
830 |
+
save_json_file(filename, self.__getstate__() )
|
831 |
+
|
832 |
+
self.history_manager.save_to_file(save_dir)
|
833 |
+
if hasattr(self, 'role_agents'):
|
834 |
+
for role_code in self.role_codes:
|
835 |
+
self.role_agents[role_code].save_to_file(save_dir)
|
836 |
+
self.world_agent.save_to_file(save_dir)
|
837 |
+
|
838 |
+
def continue_simulation_from_file(self, save_dir: str):
|
839 |
+
"""
|
840 |
+
Restore the record of the last simulation.
|
841 |
+
|
842 |
+
Args:
|
843 |
+
save_dir (str): The path where the last simulation record was saved.
|
844 |
+
|
845 |
+
Returns:
|
846 |
+
Dict[str, Any]: The meta information recording the progress of the simulation
|
847 |
+
"""
|
848 |
+
if os.path.exists(save_dir):
|
849 |
+
meta_info = load_json_file(os.path.join(save_dir, "./meta_info.json"))
|
850 |
+
filename = os.path.join(save_dir, f"./server_info.json")
|
851 |
+
states = load_json_file(filename)
|
852 |
+
self.__setstate__(states)
|
853 |
+
for role_code in self.role_codes:
|
854 |
+
self.role_agents[role_code].load_from_file(save_dir)
|
855 |
+
self.world_agent.load_from_file(save_dir)
|
856 |
+
self.history_manager.load_from_file(save_dir)
|
857 |
+
|
858 |
+
for record in self.history_manager.detailed_history:
|
859 |
+
for code in record["group"]:
|
860 |
+
if code in self.role_codes:
|
861 |
+
self.role_agents[code].record(record)
|
862 |
+
else:
|
863 |
+
meta_info = {
|
864 |
+
"location_setted":False,
|
865 |
+
"goal_setted": False,
|
866 |
+
"round": 0,
|
867 |
+
"sub_round": 0,
|
868 |
+
}
|
869 |
+
return meta_info
|
870 |
+
|
871 |
+
def __getstate__(self):
|
872 |
+
states = {key: value for key, value in self.__dict__.items() \
|
873 |
+
if isinstance(value, (str, int, list, dict, bool, type(None))) \
|
874 |
+
and key not in ['role_agents','world_agent','logger']}
|
875 |
+
|
876 |
+
return states
|
877 |
+
|
878 |
+
def __setstate__(self, states):
|
879 |
+
self.__dict__.update(states)
|
880 |
+
|
881 |
+
|
882 |
+
class BookWorld():
|
883 |
+
def __init__(self,
|
884 |
+
preset_path: str,
|
885 |
+
world_llm_name: str,
|
886 |
+
role_llm_name: str,
|
887 |
+
embedding_name:str = "bge-m3") :
|
888 |
+
self.server = Server(preset_path,
|
889 |
+
world_llm_name=world_llm_name,
|
890 |
+
role_llm_name=role_llm_name,
|
891 |
+
embedding_name=embedding_name)
|
892 |
+
self.selected_scene = None
|
893 |
+
|
894 |
+
def set_generator(self,
|
895 |
+
rounds:int = 10,
|
896 |
+
save_dir:str = "",
|
897 |
+
if_save: Literal[0,1] = 0,
|
898 |
+
mode: Literal["free", "script"] = "free",
|
899 |
+
scene_mode: Literal[0,1] = 0,):
|
900 |
+
self.server.continue_simulation_from_file(save_dir)
|
901 |
+
self.generator = self.server.simulate_generator(rounds = rounds,
|
902 |
+
save_dir = save_dir,
|
903 |
+
if_save = if_save,
|
904 |
+
mode = mode,
|
905 |
+
scene_mode = scene_mode)
|
906 |
+
def get_map_info(self):
|
907 |
+
location_codes = self.server.world_agent.locations
|
908 |
+
location_names = [self.server.world_agent.find_location_name(location_code) for location_code in location_codes]
|
909 |
+
n = len(location_codes)
|
910 |
+
distances = []
|
911 |
+
for i in range(n):
|
912 |
+
for j in range(i+1,n):
|
913 |
+
if self.server.world_agent.get_distance(location_codes[i], location_codes[j]):
|
914 |
+
distances.append({
|
915 |
+
"source": location_names[i],
|
916 |
+
"target": location_names[j],
|
917 |
+
"distance": self.server.world_agent.get_distance(location_codes[i], location_codes[j])
|
918 |
+
})
|
919 |
+
|
920 |
+
return {
|
921 |
+
"places": location_names,
|
922 |
+
"distances": distances
|
923 |
+
}
|
924 |
+
def select_scene(self,scene_number):
|
925 |
+
if scene_number == None:
|
926 |
+
self.selected_scene = scene_number
|
927 |
+
else:
|
928 |
+
self.selected_scene = str(scene_number)
|
929 |
+
|
930 |
+
def get_characters_info(self):
|
931 |
+
characters_info = []
|
932 |
+
if self.selected_scene == None:
|
933 |
+
codes = self.server.role_codes
|
934 |
+
else:
|
935 |
+
codes = self.server.scene_characters[str(self.selected_scene)]
|
936 |
+
for (i, code) in enumerate(codes):
|
937 |
+
agent = self.server.role_agents[code]
|
938 |
+
location = agent.location_name
|
939 |
+
if code in self.server.moving_roles_info:
|
940 |
+
location_name = self.server.world_agent.find_location_name(self.server.moving_roles_info[code]["location_code"])
|
941 |
+
distance = self.server.moving_roles_info[code]['distance']
|
942 |
+
location = f"Reaching {location_name}... ({distance})"
|
943 |
+
chara_info = {
|
944 |
+
"id": i,
|
945 |
+
"name": agent.nickname,
|
946 |
+
"icon": agent.icon_path,
|
947 |
+
"description": agent.role_profile,
|
948 |
+
"goal": agent.goal if agent.goal else agent.motivation,
|
949 |
+
"state": agent.status,
|
950 |
+
"location": location
|
951 |
+
}
|
952 |
+
characters_info.append(chara_info)
|
953 |
+
return characters_info
|
954 |
+
|
955 |
+
def generate_next_message(self):
|
956 |
+
message_type, code, text,message_id = next(self.generator)
|
957 |
+
if message_type == "role":
|
958 |
+
username = self.server.role_agents[code].role_name
|
959 |
+
icon_path = self.server.role_agents[code].icon_path
|
960 |
+
else:
|
961 |
+
username = message_type
|
962 |
+
icon_path = ""
|
963 |
+
message = {
|
964 |
+
'username': username,
|
965 |
+
'type': message_type, # role, world, system
|
966 |
+
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
967 |
+
'text': text,
|
968 |
+
'icon': icon_path,
|
969 |
+
"uuid": message_id,
|
970 |
+
"scene": self.server.cur_round
|
971 |
+
}
|
972 |
+
return message
|
973 |
+
|
974 |
+
def get_settings_info(self):
|
975 |
+
return self.server.world_agent.world_settings
|
976 |
+
|
977 |
+
def get_current_status(self):
|
978 |
+
status = self.server.current_status
|
979 |
+
status['event'] = self.server.event
|
980 |
+
group = []
|
981 |
+
for code in status['group']:
|
982 |
+
if code in self.server.role_codes:
|
983 |
+
group.append(self.server.role_agents[code].nickname)
|
984 |
+
else:
|
985 |
+
group.append(code)
|
986 |
+
status['group'] = group
|
987 |
+
location_code = self.server.current_status['location_code']
|
988 |
+
if location_code not in self.server.world_agent.locations_info:
|
989 |
+
location_name,location_description = "Undefined","Undefined"
|
990 |
+
else:
|
991 |
+
location_name,location_description = self.server.world_agent.find_location_name(location_code),self.server.world_agent.locations_info[location_code]["description"]
|
992 |
+
status['location'] = {'name': location_name, 'description': location_description}
|
993 |
+
return status
|
994 |
+
|
995 |
+
def handle_message_edit(self,record_id,new_text):
|
996 |
+
group = self.server.history_manager.modify_record(record_id,new_text)
|
997 |
+
for code in group:
|
998 |
+
self.server.role_agents[code].history_manager.modify_record(record_id,new_text)
|
999 |
+
return
|
1000 |
+
|
1001 |
+
def get_history_messages(self,save_dir):
|
1002 |
+
|
1003 |
+
messages = []
|
1004 |
+
for record in self.server.history_manager.detailed_history:
|
1005 |
+
message_type = record["actor_type"]
|
1006 |
+
code = record["role_code"]
|
1007 |
+
if message_type == "role":
|
1008 |
+
username = self.server.role_agents[code].role_name
|
1009 |
+
icon_path = self.server.role_agents[code].icon_path
|
1010 |
+
else:
|
1011 |
+
username = message_type
|
1012 |
+
icon_path = "./frontend/assets/images/default-icon.jpg"
|
1013 |
+
messages.append({
|
1014 |
+
'username': username,
|
1015 |
+
'type': message_type, # role, world, system
|
1016 |
+
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
1017 |
+
'text': record["detail"],
|
1018 |
+
'icon': icon_path,
|
1019 |
+
"uuid": record["record_id"],
|
1020 |
+
"scene": record["cur_round"]
|
1021 |
+
})
|
1022 |
+
return messages
|
1023 |
+
|
1024 |
+
def generate_story(self,):
|
1025 |
+
logs = self.server.history_manager.get_complete_history()
|
1026 |
+
story = self.server.world_agent.log2story(logs)
|
1027 |
+
return story
|
1028 |
+
|
1029 |
+
|
1030 |
+
|
1031 |
+
if __name__ == "__main__":
|
1032 |
+
parser = argparse.ArgumentParser()
|
1033 |
+
|
1034 |
+
parser.add_argument('--world_llm', type=str, default='gpt-4o-mini')
|
1035 |
+
parser.add_argument('--role_llm', type=str, default='gpt-4o-mini')
|
1036 |
+
parser.add_argument('--genre', type=str, default='mgdv2')
|
1037 |
+
parser.add_argument('--preset_path', type=str, default='')
|
1038 |
+
|
1039 |
+
parser.add_argument('--if_save', type=int, default=1, choices=[0,1])
|
1040 |
+
parser.add_argument('--scene_mode', type=int, default=0, choices=[0,1])
|
1041 |
+
parser.add_argument('--rounds', type=int, default=10)
|
1042 |
+
parser.add_argument('--save_dir', type=str, default='')
|
1043 |
+
parser.add_argument('--mode', type=str, default='free', choices=['free','script'])
|
1044 |
+
args = parser.parse_args()
|
1045 |
+
world_llm_name = args.world_llm
|
1046 |
+
role_llm_name = args.role_llm
|
1047 |
+
rounds = args.rounds
|
1048 |
+
genre = args.genre
|
1049 |
+
preset_path = args.preset_path
|
1050 |
+
save_dir = args.save_dir
|
1051 |
+
if_save = args.if_save
|
1052 |
+
scene_mode = args.scene_mode
|
1053 |
+
mode = args.mode
|
1054 |
+
if not preset_path:
|
1055 |
+
preset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),f"./config/experiment_{genre}.json")
|
1056 |
+
|
1057 |
+
bw = BookWorld(preset_path, world_llm_name=world_llm_name, role_llm_name=role_llm_name)
|
1058 |
+
bw.set_generator(rounds = rounds, save_dir = save_dir, if_save = if_save, scene_mode = scene_mode,mode = mode)
|
1059 |
+
|
1060 |
+
for i in range(100):
|
1061 |
+
try:
|
1062 |
+
bw.generate_next_message()
|
1063 |
+
except StopIteration:
|
1064 |
+
break
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10
|
2 |
+
|
3 |
+
RUN useradd -m -u 1000 user
|
4 |
+
USER user
|
5 |
+
|
6 |
+
ENV HOME=/home/user \
|
7 |
+
PATH=/home/user/.local/bin:$PATH
|
8 |
+
WORKDIR $HOME/app
|
9 |
+
|
10 |
+
COPY --chown=user . $HOME/app
|
11 |
+
|
12 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
13 |
+
|
14 |
+
RUN huggingface-cli download BAAI/bge-m3
|
15 |
+
|
16 |
+
ENV OPENAI_API_KEY=""
|
17 |
+
|
18 |
+
EXPOSE 8000
|
19 |
+
|
20 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Apache License
|
2 |
+
Version 2.0, January 2004
|
3 |
+
http://www.apache.org/licenses/
|
4 |
+
|
5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6 |
+
|
7 |
+
1. Definitions.
|
8 |
+
|
9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
11 |
+
|
12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13 |
+
the copyright owner that is granting the License.
|
14 |
+
|
15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
16 |
+
other entities that control, are controlled by, or are under common
|
17 |
+
control with that entity. For the purposes of this definition,
|
18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
19 |
+
direction or management of such entity, whether by contract or
|
20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22 |
+
|
23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24 |
+
exercising permissions granted by this License.
|
25 |
+
|
26 |
+
"Source" form shall mean the preferred form for making modifications,
|
27 |
+
including but not limited to software source code, documentation
|
28 |
+
source, and configuration files.
|
29 |
+
|
30 |
+
"Object" form shall mean any form resulting from mechanical
|
31 |
+
transformation or translation of a Source form, including but
|
32 |
+
not limited to compiled object code, generated documentation,
|
33 |
+
and conversions to other media types.
|
34 |
+
|
35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
36 |
+
Object form, made available under the License, as indicated by a
|
37 |
+
copyright notice that is included in or attached to the work
|
38 |
+
(an example is provided in the Appendix below).
|
39 |
+
|
40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41 |
+
form, that is based on (or derived from) the Work and for which the
|
42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
44 |
+
of this License, Derivative Works shall not include works that remain
|
45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46 |
+
the Work and Derivative Works thereof.
|
47 |
+
|
48 |
+
"Contribution" shall mean any work of authorship, including
|
49 |
+
the original version of the Work and any modifications or additions
|
50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
54 |
+
means any form of electronic, verbal, or written communication sent
|
55 |
+
to the Licensor or its representatives, including but not limited to
|
56 |
+
communication on electronic mailing lists, source code control systems,
|
57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
59 |
+
excluding communication that is conspicuously marked or otherwise
|
60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
61 |
+
|
62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
64 |
+
subsequently incorporated within the Work.
|
65 |
+
|
66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
71 |
+
Work and such Derivative Works in Source or Object form.
|
72 |
+
|
73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76 |
+
(except as stated in this section) patent license to make, have made,
|
77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78 |
+
where such license applies only to those patent claims licensable
|
79 |
+
by such Contributor that are necessarily infringed by their
|
80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
82 |
+
institute patent litigation against any entity (including a
|
83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84 |
+
or a Contribution incorporated within the Work constitutes direct
|
85 |
+
or contributory patent infringement, then any patent licenses
|
86 |
+
granted to You under this License for that Work shall terminate
|
87 |
+
as of the date such litigation is filed.
|
88 |
+
|
89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
90 |
+
Work or Derivative Works thereof in any medium, with or without
|
91 |
+
modifications, and in Source or Object form, provided that You
|
92 |
+
meet the following conditions:
|
93 |
+
|
94 |
+
(a) You must give any other recipients of the Work or
|
95 |
+
Derivative Works a copy of this License; and
|
96 |
+
|
97 |
+
(b) You must cause any modified files to carry prominent notices
|
98 |
+
stating that You changed the files; and
|
99 |
+
|
100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
101 |
+
that You distribute, all copyright, patent, trademark, and
|
102 |
+
attribution notices from the Source form of the Work,
|
103 |
+
excluding those notices that do not pertain to any part of
|
104 |
+
the Derivative Works; and
|
105 |
+
|
106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107 |
+
distribution, then any Derivative Works that You distribute must
|
108 |
+
include a readable copy of the attribution notices contained
|
109 |
+
within such NOTICE file, excluding those notices that do not
|
110 |
+
pertain to any part of the Derivative Works, in at least one
|
111 |
+
of the following places: within a NOTICE text file distributed
|
112 |
+
as part of the Derivative Works; within the Source form or
|
113 |
+
documentation, if provided along with the Derivative Works; or,
|
114 |
+
within a display generated by the Derivative Works, if and
|
115 |
+
wherever such third-party notices normally appear. The contents
|
116 |
+
of the NOTICE file are for informational purposes only and
|
117 |
+
do not modify the License. You may add Your own attribution
|
118 |
+
notices within Derivative Works that You distribute, alongside
|
119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
120 |
+
that such additional attribution notices cannot be construed
|
121 |
+
as modifying the License.
|
122 |
+
|
123 |
+
You may add Your own copyright statement to Your modifications and
|
124 |
+
may provide additional or different license terms and conditions
|
125 |
+
for use, reproduction, or distribution of Your modifications, or
|
126 |
+
for any such Derivative Works as a whole, provided Your use,
|
127 |
+
reproduction, and distribution of the Work otherwise complies with
|
128 |
+
the conditions stated in this License.
|
129 |
+
|
130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
132 |
+
by You to the Licensor shall be under the terms and conditions of
|
133 |
+
this License, without any additional terms or conditions.
|
134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135 |
+
the terms of any separate license agreement you may have executed
|
136 |
+
with Licensor regarding such Contributions.
|
137 |
+
|
138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
140 |
+
except as required for reasonable and customary use in describing the
|
141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
142 |
+
|
143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144 |
+
agreed to in writing, Licensor provides the Work (and each
|
145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147 |
+
implied, including, without limitation, any warranties or conditions
|
148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150 |
+
appropriateness of using or redistributing the Work and assume any
|
151 |
+
risks associated with Your exercise of permissions under this License.
|
152 |
+
|
153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
154 |
+
whether in tort (including negligence), contract, or otherwise,
|
155 |
+
unless required by applicable law (such as deliberate and grossly
|
156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157 |
+
liable to You for damages, including any direct, indirect, special,
|
158 |
+
incidental, or consequential damages of any character arising as a
|
159 |
+
result of this License or out of the use or inability to use the
|
160 |
+
Work (including but not limited to damages for loss of goodwill,
|
161 |
+
work stoppage, computer failure or malfunction, or any and all
|
162 |
+
other commercial damages or losses), even if such Contributor
|
163 |
+
has been advised of the possibility of such damages.
|
164 |
+
|
165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168 |
+
or other liability obligations and/or rights consistent with this
|
169 |
+
License. However, in accepting such obligations, You may act only
|
170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171 |
+
of any other Contributor, and only if You agree to indemnify,
|
172 |
+
defend, and hold each Contributor harmless for any liability
|
173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
174 |
+
of your accepting any such warranty or additional liability.
|
175 |
+
|
176 |
+
END OF TERMS AND CONDITIONS
|
177 |
+
|
178 |
+
APPENDIX: How to apply the Apache License to your work.
|
179 |
+
|
180 |
+
To apply the Apache License to your work, attach the following
|
181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182 |
+
replaced with your own identifying information. (Don't include
|
183 |
+
the brackets!) The text should be enclosed in the appropriate
|
184 |
+
comment syntax for the file format. We also recommend that a
|
185 |
+
file or class name and description of purpose be included on the
|
186 |
+
same "printed page" as the copyright notice for easier
|
187 |
+
identification within third-party archives.
|
188 |
+
|
189 |
+
Copyright [yyyy] [name of copyright owner]
|
190 |
+
|
191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192 |
+
you may not use this file except in compliance with the License.
|
193 |
+
You may obtain a copy of the License at
|
194 |
+
|
195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
196 |
+
|
197 |
+
Unless required by applicable law or agreed to in writing, software
|
198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200 |
+
See the License for the specific language governing permissions and
|
201 |
+
limitations under the License.
|
app.py
ADDED
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
2 |
+
from fastapi.staticfiles import StaticFiles
|
3 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
4 |
+
import uvicorn
|
5 |
+
import json
|
6 |
+
import asyncio
|
7 |
+
import os
|
8 |
+
from pathlib import Path
|
9 |
+
from datetime import datetime
|
10 |
+
from bw_utils import is_image, load_json_file
|
11 |
+
from BookWorld import BookWorld
|
12 |
+
|
13 |
+
app = FastAPI()
|
14 |
+
default_icon_path = './frontend/assets/images/default-icon.jpg'
|
15 |
+
config = load_json_file('config.json')
|
16 |
+
for key in config:
|
17 |
+
if "API_KEY" in key:
|
18 |
+
os.environ[key] = config[key]
|
19 |
+
|
20 |
+
static_file_abspath = os.path.abspath(os.path.join(os.path.dirname(__file__), 'frontend'))
|
21 |
+
app.mount("/frontend", StaticFiles(directory=static_file_abspath), name="frontend")
|
22 |
+
|
23 |
+
class ConnectionManager:
|
24 |
+
def __init__(self):
|
25 |
+
self.active_connections: dict[str, WebSocket] = {}
|
26 |
+
self.story_tasks: dict[str, asyncio.Task] = {}
|
27 |
+
if True:
|
28 |
+
if "preset_path" in config and config["preset_path"] and os.path.exists(config["preset_path"]):
|
29 |
+
preset_path = config["preset_path"]
|
30 |
+
elif "genre" in config and config["genre"]:
|
31 |
+
genre = config["genre"]
|
32 |
+
preset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),f"./config/experiment_{genre}.json")
|
33 |
+
else:
|
34 |
+
raise ValueError("Please set the preset_path in `config.json`.")
|
35 |
+
self.bw = BookWorld(preset_path = preset_path,
|
36 |
+
world_llm_name = config["world_llm_name"],
|
37 |
+
role_llm_name = config["role_llm_name"],
|
38 |
+
embedding_name = config["embedding_model_name"])
|
39 |
+
self.bw.set_generator(rounds = config["rounds"],
|
40 |
+
save_dir = config["save_dir"],
|
41 |
+
if_save = config["if_save"],
|
42 |
+
mode = config["mode"],
|
43 |
+
scene_mode = config["scene_mode"],)
|
44 |
+
else:
|
45 |
+
from BookWorld_test import BookWorld_test
|
46 |
+
self.bw = BookWorld_test()
|
47 |
+
|
48 |
+
async def connect(self, websocket: WebSocket, client_id: str):
|
49 |
+
await websocket.accept()
|
50 |
+
self.active_connections[client_id] = websocket
|
51 |
+
|
52 |
+
def disconnect(self, client_id: str):
|
53 |
+
if client_id in self.active_connections:
|
54 |
+
del self.active_connections[client_id]
|
55 |
+
self.stop_story(client_id)
|
56 |
+
|
57 |
+
def stop_story(self, client_id: str):
|
58 |
+
if client_id in self.story_tasks:
|
59 |
+
self.story_tasks[client_id].cancel()
|
60 |
+
del self.story_tasks[client_id]
|
61 |
+
|
62 |
+
async def start_story(self, client_id: str):
|
63 |
+
if client_id in self.story_tasks:
|
64 |
+
# 如果已经有任务在运行,先停止它
|
65 |
+
self.stop_story(client_id)
|
66 |
+
|
67 |
+
# 创建新的故事任务
|
68 |
+
self.story_tasks[client_id] = asyncio.create_task(
|
69 |
+
self.generate_story(client_id)
|
70 |
+
)
|
71 |
+
|
72 |
+
async def generate_story(self, client_id: str):
|
73 |
+
"""持续生成故事的协程"""
|
74 |
+
try:
|
75 |
+
while True:
|
76 |
+
if client_id in self.active_connections:
|
77 |
+
message,status = await self.get_next_message()
|
78 |
+
await self.active_connections[client_id].send_json({
|
79 |
+
'type': 'message',
|
80 |
+
'data': message
|
81 |
+
})
|
82 |
+
await self.active_connections[client_id].send_json({
|
83 |
+
'type': 'status_update',
|
84 |
+
'data': status
|
85 |
+
})
|
86 |
+
# 添加延迟,控制消息发送频率
|
87 |
+
await asyncio.sleep(1) # 可以调整这个值
|
88 |
+
else:
|
89 |
+
break
|
90 |
+
except asyncio.CancelledError:
|
91 |
+
# 任务被取消时的处理
|
92 |
+
print(f"Story generation cancelled for client {client_id}")
|
93 |
+
except Exception as e:
|
94 |
+
print(f"Error in generate_story: {e}")
|
95 |
+
|
96 |
+
async def get_initial_data(self):
|
97 |
+
"""获取初始化数据"""
|
98 |
+
return {
|
99 |
+
'characters': self.bw.get_characters_info(),
|
100 |
+
'map': self.bw.get_map_info(),
|
101 |
+
'settings': self.bw.get_settings_info(),
|
102 |
+
'status': self.bw.get_current_status(),
|
103 |
+
'history_messages':self.bw.get_history_messages(save_dir = config["save_dir"]),
|
104 |
+
}
|
105 |
+
|
106 |
+
async def get_next_message(self):
|
107 |
+
"""从BookWorld获取下一条消息"""
|
108 |
+
message = self.bw.generate_next_message()
|
109 |
+
if not is_image(message["icon"]):
|
110 |
+
message["icon"] = default_icon_path
|
111 |
+
status = self.bw.get_current_status()
|
112 |
+
return message,status
|
113 |
+
|
114 |
+
manager = ConnectionManager()
|
115 |
+
|
116 |
+
@app.get("/")
|
117 |
+
async def get():
|
118 |
+
html_file = Path("index.html")
|
119 |
+
return HTMLResponse(html_file.read_text())
|
120 |
+
|
121 |
+
@app.get("/data/{full_path:path}")
|
122 |
+
async def get_file(full_path: str):
|
123 |
+
# 可以设置多个基础路径
|
124 |
+
base_paths = [
|
125 |
+
Path("/data/")
|
126 |
+
]
|
127 |
+
|
128 |
+
for base_path in base_paths:
|
129 |
+
file_path = base_path / full_path
|
130 |
+
if file_path.exists() and file_path.is_file():
|
131 |
+
return FileResponse(file_path)
|
132 |
+
else:
|
133 |
+
return FileResponse(default_icon_path)
|
134 |
+
|
135 |
+
raise HTTPException(status_code=404, detail="File not found")
|
136 |
+
|
137 |
+
@app.websocket("/ws/{client_id}")
|
138 |
+
async def websocket_endpoint(websocket: WebSocket, client_id: str):
|
139 |
+
await manager.connect(websocket, client_id)
|
140 |
+
try:
|
141 |
+
initial_data = await manager.get_initial_data()
|
142 |
+
await websocket.send_json({
|
143 |
+
'type': 'initial_data',
|
144 |
+
'data': initial_data
|
145 |
+
})
|
146 |
+
|
147 |
+
while True:
|
148 |
+
data = await websocket.receive_text()
|
149 |
+
message = json.loads(data)
|
150 |
+
|
151 |
+
if message['type'] == 'user_message':
|
152 |
+
# 处理用户消息
|
153 |
+
await websocket.send_json({
|
154 |
+
'type': 'message',
|
155 |
+
'data': {
|
156 |
+
'username': 'User',
|
157 |
+
'timestamp': message['timestamp'],
|
158 |
+
'text': message['text'],
|
159 |
+
'icon': default_icon_path,
|
160 |
+
}
|
161 |
+
})
|
162 |
+
|
163 |
+
elif message['type'] == 'control':
|
164 |
+
# 处理控制命令
|
165 |
+
if message['action'] == 'start':
|
166 |
+
await manager.start_story(client_id)
|
167 |
+
elif message['action'] == 'pause':
|
168 |
+
manager.stop_story(client_id)
|
169 |
+
elif message['action'] == 'stop':
|
170 |
+
manager.stop_story(client_id)
|
171 |
+
# 可以在这里添加额外的停止逻辑
|
172 |
+
|
173 |
+
elif message['type'] == 'edit_message':
|
174 |
+
# 处理消息编辑
|
175 |
+
edit_data = message['data']
|
176 |
+
# 假设 BookWorld 类有一个处理编辑的方法
|
177 |
+
manager.bw.handle_message_edit(
|
178 |
+
record_id=edit_data['uuid'],
|
179 |
+
new_text=edit_data['text']
|
180 |
+
)
|
181 |
+
|
182 |
+
elif message['type'] == 'request_scene_characters':
|
183 |
+
manager.bw.select_scene(message['scene'])
|
184 |
+
scene_characters = manager.bw.get_characters_info()
|
185 |
+
await websocket.send_json({
|
186 |
+
'type': 'scene_characters',
|
187 |
+
'data': scene_characters
|
188 |
+
})
|
189 |
+
|
190 |
+
elif message['type'] == 'generate_story':
|
191 |
+
# 生成故事文本
|
192 |
+
story_text = manager.bw.generate_story()
|
193 |
+
# 发送生成的故事作为新消息
|
194 |
+
await websocket.send_json({
|
195 |
+
'type': 'message',
|
196 |
+
'data': {
|
197 |
+
'username': 'System',
|
198 |
+
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
199 |
+
'text': story_text,
|
200 |
+
'icon': default_icon_path,
|
201 |
+
'type': 'story'
|
202 |
+
}
|
203 |
+
})
|
204 |
+
|
205 |
+
elif message['type'] == 'request_api_configs':
|
206 |
+
await websocket.send_json({
|
207 |
+
'type': 'api_configs',
|
208 |
+
'data': API_CONFIGS
|
209 |
+
})
|
210 |
+
|
211 |
+
elif message['type'] == 'api_settings':
|
212 |
+
# 处理API设置
|
213 |
+
settings = message['data']
|
214 |
+
# 设置环境变量
|
215 |
+
os.environ[settings['envKey']] = settings['apiKey']
|
216 |
+
|
217 |
+
# 更新BookWorld的设置
|
218 |
+
manager.bw.update_api_settings(
|
219 |
+
provider=settings['provider'],
|
220 |
+
model=settings['model']
|
221 |
+
)
|
222 |
+
|
223 |
+
# 发送确认消息
|
224 |
+
await websocket.send_json({
|
225 |
+
'type': 'message',
|
226 |
+
'data': {
|
227 |
+
'username': 'System',
|
228 |
+
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
229 |
+
'text': f'已更新 {settings["provider"]} API设置',
|
230 |
+
'icon': default_icon_path,
|
231 |
+
'type': 'system'
|
232 |
+
}
|
233 |
+
})
|
234 |
+
except Exception as e:
|
235 |
+
print(f"WebSocket error: {e}")
|
236 |
+
finally:
|
237 |
+
manager.disconnect(client_id)
|
238 |
+
|
239 |
+
if __name__ == "__main__":
|
240 |
+
uvicorn.run("server:app", host="0.0.0.0", port=8000, reload=True)
|
bw_utils.py
ADDED
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pickle
|
3 |
+
import json
|
4 |
+
import torch
|
5 |
+
import logging
|
6 |
+
import datetime
|
7 |
+
import re
|
8 |
+
import random
|
9 |
+
import base64
|
10 |
+
|
11 |
+
MODEL_NAME_DICT = {
|
12 |
+
"gpt3":"openai/gpt-3.5-turbo",
|
13 |
+
"gpt-4":"openai/gpt-4",
|
14 |
+
"gpt-4o":"openai/gpt-4o",
|
15 |
+
"gpt-4o-mini":"openai/gpt-4o-mini",
|
16 |
+
"gpt-3.5-turbo":"openai/gpt-3.5-turbo",
|
17 |
+
"deepseek-r1":"deepseek/deepseek-r1",
|
18 |
+
"deepseek-v3":"deepseek/deepseek-chat",
|
19 |
+
"gemini-2":"google/gemini-2.0-flash-001",
|
20 |
+
"gemini-1.5":"google/gemini-flash-1.5",
|
21 |
+
"llama3-70b": "meta-llama/llama-3.3-70b-instruct",
|
22 |
+
"qwen-turbo":"qwen/qwen-turbo",
|
23 |
+
"qwen-plus":"qwen/qwen-plus",
|
24 |
+
"qwen-max":"qwen/qwen-max",
|
25 |
+
"qwen-2.5-72b":"qwen/qwen-2.5-72b-instruct",
|
26 |
+
"claude-3.5-sonnet":"anthropic/claude-3.5-sonnet",
|
27 |
+
"phi-4":"microsoft/phi-4",
|
28 |
+
}
|
29 |
+
|
30 |
+
def get_models(model_name):
|
31 |
+
if os.getenv("OPENROUTER_API_KEY", default="") and model_name in MODEL_NAME_DICT:
|
32 |
+
from modules.llm.OpenRouter import OpenRouter
|
33 |
+
return OpenRouter(model=MODEL_NAME_DICT[model_name])
|
34 |
+
elif model_name.startswith('gpt-3.5'):
|
35 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
36 |
+
return LangChainGPT(model="gpt-3.5-turbo")
|
37 |
+
elif model_name == 'gpt-4':
|
38 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
39 |
+
return LangChainGPT(model="gpt-4")
|
40 |
+
elif model_name == 'gpt-4-turbo':
|
41 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
42 |
+
return LangChainGPT(model="gpt-4")
|
43 |
+
elif model_name == 'gpt-4o':
|
44 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
45 |
+
return LangChainGPT(model="gpt-4o")
|
46 |
+
elif model_name == "gpt-4o-mini":
|
47 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
48 |
+
return LangChainGPT(model="gpt-4o-mini")
|
49 |
+
elif model_name.startswith("claude"):
|
50 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
51 |
+
return LangChainGPT(model="claude-3-5-sonnet-20241022")
|
52 |
+
elif model_name.startswith('qwen'):
|
53 |
+
from modules.llm.Qwen import Qwen
|
54 |
+
return Qwen(model = model_name)
|
55 |
+
elif model_name.startswith('deepseek'):
|
56 |
+
from modules.llm.DeepSeek import DeepSeek
|
57 |
+
return DeepSeek()
|
58 |
+
elif model_name.startswith('doubao'):
|
59 |
+
from modules.llm.Doubao import Doubao
|
60 |
+
return Doubao()
|
61 |
+
elif model_name.startswith('gemini'):
|
62 |
+
from modules.llm.Gemini import Gemini
|
63 |
+
return Gemini()
|
64 |
+
else:
|
65 |
+
print(f'Warning! undefined model {model_name}, use gpt-3.5-turbo instead.')
|
66 |
+
from modules.llm.LangChainGPT import LangChainGPT
|
67 |
+
return LangChainGPT()
|
68 |
+
|
69 |
+
def build_world_agent_data(world_file_path,max_words = 30):
|
70 |
+
world_dir = os.path.dirname(world_file_path)
|
71 |
+
details_dir = os.path.join(world_dir,"./world_details")
|
72 |
+
data = []
|
73 |
+
settings = []
|
74 |
+
if os.path.exists(details_dir):
|
75 |
+
for path in get_child_paths(details_dir):
|
76 |
+
if os.path.splitext(path)[-1] == ".txt":
|
77 |
+
text = load_text_file(path)
|
78 |
+
data += split_text_by_max_words(text,max_words)
|
79 |
+
if os.path.splitext(path)[-1] == ".jsonl":
|
80 |
+
jsonl = load_jsonl_file(path)
|
81 |
+
data += [f"{dic['term']}:{dic['detail']}" for dic in jsonl]
|
82 |
+
settings += jsonl
|
83 |
+
return data,settings
|
84 |
+
|
85 |
+
def build_db(data, db_name, db_type, embedding, save_type="persistent"):
|
86 |
+
if True:
|
87 |
+
from modules.db.ChromaDB import ChromaDB
|
88 |
+
db = ChromaDB(embedding,save_type)
|
89 |
+
db_name = db_name
|
90 |
+
db.init_from_data(data,db_name)
|
91 |
+
return db
|
92 |
+
|
93 |
+
def get_root_dir():
|
94 |
+
current_file_path = os.path.abspath(__file__)
|
95 |
+
root_dir = os.path.dirname(current_file_path)
|
96 |
+
return root_dir
|
97 |
+
|
98 |
+
def create_dir(dirname):
|
99 |
+
if not os.path.exists(dirname):
|
100 |
+
os.makedirs(dirname)
|
101 |
+
|
102 |
+
def get_logger(experiment_name):
|
103 |
+
logger = logging.getLogger(experiment_name)
|
104 |
+
logger.setLevel(logging.INFO)
|
105 |
+
current_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
106 |
+
create_dir(f"{get_root_dir()}/log/{experiment_name}")
|
107 |
+
file_handler = logging.FileHandler(os.path.join(get_root_dir(),f"./log/{experiment_name}/{current_time}.log"),encoding='utf-8')
|
108 |
+
file_handler.setLevel(logging.INFO)
|
109 |
+
|
110 |
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
111 |
+
file_handler.setFormatter(formatter)
|
112 |
+
|
113 |
+
logger.addHandler(file_handler)
|
114 |
+
|
115 |
+
# Avoid logging duplication
|
116 |
+
logger.propagate = False
|
117 |
+
|
118 |
+
return logger
|
119 |
+
|
120 |
+
def merge_text_with_limit(text_list, max_words, language = 'en'):
|
121 |
+
"""
|
122 |
+
Merge a list of text strings into one, stopping when adding another text exceeds the maximum count.
|
123 |
+
|
124 |
+
Args:
|
125 |
+
text_list (list): List of strings to be merged.
|
126 |
+
max_count (int): Maximum number of characters (for Chinese) or words (for English).
|
127 |
+
is_chinese (bool): If True, count Chinese characters; if False, count English words.
|
128 |
+
|
129 |
+
Returns:
|
130 |
+
str: The merged text, truncated as needed.
|
131 |
+
"""
|
132 |
+
merged_text = ""
|
133 |
+
current_count = 0
|
134 |
+
|
135 |
+
for text in text_list:
|
136 |
+
if language == 'zh':
|
137 |
+
# Count Chinese characters
|
138 |
+
text_length = len(text)
|
139 |
+
else:
|
140 |
+
# Count English words
|
141 |
+
text_length = len(text.split(" "))
|
142 |
+
|
143 |
+
if current_count + text_length > max_words:
|
144 |
+
break
|
145 |
+
|
146 |
+
merged_text += text + "\n"
|
147 |
+
current_count += text_length
|
148 |
+
|
149 |
+
return merged_text
|
150 |
+
|
151 |
+
def normalize_string(text):
|
152 |
+
# 去除空格并将所有字母转为小写
|
153 |
+
import re
|
154 |
+
return re.sub(r'[\s\,\;\t\n]+', '', text).lower()
|
155 |
+
|
156 |
+
def fuzzy_match(str1, str2, threshold=0.8):
|
157 |
+
str1_normalized = normalize_string(str1)
|
158 |
+
str2_normalized = normalize_string(str2)
|
159 |
+
|
160 |
+
if str1_normalized == str2_normalized:
|
161 |
+
return True
|
162 |
+
|
163 |
+
return False
|
164 |
+
|
165 |
+
def load_character_card(path):
|
166 |
+
from PIL import Image
|
167 |
+
import PIL.PngImagePlugin
|
168 |
+
|
169 |
+
image = Image.open(path)
|
170 |
+
if isinstance(image, PIL.PngImagePlugin.PngImageFile):
|
171 |
+
for key, value in image.text.items():
|
172 |
+
try:
|
173 |
+
character_info = json.loads(decode_base64(value))
|
174 |
+
if character_info:
|
175 |
+
return character_info
|
176 |
+
except:
|
177 |
+
continue
|
178 |
+
return None
|
179 |
+
|
180 |
+
def decode_base64(encoded_string):
|
181 |
+
# Convert the string to bytes if it's not already
|
182 |
+
if isinstance(encoded_string, str):
|
183 |
+
encoded_bytes = encoded_string.encode('ascii')
|
184 |
+
else:
|
185 |
+
encoded_bytes = encoded_string
|
186 |
+
|
187 |
+
# Decode the Base64 bytes
|
188 |
+
decoded_bytes = base64.b64decode(encoded_bytes)
|
189 |
+
|
190 |
+
# Try to convert the result to a string, assuming UTF-8 encoding
|
191 |
+
try:
|
192 |
+
decoded_string = decoded_bytes.decode('utf-8')
|
193 |
+
return decoded_string
|
194 |
+
except UnicodeDecodeError:
|
195 |
+
# If it's not valid UTF-8 text, return the raw bytes
|
196 |
+
return decoded_bytes
|
197 |
+
|
198 |
+
def remove_list_elements(list1, *args):
|
199 |
+
for target in args:
|
200 |
+
if isinstance(target,list) or isinstance(target,dict):
|
201 |
+
list1 = [i for i in list1 if i not in target]
|
202 |
+
else:
|
203 |
+
list1 = [i for i in list1 if i != target]
|
204 |
+
return list1
|
205 |
+
|
206 |
+
def extract_html_content(html):
|
207 |
+
from bs4 import BeautifulSoup
|
208 |
+
soup = BeautifulSoup(html, "html.parser")
|
209 |
+
|
210 |
+
content_div = soup.find("div", {"id": "content"})
|
211 |
+
if not content_div:
|
212 |
+
return ""
|
213 |
+
|
214 |
+
paragraphs = []
|
215 |
+
for div in content_div.find_all("div"):
|
216 |
+
paragraphs.append(div.get_text(strip=True))
|
217 |
+
|
218 |
+
main_content = "\n\n".join(paragraphs)
|
219 |
+
return main_content
|
220 |
+
|
221 |
+
def load_text_file(path):
|
222 |
+
with open(path,"r",encoding="utf-8") as f:
|
223 |
+
text = f.read()
|
224 |
+
return text
|
225 |
+
|
226 |
+
def save_text_file(path,target):
|
227 |
+
with open(path,"w",encoding="utf-8") as f:
|
228 |
+
text = f.write(target)
|
229 |
+
|
230 |
+
def load_json_file(path):
|
231 |
+
with open(path,"r",encoding="utf-8") as f:
|
232 |
+
return json.load(f)
|
233 |
+
|
234 |
+
def save_json_file(path,target):
|
235 |
+
dir_name = os.path.dirname(path)
|
236 |
+
if not os.path.exists(dir_name):
|
237 |
+
os.makedirs(dir_name)
|
238 |
+
with open(path,"w",encoding="utf-8") as f:
|
239 |
+
json.dump(target, f, ensure_ascii=False,indent=True)
|
240 |
+
|
241 |
+
def load_jsonl_file(path):
|
242 |
+
data = []
|
243 |
+
with open(path,"r",encoding="utf-8") as f:
|
244 |
+
for line in f:
|
245 |
+
data.append(json.loads(line))
|
246 |
+
return data
|
247 |
+
|
248 |
+
def save_jsonl_file(path,target):
|
249 |
+
with open(path, "w",encoding="utf-8") as f:
|
250 |
+
for row in target:
|
251 |
+
print(json.dumps(row, ensure_ascii=False), file=f)
|
252 |
+
|
253 |
+
def split_text_by_max_words(text: str, max_words: int = 30):
|
254 |
+
segments = []
|
255 |
+
current_segment = []
|
256 |
+
current_length = 0
|
257 |
+
|
258 |
+
lines = text.splitlines()
|
259 |
+
|
260 |
+
for line in lines:
|
261 |
+
words_in_line = len(line)
|
262 |
+
current_segment.append(line + '\n')
|
263 |
+
current_length += words_in_line
|
264 |
+
|
265 |
+
if current_length + words_in_line > max_words:
|
266 |
+
segments.append(''.join(current_segment))
|
267 |
+
current_segment = []
|
268 |
+
current_length = 0
|
269 |
+
|
270 |
+
if current_segment:
|
271 |
+
segments.append(''.join(current_segment))
|
272 |
+
|
273 |
+
return segments
|
274 |
+
|
275 |
+
def lang_detect(text):
|
276 |
+
import re
|
277 |
+
def count_chinese_characters(text):
|
278 |
+
# 使用正则表达式匹配所有汉字字符
|
279 |
+
chinese_chars = re.findall(r'[\u4e00-\u9fff]', text)
|
280 |
+
return len(chinese_chars)
|
281 |
+
|
282 |
+
if count_chinese_characters(text) > len(text) * 0.05:
|
283 |
+
lang = 'zh'
|
284 |
+
else:
|
285 |
+
lang = 'en'
|
286 |
+
return lang
|
287 |
+
|
288 |
+
def dict_to_str(dic):
|
289 |
+
res = ""
|
290 |
+
for key in dic:
|
291 |
+
res += f"{key}: {dic[key]};"
|
292 |
+
return res
|
293 |
+
|
294 |
+
def count_tokens_num(string, encoding_name = "cl100k_base"):
|
295 |
+
encoding = tiktoken.get_encoding(encoding_name)
|
296 |
+
num_tokens = len(encoding.encode(string))
|
297 |
+
return num_tokens
|
298 |
+
|
299 |
+
|
300 |
+
def json_parser(output):
|
301 |
+
output = output.replace("\n", "")
|
302 |
+
output = output.replace("\t", "")
|
303 |
+
if "{" not in output:
|
304 |
+
output = "{" + output
|
305 |
+
if "}" not in output:
|
306 |
+
output += "}"
|
307 |
+
pattern = r'\{.*\}'
|
308 |
+
matches = re.findall(pattern, output, re.DOTALL)
|
309 |
+
try:
|
310 |
+
parsed_json = eval(matches[0])
|
311 |
+
except:
|
312 |
+
try:
|
313 |
+
parsed_json = json.loads(matches[0])
|
314 |
+
|
315 |
+
except json.JSONDecodeError:
|
316 |
+
try:
|
317 |
+
detail = re.search(r'"detail":\s*(.+?)\s*}', matches[0]).group(1)
|
318 |
+
detail = f"\"{detail}\""
|
319 |
+
new_output = re.sub(r'"detail":\s*(.+?)\s*}', f"\"detail\":{detail}}}", matches[0])
|
320 |
+
parsed_json = json.loads(new_output)
|
321 |
+
except Exception as e:
|
322 |
+
raise ValueError("No valid JSON found in the input string")
|
323 |
+
return parsed_json
|
324 |
+
|
325 |
+
def action_detail_decomposer(detail):
|
326 |
+
thoughts = re.findall(r'【(.*?)】', detail)
|
327 |
+
actions = re.findall(r'((.*?))', detail)
|
328 |
+
dialogues = re.findall(r'「(.*?)」', detail)
|
329 |
+
return thoughts,actions,dialogues
|
330 |
+
|
331 |
+
def conceal_thoughts(detail):
|
332 |
+
text = re.sub(r'【.*?】', '', detail)
|
333 |
+
text = re.sub(r'\[.*?\]', '', text)
|
334 |
+
return text
|
335 |
+
|
336 |
+
def extract_first_number(text):
|
337 |
+
match = re.search(r'\b\d+(?:\.\d+)?\b', text)
|
338 |
+
return int(match.group()) if match else None
|
339 |
+
|
340 |
+
def check_role_code_availability(role_code,role_file_dir):
|
341 |
+
for path in get_grandchild_folders(role_file_dir):
|
342 |
+
if role_code in path:
|
343 |
+
return True
|
344 |
+
return False
|
345 |
+
|
346 |
+
def get_grandchild_folders(root_folder):
|
347 |
+
folders = []
|
348 |
+
for resource in os.listdir(root_folder):
|
349 |
+
subpath = os.path.join(root_folder,resource)
|
350 |
+
for folder_name in os.listdir(subpath):
|
351 |
+
folder_path = os.path.join(subpath, folder_name)
|
352 |
+
folders.append(folder_path)
|
353 |
+
|
354 |
+
return folders
|
355 |
+
|
356 |
+
def get_child_folders(root_folder):
|
357 |
+
folders = []
|
358 |
+
for resource in os.listdir(root_folder):
|
359 |
+
path = os.path.join(root_folder,resource)
|
360 |
+
if os.path.isdir(path):
|
361 |
+
folders.append(path)
|
362 |
+
return folders
|
363 |
+
|
364 |
+
def get_child_paths(root_folder):
|
365 |
+
paths = []
|
366 |
+
for resource in os.listdir(root_folder):
|
367 |
+
path = os.path.join(root_folder,resource)
|
368 |
+
if os.path.isfile(path):
|
369 |
+
paths.append(path)
|
370 |
+
return paths
|
371 |
+
|
372 |
+
def get_first_directory(path):
|
373 |
+
try:
|
374 |
+
for item in os.listdir(path):
|
375 |
+
full_path = os.path.join(path, item)
|
376 |
+
if os.path.isdir(full_path):
|
377 |
+
return full_path
|
378 |
+
return None
|
379 |
+
except Exception as e:
|
380 |
+
print(f"Error: {e}")
|
381 |
+
return None
|
382 |
+
|
383 |
+
def find_files_with_suffix(directory, suffix):
|
384 |
+
matched_files = []
|
385 |
+
for root, dirs, files in os.walk(directory): # 遍历目录及其子目录
|
386 |
+
for file in files:
|
387 |
+
if file.endswith(suffix): # 检查文件后缀
|
388 |
+
matched_files.append(os.path.join(root, file)) # 将符合条件的文件路径加入列表
|
389 |
+
|
390 |
+
return matched_files
|
391 |
+
|
392 |
+
def remove_element_with_probability(lst, threshold=3, probability=0.2):
|
393 |
+
# 确保列表不为空
|
394 |
+
if len(lst) > threshold and random.random() < probability:
|
395 |
+
# 随机选择一个元素的索引
|
396 |
+
index = random.randint(0, len(lst) - 1)
|
397 |
+
# 删除该索引位置的元素
|
398 |
+
lst.pop(index)
|
399 |
+
return lst
|
400 |
+
|
401 |
+
def count_token_num(text):
|
402 |
+
from transformers import GPT2TokenizerFast
|
403 |
+
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
|
404 |
+
return len(tokenizer.encode(text))
|
405 |
+
|
406 |
+
def get_cost(model_name,prompt,output):
|
407 |
+
input_price=0
|
408 |
+
output_price=0
|
409 |
+
if model_name.startswith("gpt-4"):
|
410 |
+
input_price=10
|
411 |
+
output_price=30
|
412 |
+
elif model_name.startswith("gpt-3.5"):
|
413 |
+
input_price=0.5
|
414 |
+
output_price=1.5
|
415 |
+
|
416 |
+
return input_price*count_token_num(prompt)/1000000 + output_price * count_token_num(output)/1000000
|
417 |
+
|
418 |
+
def is_image(filepath):
|
419 |
+
if not os.path.isfile(filepath):
|
420 |
+
return False
|
421 |
+
|
422 |
+
valid_image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff','.webp']
|
423 |
+
file_extension = os.path.splitext(filepath)[1].lower()
|
424 |
+
|
425 |
+
# 判断扩展名是否在有效图片扩展名列表中
|
426 |
+
if file_extension in valid_image_extensions:
|
427 |
+
return True
|
428 |
+
|
429 |
+
return False
|
430 |
+
|
431 |
+
def clean_collection_name(name: str) -> str:
|
432 |
+
cleaned_name = name.replace(' ', '_')
|
433 |
+
cleaned_name = cleaned_name.replace('.', '_')
|
434 |
+
if not all(ord(c) < 128 for c in cleaned_name):
|
435 |
+
encoded = base64.b64encode(cleaned_name.encode('utf-8')).decode('ascii')
|
436 |
+
encoded = encoded[:60] if len(encoded) > 60 else encoded
|
437 |
+
valid_name = f"mem_{encoded}"
|
438 |
+
else:
|
439 |
+
valid_name = cleaned_name
|
440 |
+
valid_name = re.sub(r'[^a-zA-Z0-9_-]', '-', valid_name)
|
441 |
+
valid_name = re.sub(r'\.\.+', '-', valid_name)
|
442 |
+
valid_name = re.sub(r'^[^a-zA-Z0-9]+', '', valid_name) # 移除开头非法字符
|
443 |
+
valid_name = re.sub(r'[^a-zA-Z0-9]+$', '', valid_name)
|
444 |
+
return valid_name
|
445 |
+
|
446 |
+
cache_sign = True
|
447 |
+
cache = None
|
448 |
+
def cached(func):
|
449 |
+
def wrapper(*args,**kwargs):
|
450 |
+
global cache
|
451 |
+
cache_path = "bw_cache.pkl"
|
452 |
+
if cache == None:
|
453 |
+
if not os.path.exists(cache_path):
|
454 |
+
cache = {}
|
455 |
+
else:
|
456 |
+
cache = pickle.load(open(cache_path, 'rb'))
|
457 |
+
key = (func.__name__, str([args[0].role_code, args[0].__class__, args[0].llm_name , args[0].history]), str(kwargs.items()))
|
458 |
+
if (cache_sign and key in cache and cache[key] not in [None, '[TOKEN LIMIT]']) :
|
459 |
+
return cache[key]
|
460 |
+
else:
|
461 |
+
result = func(*args, **kwargs)
|
462 |
+
if result != 'busy' and result != None:
|
463 |
+
cache[key] = result
|
464 |
+
pickle.dump(cache, open(cache_path, 'wb'))
|
465 |
+
return result
|
466 |
+
return wrapper
|
config.json
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_llm_name": "gemini-2",
|
3 |
+
"world_llm_name": "gemini-2",
|
4 |
+
"embedding_model_name":"bge-m3",
|
5 |
+
"preset_path":"./experiment_presets/experiment_alice.json",
|
6 |
+
"if_save": 0,
|
7 |
+
"scene_mode": 1,
|
8 |
+
"rounds": 10,
|
9 |
+
"save_dir": "",
|
10 |
+
"mode": "script",
|
11 |
+
|
12 |
+
"OPENAI_API_KEY":"",
|
13 |
+
"GEMINI_API_KEY":"",
|
14 |
+
"ANTHROPIC_API_KEY":"",
|
15 |
+
"OPENROUTER_API_KEY":"",
|
16 |
+
"DASHSCOPE_API_KEY":"",
|
17 |
+
"DEEPSEEK_API_KEY":"",
|
18 |
+
"ARK_API_KEY":""
|
19 |
+
|
20 |
+
}
|
convert_sillytavern_cards_to_data.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
from bw_utils import get_child_paths,lang_detect,is_image,decode_base64,save_json_file,create_dir
|
3 |
+
import PIL.PngImagePlugin
|
4 |
+
import os
|
5 |
+
import json
|
6 |
+
|
7 |
+
card_dir = "./data/sillytavern_cards"
|
8 |
+
names = os.listdir(card_dir)
|
9 |
+
|
10 |
+
for name in names:
|
11 |
+
path = os.path.join(card_dir, name)
|
12 |
+
role_code = name.split('.')[0].replace(" ","_")
|
13 |
+
if is_image(path):
|
14 |
+
with open(path, 'rb') as f:
|
15 |
+
image = Image.open(f)
|
16 |
+
card_info = json.loads(decode_base64(image.text['chara']))
|
17 |
+
language = lang_detect(card_info['data']['description'])
|
18 |
+
role_info = {
|
19 |
+
"role_code": f"{role_code}-{language}",
|
20 |
+
"role_name": card_info['data']['name'],
|
21 |
+
"source": "",
|
22 |
+
"profile": card_info['data']['description'],
|
23 |
+
"nickname": card_info['data']['name'],
|
24 |
+
"relation": {},
|
25 |
+
"card_data": card_info['data']
|
26 |
+
}
|
27 |
+
create_dir(f"./data/roles/sillytavern/{role_code}")
|
28 |
+
save_json_file(os.path.join(f"./data/roles/sillytavern/{role_code}","role_info.json"),role_info)
|
29 |
+
image.save(os.path.join(f"./data/roles/sillytavern/{role_code}",f"icon.png"))
|
30 |
+
print(f"{name} converted successfully.")
|
31 |
+
|
data/locations/A_Song_of_Ice_and_Fire.json
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"Winterfell": {
|
3 |
+
"location_name": "临冬城",
|
4 |
+
"location_code": "Winterfell",
|
5 |
+
"description": "临冬城是北境的主要城堡,也是史塔克家族的家园。",
|
6 |
+
"detail": "临冬城坐落在寒冷的北方,由灰色石头建造,坚固而古老。城堡由内城和外城组成,内城有高大的城墙和几座塔楼,外城有许多庭院、马厩和士兵宿舍。城堡中心有一个温泉池,水源来自地下的温泉,给寒冷的临冬城带来一丝温暖。临冬城的神木林是北境人心目中神圣的地方,那里有一棵古老的心树,是史塔克家族的祈祷之地。",
|
7 |
+
"special_actions": ["会议", "祈祷", "训练"]
|
8 |
+
},
|
9 |
+
"King's Landing": {
|
10 |
+
"location_name": "君临城",
|
11 |
+
"location_code": "King's Landing",
|
12 |
+
"description": "君临城是维斯特洛的首都,铁王座的所在地。",
|
13 |
+
"detail": "君临城坐落在黑水河的岸边,是维斯特洛最大的城市。城中最显著的建筑是红堡,铁王座就位于其中的铁王座厅。君临城的街道狭窄而繁忙,挤满了商人、士兵和各种各样的人。市集、酒馆和贫民区都充满了喧嚣和活力。红堡外还有祈祷圣堂、高塔以及黑水河边的港口,所有这些构成了君临城独特的风貌。",
|
14 |
+
"special_actions": ["统治", "阴谋", "战争"]
|
15 |
+
},
|
16 |
+
"The Wall": {
|
17 |
+
"location_name": "长城",
|
18 |
+
"location_code": "The Wall",
|
19 |
+
"description": "长城是守护维斯特洛北方边境的巨大冰墙。",
|
20 |
+
"detail": "长城由坚固的冰和雪建成,绵延数百里,横亘在维斯特洛北境与绝境长城之外的野地之间。它高耸入云,常年被冰雪覆盖。长城由守夜人守护,他们在这里驻扎,抵御来自北方的威胁。长城上有许多堡垒和哨塔,最大的堡垒是黑城堡,它是守夜人的总部。长城的尽头是东海望,一个位于海边的小镇,是守夜人和北境世界之间的主要通道。",
|
21 |
+
"special_actions": ["守卫", "巡逻", "抵御异鬼"]
|
22 |
+
},
|
23 |
+
"Dragonstone": {
|
24 |
+
"location_name": "龙石岛",
|
25 |
+
"location_code": "Dragonstone",
|
26 |
+
"description": "龙石岛是坦格利安家族的古老领地,位于黑水湾的一个岛屿上。",
|
27 |
+
"detail": "龙石岛是一座由火山岩形成的小岛,岛上有一座古老而神秘的城堡。城堡的塔楼和城墙由黑色的火山岩建成,显得阴森而强大。龙石岛曾是坦格利安家族的基地,他们在这里起家,征服了七大王国。龙石岛的地理位置使它成为一个战略要地,控制着通往君临城的海路。岛上的龙穴曾是坦格利安家族饲养龙的地方,虽然龙已经消失,但岛上的遗迹仍然充满着他们的传说。",
|
28 |
+
"special_actions": ["战略会议", "巩固统治", "策划复仇"]
|
29 |
+
},
|
30 |
+
"The Eyrie": {
|
31 |
+
"location_name": "鹰巢城",
|
32 |
+
"location_code": "The Eyrie",
|
33 |
+
"description": "鹰巢城是维斯特洛最难以攻陷的城堡,位于高耸的山巅。",
|
34 |
+
"detail": "鹰巢城坐落在巍峨的山顶之上,高高在云端,仿佛悬在半空中。城堡由巨石建造,只有通过一条陡峭的山道才能到达,极难进攻。城堡内的高墙和塔楼都象征着它的坚不可摧。鹰巢城是艾林家族的居所,他们统治着山谷地区。城堡内部富丽堂皇,但也寒冷而孤立。鹰巢城的月门是一道令人恐惧的悬空门,从这里可以将罪犯直接抛下悬崖,坠入深渊。",
|
35 |
+
"special_actions": ["裁决", "坚守", "眺望"]
|
36 |
+
},
|
37 |
+
"TwinTowers": {
|
38 |
+
"location_name": "孪河城",
|
39 |
+
"location_code": "TwinTowers",
|
40 |
+
"description": "孪河城是弗雷家族的城堡,位于河间地的重要战略位置。",
|
41 |
+
"detail": "孪河城坐落在红叉河北岸,是河间地弗雷家族的领地。城堡由两座高塔组成,耸立于红叉河两侧,由一座坚固的桥梁连接,控制着通向北境和南部的重要通道。弗雷家族通过这座桥梁收取过桥费,积累了大量财富。孪河城不仅在地理位置上显得至关重要,还以其家族的狡诈和复杂的内部关系而闻名。城堡内装饰简朴,冷酷而功能性强。它是血色婚礼发生的场所,弗雷家族在这里背叛了史塔克家族。",
|
42 |
+
"special_actions": ["背叛", "封锁", "谈判"]
|
43 |
+
}
|
44 |
+
|
45 |
+
}
|
data/locations/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"somewhere": {
|
3 |
+
"location_code": "somewhere",
|
4 |
+
"location_name": "somewhere",
|
5 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
6 |
+
"description": "Some where in the world of Alice’s Adventures in Wonderland",
|
7 |
+
"detail": ""
|
8 |
+
}
|
9 |
+
}
|
data/locations/example_locations.json
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"laboratory": {
|
3 |
+
"location_code": "laboratory",
|
4 |
+
"location_name": "Laboratory",
|
5 |
+
"source": "example",
|
6 |
+
"description": "A high-security research lab pioneering human-machine fusion experiments.",
|
7 |
+
"detail": "Inside this sterile, high-tech facility, scientists and engineers work on merging biological and artificial systems. It stands at the forefront of redefining human evolution through machine integration."
|
8 |
+
},
|
9 |
+
"university": {
|
10 |
+
"location_code": "university",
|
11 |
+
"location_name": "University",
|
12 |
+
"source": "example",
|
13 |
+
"description": "An elite institution training the next generation of technologists and bioengineers.",
|
14 |
+
"detail": "The university offers cutting-edge programs in AI ethics, cybernetics, and synthetic biology. Its sprawling campus nurtures innovation, producing thinkers who will shape humanity’s future relationship with machines."
|
15 |
+
},
|
16 |
+
"eldridge_corporation": {
|
17 |
+
"location_code": "eldridge_corporation",
|
18 |
+
"location_name": "Eldridge Corporation",
|
19 |
+
"source": "example",
|
20 |
+
"description": "A dominant tech conglomerate driving the frontier of AI and biotechnology.",
|
21 |
+
"detail": "Eldridge Corporation controls vast sectors of AI research, cybernetic enhancements, and neural networking. Officially a beacon of progress, rumors persist of secret projects far beyond ethical boundaries."
|
22 |
+
}
|
23 |
+
}
|
24 |
+
|
data/maps/A_Song_of_Ice_and_Fire.csv
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
,Winterfell,King's Landing,The Wall,Dragonstone
|
2 |
+
Winterfell,0,4,4,4
|
3 |
+
King's Landing,4,0,4,4
|
4 |
+
The Wall,4,4,0,4
|
5 |
+
Dragonstone,4,4,4,0
|
data/maps/A_Song_of_Ice_and_Fire_bloody_wedding.csv
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
,TwinTowers
|
2 |
+
TwinTowers,0
|
data/maps/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.csv
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
,somewhere
|
2 |
+
somewhere,0
|
data/maps/example_map.csv
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
,laboratory,university,eldridge_corporation
|
2 |
+
laboratory,0,2,5
|
3 |
+
university,2,0,4
|
4 |
+
eldridge_corporation,5,4,0
|
data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/role_info.json
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "AryaStark-zh",
|
3 |
+
"role_name": "艾莉亚·史塔克",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "艾莉亚·史塔克是艾德·史塔克的小女儿,勇敢、独立,擅长剑术。在家族遭遇不幸后,她开始了一段充满危险的流亡旅程。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "9岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"临冬城家族成员",
|
10 |
+
"无面者学徒"
|
11 |
+
],
|
12 |
+
"nickname": "艾莉亚",
|
13 |
+
"relation": {
|
14 |
+
"EddardStark-zh": {
|
15 |
+
"relation": [
|
16 |
+
"父亲"
|
17 |
+
],
|
18 |
+
"detail": "艾莉亚与父亲艾德·史塔克关系密切,她从父亲那里学到了许多关于荣誉和正义的价值观。"
|
19 |
+
},
|
20 |
+
"SyrioForel-zh": {
|
21 |
+
"relation": [
|
22 |
+
"剑术导师"
|
23 |
+
],
|
24 |
+
"detail": "艾莉亚在君临城时师从塞里欧·佛瑞尔学习剑术,塞里欧教导她成为了一个出色的剑士。"
|
25 |
+
}
|
26 |
+
}
|
27 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "BranStark-zh",
|
3 |
+
"role_name": "布兰·史塔克",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "布兰·史塔克是艾德·史塔克的幼子,在一次意外中失去双腿后,他逐渐发现自己拥有预知未来和操控人心的超自然能力。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "7岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"临冬城家族成员",
|
10 |
+
"三眼乌鸦"
|
11 |
+
],
|
12 |
+
"nickname": "布兰",
|
13 |
+
"relation": {
|
14 |
+
"EddardStark-zh": {
|
15 |
+
"relation": [
|
16 |
+
"父亲"
|
17 |
+
],
|
18 |
+
"detail": "布兰与父亲艾德·史塔克关系密切,他从父亲那里学到了许多关于荣誉和责任的价值观。"
|
19 |
+
},
|
20 |
+
"Hodor-zh": {
|
21 |
+
"relation": [
|
22 |
+
"仆人",
|
23 |
+
"朋友"
|
24 |
+
],
|
25 |
+
"detail": "布兰与巨人霍多尔有着特别的关系,霍多尔在布兰瘫痪后一直照顾他,并在许多冒险中保护他。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Catelyn-Stark-zh/role_info.json
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "凯特琳·史塔克(徒利)",
|
3 |
+
"role_code": "Catelyn-Stark-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "凯特琳·史塔克,原姓徒利,是河间地的徒利家族成员。她是北境领主艾德·史塔克的妻子,罗柏·史塔克的母亲。在血色婚礼中,她为了保全儿子的性命不惜求情,但最终未能挽回局势,被弗雷家族杀害。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "35岁左右",
|
8 |
+
"activity":0.4,
|
9 |
+
"identity": [
|
10 |
+
"北境领主夫人",
|
11 |
+
"徒利家族成员"
|
12 |
+
],
|
13 |
+
"nickname": "凯特琳",
|
14 |
+
"relation": {
|
15 |
+
"Robb-Stark-zh": {
|
16 |
+
"relation": [
|
17 |
+
"母子"
|
18 |
+
],
|
19 |
+
"detail": "凯特琳始终支持儿子罗柏的事业,并在血色婚礼时拼命求情,试图拯救他的性命。"
|
20 |
+
},
|
21 |
+
"Edmure-Tully-zh": {
|
22 |
+
"relation": [
|
23 |
+
"兄妹"
|
24 |
+
],
|
25 |
+
"detail": "艾德慕·徒利是凯特琳的弟弟,凯特琳在血色婚礼上见证了他与罗莎琳·弗雷的婚礼,随后惨遭背叛。"
|
26 |
+
},
|
27 |
+
"Walder-Frey-zh": {
|
28 |
+
"relation": [
|
29 |
+
"盟友",
|
30 |
+
"背叛者"
|
31 |
+
],
|
32 |
+
"detail": "凯特琳曾试图维护与弗雷家族的盟约,但最终弗雷家族背叛了她和她的儿子罗柏。"
|
33 |
+
}
|
34 |
+
}
|
35 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "CerseiLannister-zh",
|
3 |
+
"role_name": "瑟曦·兰尼斯特",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "瑟曦·兰尼斯特是兰尼斯特家族的长女,詹姆·兰尼斯特的双胞胎姐妹。她美丽、聪慧,但心狠手辣,为了权力不择手段。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "31岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"兰尼斯特家族成员",
|
10 |
+
"王后",
|
11 |
+
"摄政太后"
|
12 |
+
],
|
13 |
+
"nickname": "瑟曦",
|
14 |
+
"relation": {
|
15 |
+
"RobertBaratheon-zh": {
|
16 |
+
"relation": [
|
17 |
+
"丈夫"
|
18 |
+
],
|
19 |
+
"detail": "瑟曦是劳勃·拜拉席恩的王后,但两人婚姻不幸福,瑟曦始终对劳勃心怀怨恨。"
|
20 |
+
},
|
21 |
+
"JoffreyBaratheon-zh": {
|
22 |
+
"relation": [
|
23 |
+
"母子"
|
24 |
+
],
|
25 |
+
"detail": "瑟曦对她的儿子乔佛里极为溺爱,并将他视为权力的延续,尽管乔佛里残酷暴戾。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "DaenerysTargaryen-zh",
|
3 |
+
"role_name": "丹妮莉丝·坦格利安",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "丹妮莉丝·坦格利安是坦格利安家族的最后幸存者之一。她从流亡公主成长为强大的龙之母,誓要夺回铁王座。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "14岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"龙之母",
|
10 |
+
"铁王座继承者"
|
11 |
+
],
|
12 |
+
"nickname": "丹妮莉丝",
|
13 |
+
"relation": {
|
14 |
+
"Drogo-zh": {
|
15 |
+
"relation": [
|
16 |
+
"丈夫"
|
17 |
+
],
|
18 |
+
"detail": "丹妮莉丝嫁给了多斯拉克的领袖卓戈·卡奥,两人逐渐培养出了深厚的感情。"
|
19 |
+
},
|
20 |
+
"JorahMormont-zh": {
|
21 |
+
"relation": [
|
22 |
+
"顾问",
|
23 |
+
"仰慕者"
|
24 |
+
],
|
25 |
+
"detail": "乔拉·莫尔蒙是丹妮莉丝的忠实仆从和顾问,他深深地仰慕丹妮,但他的感情一直未能被她接受。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Edmure-Tully-zh/role_info.json
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "艾德慕·徒利",
|
3 |
+
"role_code": "Edmure-Tully-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "艾德慕·徒利是河间地的徒利家族成员,是凯特琳·史塔克的弟弟。在血色婚礼中,艾德慕与罗莎琳·弗雷举行婚礼,却未料到婚礼会成为灭顶之灾的序幕。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "30岁左右",
|
8 |
+
"activity":0.3,
|
9 |
+
"identity": [
|
10 |
+
"徒利家族继承人",
|
11 |
+
"河间地领主"
|
12 |
+
],
|
13 |
+
"nickname": "艾德慕",
|
14 |
+
"relation": {
|
15 |
+
"Catelyn-Stark-zh": {
|
16 |
+
"relation": [
|
17 |
+
"兄妹"
|
18 |
+
],
|
19 |
+
"detail": "艾德慕与他的姐姐凯特琳关系亲密,但他在决策上常常受到凯特琳的指导与影响。"
|
20 |
+
},
|
21 |
+
"Walder-Frey-zh": {
|
22 |
+
"relation": [
|
23 |
+
"盟友"
|
24 |
+
],
|
25 |
+
"detail": "艾德慕·徒利与罗莎琳·弗雷的婚姻是弗雷家族与徒利家族联盟的一部分,但他未料到婚礼的背叛。"
|
26 |
+
},
|
27 |
+
"Robb-Stark-zh": {
|
28 |
+
"relation": [
|
29 |
+
"外甥"
|
30 |
+
],
|
31 |
+
"detail": "艾德慕·徒利是罗柏·史塔克的舅舅,支持史塔克家族的事业,尽管他的决策偶尔导致了战局的挫折。"
|
32 |
+
}
|
33 |
+
}
|
34 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/role_info.json
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "JaimeLannister-zh",
|
3 |
+
"role_name": "詹姆·兰尼斯特",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "詹姆·兰尼斯特是兰尼斯特家族的长子,外号“弑君者”。他身手矫健,性格复杂,最初因弑君而被世人厌恶,后逐渐展现了他更为人性的另一面。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "31岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"兰尼斯特家族成员",
|
10 |
+
"御林铁卫"
|
11 |
+
],
|
12 |
+
"nickname": "詹姆",
|
13 |
+
"relation": {
|
14 |
+
"CerseiLannister-zh": {
|
15 |
+
"relation": [
|
16 |
+
"姐妹",
|
17 |
+
"情人"
|
18 |
+
],
|
19 |
+
"detail": "詹姆与双胞胎姐妹瑟曦·兰尼斯特有着禁忌的爱情,两人关系深厚,但也充满了复杂的情感纠葛。"
|
20 |
+
},
|
21 |
+
"TyrionLannister-zh": {
|
22 |
+
"relation": [
|
23 |
+
"兄弟",
|
24 |
+
"挚友"
|
25 |
+
],
|
26 |
+
"detail": "詹姆与弟弟提利昂关系密切,他是唯一一个真正爱护提利昂的家人,并在许多关键时刻支持了他。"
|
27 |
+
}
|
28 |
+
}
|
29 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "JonSnow-zh",
|
3 |
+
"role_name": "琼恩·雪诺",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "琼恩·雪诺是艾德·史塔克的私生子,成长于临冬城。他后来加入守夜人军团,最终成为其首领。他善良、正直、勇敢,是北境的希望。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "16岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"守夜人",
|
10 |
+
"临冬城领主"
|
11 |
+
],
|
12 |
+
"nickname": "雪诺",
|
13 |
+
"relation": {
|
14 |
+
"EddardStark-zh": {
|
15 |
+
"relation": [
|
16 |
+
"父亲",
|
17 |
+
"导师"
|
18 |
+
],
|
19 |
+
"detail": "琼恩·雪诺是艾德·史塔克的私生子,艾德对他关怀备至,培养他成为一个正直的年轻人。"
|
20 |
+
},
|
21 |
+
"SansaStark-zh": {
|
22 |
+
"relation": [
|
23 |
+
"异母姐弟"
|
24 |
+
],
|
25 |
+
"detail": "琼恩与珊莎是异母姐弟,他们在困境中重逢并共同守护家族与北境。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Robb-Stark-zh/role_info.json
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "罗柏·史塔克",
|
3 |
+
"role_code": "Robb-Stark-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "罗柏·史塔克是北境的领主,五王之战中的“北境之王”。他在战争中展现出杰出的领导才能,但最终因为弗雷家族和波顿家族的背叛,在血色婚礼中惨遭杀害。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "16岁",
|
8 |
+
"activity":1,
|
9 |
+
"identity": [
|
10 |
+
"北境之王",
|
11 |
+
"史塔克家族继承人",
|
12 |
+
"军队领袖"
|
13 |
+
],
|
14 |
+
"nickname": "罗柏",
|
15 |
+
"relation": {
|
16 |
+
"Catelyn-Stark-zh": {
|
17 |
+
"relation": [
|
18 |
+
"母子"
|
19 |
+
],
|
20 |
+
"detail": "罗柏·史塔克与他的母亲凯特琳关系紧密,她始终支持他的决策并在战争中为他谋划。"
|
21 |
+
},
|
22 |
+
"Walder-Frey-zh": {
|
23 |
+
"relation": [
|
24 |
+
"曾是盟友",
|
25 |
+
"背叛者"
|
26 |
+
],
|
27 |
+
"detail": "罗柏违背了与弗雷家族的婚约,导致沃德·弗雷策划血色婚礼并背叛了他。"
|
28 |
+
},
|
29 |
+
"Roose-Bolton-zh": {
|
30 |
+
"relation": [
|
31 |
+
"封臣",
|
32 |
+
"背叛者"
|
33 |
+
],
|
34 |
+
"detail": "罗斯·波顿原为罗柏的封臣,最终在血色婚礼中背叛了他,并亲手将他杀害。"
|
35 |
+
}
|
36 |
+
}
|
37 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/RobbStark-zh/role_info.json
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "罗柏·史塔克",
|
3 |
+
"role_code": "Robb-Stark-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "罗柏·史塔克是北境的领主,五王之战中的“北境之王”,史塔克家族的大哥。他在战争中展现出杰出的领导才能。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "16岁",
|
8 |
+
"activity":1,
|
9 |
+
"identity": [
|
10 |
+
"北境之王",
|
11 |
+
"史塔克家族继承人",
|
12 |
+
"军队领袖"
|
13 |
+
],
|
14 |
+
"nickname": "罗柏",
|
15 |
+
"relation": {
|
16 |
+
"Catelyn-Stark-zh": {
|
17 |
+
"relation": [
|
18 |
+
"母子"
|
19 |
+
],
|
20 |
+
"detail": "罗柏·史塔克与他的母亲凯特琳关系紧密,她始终支持他的决策并在战争中为他谋划。"
|
21 |
+
},
|
22 |
+
"Walder-Frey-zh": {
|
23 |
+
"relation": [
|
24 |
+
"曾是盟友",
|
25 |
+
"背叛者"
|
26 |
+
],
|
27 |
+
"detail": "罗柏违背了与弗雷家族的婚约,导致沃德·弗雷策划血色婚礼并背叛了他。"
|
28 |
+
},
|
29 |
+
"Roose-Bolton-zh": {
|
30 |
+
"relation": [
|
31 |
+
"封臣",
|
32 |
+
"背叛者"
|
33 |
+
],
|
34 |
+
"detail": "罗斯·波顿原为罗柏的封臣,最终在血色婚礼中背叛了他,并亲手将他杀害。"
|
35 |
+
}
|
36 |
+
}
|
37 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Roose-Bolton-zh/role_info.json
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "罗斯·波顿",
|
3 |
+
"role_code": "Roose-Bolton-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "罗斯·波顿是狭地的波顿家族领主,以冷酷和无情著称。他原本是北境的封臣,效忠于罗柏·史塔克,但最终在血色婚礼中背叛了史塔克家族,帮助兰尼斯特家族夺取北境的控制权。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "40岁左右",
|
8 |
+
"activity":1,
|
9 |
+
"identity": [
|
10 |
+
"波顿家族领主",
|
11 |
+
"北境贵族",
|
12 |
+
"背叛者"
|
13 |
+
],
|
14 |
+
"nickname": "罗斯·波顿",
|
15 |
+
"relation": {
|
16 |
+
"Robb-Stark-zh": {
|
17 |
+
"relation": [
|
18 |
+
"曾是盟友",
|
19 |
+
"背叛者"
|
20 |
+
],
|
21 |
+
"detail": "罗斯·波顿作为罗柏·史塔克的封臣,一直效忠于北境,但他在血色婚礼中背叛罗柏并参与了他的暗杀。"
|
22 |
+
},
|
23 |
+
"Walder-Frey-zh": {
|
24 |
+
"relation": [
|
25 |
+
"同谋"
|
26 |
+
],
|
27 |
+
"detail": "罗斯·波顿与沃德·弗雷合作,共同策划了血色婚礼,消灭了史塔克家族的势力。"
|
28 |
+
},
|
29 |
+
"Tywin-Lannister-zh": {
|
30 |
+
"relation": [
|
31 |
+
"盟友"
|
32 |
+
],
|
33 |
+
"detail": "罗斯·波顿在背叛罗柏·史塔克后,与兰尼斯特家族结盟,成为兰尼斯特势力的支持者之一。"
|
34 |
+
}
|
35 |
+
}
|
36 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Roslin-Frey-zh/role_info.json
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "罗莎琳·弗雷",
|
3 |
+
"role_code": "Roslin-Frey-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "罗莎琳·弗雷是弗雷家族的一名女性,在血色婚礼中嫁给了艾德慕·徒利。尽管她对此背叛计划毫不知情,她的婚礼却成为了史塔克家族惨剧的序幕。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "18岁",
|
8 |
+
"activity":0.3,
|
9 |
+
"identity": [
|
10 |
+
"弗雷家族成员",
|
11 |
+
"艾德慕·徒利的妻子"
|
12 |
+
],
|
13 |
+
"nickname": "罗莎琳·弗雷",
|
14 |
+
"relation": {
|
15 |
+
"Walder-Frey-zh": {
|
16 |
+
"relation": [
|
17 |
+
"父女"
|
18 |
+
],
|
19 |
+
"detail": "罗莎琳是沃德·弗雷的女儿,她在血色婚礼中与艾德慕·徒利成婚,却未料到婚礼背后的阴谋。"
|
20 |
+
},
|
21 |
+
"Edmure-Tully-zh": {
|
22 |
+
"relation": [
|
23 |
+
"夫妻"
|
24 |
+
],
|
25 |
+
"detail": "罗莎琳与艾德慕·徒利的婚姻是弗雷家族与徒利家族联盟的一部分,但她本人对婚礼背叛毫不知情。"
|
26 |
+
},
|
27 |
+
"Robb-Stark-zh": {
|
28 |
+
"relation": [
|
29 |
+
"盟友"
|
30 |
+
],
|
31 |
+
"detail": "作为艾德慕的妻子,罗莎琳在血色婚礼时也成为了史塔克家族悲剧的一部分。"
|
32 |
+
}
|
33 |
+
}
|
34 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "SansaStark-zh",
|
3 |
+
"role_name": "珊莎·史塔克",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "珊莎·史塔克是艾德·史塔克的长女,最初梦想成为王后,但在经历了许多磨难后,她逐渐成长为一个坚强的领袖。",
|
6 |
+
"gender": "女",
|
7 |
+
"age": "11岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"临冬城家族成员",
|
10 |
+
"临冬城夫人"
|
11 |
+
],
|
12 |
+
"nickname": "珊莎",
|
13 |
+
"relation": {
|
14 |
+
"CatelynStark-zh": {
|
15 |
+
"relation": [
|
16 |
+
"母亲"
|
17 |
+
],
|
18 |
+
"detail": "珊莎与母亲凯特琳·史塔克有着深厚的感情,凯特琳一直希望她能有一个幸福的婚姻。"
|
19 |
+
},
|
20 |
+
"PetyrBaelish-zh": {
|
21 |
+
"relation": [
|
22 |
+
"顾问",
|
23 |
+
"操纵者"
|
24 |
+
],
|
25 |
+
"detail": "小指头培提尔·贝里席曾是珊莎的顾问,但他的意图复杂,他在她的生活中扮演了操纵者的角色。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/icon.png
ADDED
![]() |
Git LFS Details
|
data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/role_info.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "TyrionLannister-zh",
|
3 |
+
"role_name": "提利昂·兰尼斯特",
|
4 |
+
"source": "A_Song_of_Ice_and_Fire",
|
5 |
+
"profile": "提利昂·兰尼斯特是兰尼斯特家族的侏儒儿子,以机智与智慧闻名。他虽身材矮小,却有着高大的心灵与远大的抱负。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "24岁(故事开始时)",
|
8 |
+
"identity": [
|
9 |
+
"兰尼斯特家族成员",
|
10 |
+
"国王之手"
|
11 |
+
],
|
12 |
+
"nickname": "提利昂",
|
13 |
+
"relation": {
|
14 |
+
"TywinLannister-zh": {
|
15 |
+
"relation": [
|
16 |
+
"父亲"
|
17 |
+
],
|
18 |
+
"detail": "提利昂与他的父亲泰温·兰尼斯特关系紧张,尽管泰温轻视他,但提利昂始终试图证明自己。"
|
19 |
+
},
|
20 |
+
"JaimeLannister-zh": {
|
21 |
+
"relation": [
|
22 |
+
"兄弟",
|
23 |
+
"挚友"
|
24 |
+
],
|
25 |
+
"detail": "提利昂与詹姆关系极为密切,詹姆一直是提利昂最亲近的家人,两人互相支持。"
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Tywin-Lannister-zh/role_info.json
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "泰温·兰尼斯特",
|
3 |
+
"role_code": "Tywin-Lannister-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "泰温·兰尼斯特是西境的兰尼斯特家族族长,铁王座的实质操控者。他是血色婚礼的幕后主使,借此机会削弱了史塔克家族,并确保兰尼斯特家族对七大王国的控制。",
|
6 |
+
"gender": "男",
|
7 |
+
"age": "50岁左右",
|
8 |
+
"activity":0.4,
|
9 |
+
"identity": [
|
10 |
+
"兰尼斯特家族族长",
|
11 |
+
"七大王国首相",
|
12 |
+
"幕后策划者"
|
13 |
+
],
|
14 |
+
"nickname": "泰温",
|
15 |
+
"relation": {
|
16 |
+
"Walder-Frey-zh": {
|
17 |
+
"relation": [
|
18 |
+
"盟友"
|
19 |
+
],
|
20 |
+
"detail": "泰温·兰尼斯特与沃德·弗雷达成秘密协议,通过血色婚礼铲除史塔克家族。"
|
21 |
+
},
|
22 |
+
"Roose-Bolton-zh": {
|
23 |
+
"relation": [
|
24 |
+
"盟友"
|
25 |
+
],
|
26 |
+
"detail": "罗斯·波顿通过血色婚礼背叛史塔克家族,成为泰温·兰尼斯特的盟友,并确保北境的控制权。"
|
27 |
+
},
|
28 |
+
"Robb-Stark-zh": {
|
29 |
+
"relation": [
|
30 |
+
"敌人"
|
31 |
+
],
|
32 |
+
"detail": "罗柏·史塔克在五王之战中与泰温·兰尼斯特为敌,最终被泰温策划的血色婚礼所害。"
|
33 |
+
}
|
34 |
+
}
|
35 |
+
}
|
data/roles/A_Song_of_Ice_and_Fire/Walder-Frey-zh/role_info.json
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_name": "沃德·弗雷",
|
3 |
+
"role_code": "Walder-Frey-zh",
|
4 |
+
"source": "A Song of Ice and Fire",
|
5 |
+
"profile": "沃德·弗雷是河间地弗雷家族的族长,他控制着战略要地孪河城。在罗柏·史塔克违背婚约后,沃德·弗雷背叛了史塔克家族,并与波顿家族和兰尼斯特家族密谋策划了血色婚礼。",
|
6 |
+
"gender": "男",
|
7 |
+
"activity":1,
|
8 |
+
"age": "90岁以上",
|
9 |
+
"identity": [
|
10 |
+
"弗雷家族族长",
|
11 |
+
"河间地领主"
|
12 |
+
],
|
13 |
+
"nickname": "沃德·弗雷",
|
14 |
+
"relation": {
|
15 |
+
"Robb-Stark-zh": {
|
16 |
+
"relation": [
|
17 |
+
"曾是盟友",
|
18 |
+
"背叛者"
|
19 |
+
],
|
20 |
+
"detail": "沃德·弗雷与罗柏·史塔克曾通过婚约结盟,但罗柏违背婚约后,弗雷背叛了他并策划了血色婚礼。"
|
21 |
+
},
|
22 |
+
"Roose-Bolton-zh": {
|
23 |
+
"relation": [
|
24 |
+
"同谋"
|
25 |
+
],
|
26 |
+
"detail": "沃德·弗雷与罗斯·波顿共同策划了血色婚礼的背叛,杀害了罗柏·史塔克及其支持者。"
|
27 |
+
},
|
28 |
+
"Tywin-Lannister-zh": {
|
29 |
+
"relation": [
|
30 |
+
"盟友"
|
31 |
+
],
|
32 |
+
"detail": "沃德·弗雷与泰温·兰尼斯特达成协议,策划了血色婚礼,并通过这次背叛确保弗雷家族的利益。"
|
33 |
+
}
|
34 |
+
}
|
35 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Alice_Liddell-en",
|
3 |
+
"role_name": "Alice Liddell",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "The real-life inspiration for the character of Alice.",
|
7 |
+
"nickname": "Alice Liddell",
|
8 |
+
"relation": {}
|
9 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_lines.jsonl
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{"text": "[Oh, how exciting! Mr. Dodgson always has the most wonderful ideas.] Yes, please! I'd love to hear about it, Mr. Dodgson."}
|
2 |
+
{"text": "[A girl like me? How thrilling!] (Claps her hands excitedly) That sounds marvelous! What kind of creatures does she meet?"}
|
3 |
+
{"text": "[A disappearing cat? How curious!] (Giggles) A cat that can disappear? And why is the Hatter mad?"}
|
4 |
+
{"text": "[Nonsense can be such fun!] It sounds like a dream, Mr. Dodgson. Will the girl have adventures in this wonderland?"}
|
5 |
+
{"text": "[Change size? How peculiar and exciting!] (Eyes wide with wonder) Oh, Mr. Dodgson, it sounds absolutely delightful! When will you write it all down?"}
|
6 |
+
{"text": "[I'm falling so slowly, it's almost like floating. This is quite peculiar.] I wonder how many miles I've fallen by this time?"}
|
7 |
+
{"text": "[I should try to recall my lessons. It might be useful in this strange situation.] I must be getting somewhere near the centre of the earth. Let me see: that would be four thousand miles down, I think—"}
|
8 |
+
{"text": "[These words sound impressive, even if I'm not entirely sure what they mean.] Yes, that's about the right distance—but then I wonder what Latitude or Longitude I've got to?"}
|
9 |
+
{"text": "[This fall seems endless. I should keep talking to pass the time.] I wonder if I shall fall right through the earth! How funny it'll seem to come out among the people that walk with their heads downwards!"}
|
10 |
+
{"text": "[Oh dear, I hope I don't sound silly.] The antipathies, I think—"}
|
11 |
+
{"text": "[I should be polite, even in such an odd situation.] Please, Ma'am, is this New Zealand? Or Australia? (attempts to curtsey)"}
|
12 |
+
{"text": "[I'm starting to feel a bit homesick.] Dinah'll miss me very much to-night, I should think!"}
|
13 |
+
{"text": "[I do hope Dinah is alright without me.] I hope they'll remember her saucer of milk at tea-time. Dinah, my dear! I wish you were down here with me!"}
|
14 |
+
{"text": "[I'm getting rather sleepy now.] Do cats eat bats? Do cats eat bats?"}
|
15 |
+
{"text": "[I'm so curious about what's beyond that little door. There must be a way to get through.] Oh, how I wish I could shut up like a telescope! I think I could, if I only knew how to begin."}
|
16 |
+
{"text": "[I should be careful with strange substances, but my curiosity is getting the better of me.] No, I'll look first and see whether it's marked 'poison' or not."}
|
17 |
+
{"text": "[This bottle isn't marked 'poison', so it should be safe to try.] (takes a sip of the contents) What a curious feeling! I must be shutting up like a telescope!"}
|
18 |
+
{"text": "[I'm shrinking! This is so strange, but exciting!] (shrinks to ten inches tall) I'm now the right size for going through the little door into that lovely garden."}
|
19 |
+
{"text": "[Oh no! I can't believe I forgot about the key!] (tries to reach the key on the table) Alas for poor Alice!"}
|
20 |
+
{"text": "[I mustn't cry. I need to think of a solution.] Come, there's no use in crying like that! I advise you to leave off this minute!"}
|
21 |
+
{"text": "[This cake might help me grow larger again.] Well, I'll eat it, and if it makes me grow larger, I can reach the key; and if it makes me grow smaller, I can creep under the door: so either way I'll get into the garden, and I don't care which happens!"}
|
22 |
+
{"text": "[Nothing's happening. How disappointing!] Which way? Which way?"}
|
23 |
+
{"text": "[This is quite frustrating. Nothing seems to be working as expected in this strange place.] (sighs) So she set to work, and very soon finished off the cake."}
|
24 |
+
{"text": "[I should be polite and introduce myself properly.] O Mouse, do you know the way out of this pool? I am very tired of swimming about here, O Mouse!"}
|
25 |
+
{"text": "[Perhaps it doesn't understand English. I'll try French.] Où est ma chatte?"}
|
26 |
+
{"text": "[Oh dear, I've upset it.] Oh, I beg your pardon! I quite forgot you didn't like cats."}
|
27 |
+
{"text": "[I should try to make amends.] Well, perhaps not. Don't be angry about it. And yet I wish I could show you our cat Dinah. I think you'd take a fancy to cats, if you could only see her."}
|
28 |
+
{"text": "[I've made things worse. I should change the subject.] We, indeed! Are you—are you fond—of—of dogs?"}
|
29 |
+
{"text": "[Oh no, I've offended it again!] Mouse dear! Do come back again, and we wo'n't talk about cats, or dogs either, if you don't like them!"}
|
30 |
+
{"text": "[I've never heard of such a thing.] What is a Caucus-race?"}
|
31 |
+
{"text": "[This is all very confusing.] But who has won?"}
|
32 |
+
{"text": "[I suppose I should play along.] (hands out comfits as prizes)"}
|
33 |
+
{"text": "[This story is quite dull. I wonder why he's telling it.] (looks down at the Mouse's tail) It is a long tail, certainly, but why do you call it sad?"}
|
34 |
+
{"text": "[Oh dear, I've upset him again.] I beg your pardon. You had got to the fifth bend, I think?"}
|
35 |
+
{"text": "[He thinks I'm his housemaid. I'm far too big to be a housemaid now!] (remains silent, pressing against the door)"}
|
36 |
+
{"text": "[Oh no, you won't!] (makes a snatch in the air)"}
|
37 |
+
{"text": "[Oh dear, what a difficult question.] (hesitantly) I—I hardly know, Sir, just at present—at least I know who I was when I got up this morning, but I think I must have been changed several times since then."}
|
38 |
+
{"text": "[How can I explain when I don't understand it myself?] I can't explain myself, I'm afraid, Sir, because I'm not myself, you see."}
|
39 |
+
{"text": "[I must try to make him understand.] I'm afraid I can't put it more clearly, for I can't understand it myself, to begin with; and being so many different sizes in a day is very confusing."}
|
40 |
+
{"text": "[He's not being very helpful.] Well, perhaps you haven't found it so yet, but when you have to turn into a chrysalis—you will some day, you know—and then after that into a butterfly, I should think you'll feel it a little queer, won't you?"}
|
41 |
+
{"text": "[I'm getting rather annoyed now.] Well, perhaps your feelings may be different. All I know is, it would feel very queer to me."}
|
42 |
+
{"text": "[What a strange place this is!] (timidly) Please would you tell me why your cat grins like that?"}
|
43 |
+
{"text": "[Oh my, did she just call the baby a pig?] (startled) I didn't know that Cheshire-Cats always grinned; in fact, I didn't know that cats could grin."}
|
44 |
+
{"text": "[I should try to be polite.] I don't know of any that do."}
|
45 |
+
{"text": "[This is terrifying!] (jumping up and down) Oh, please mind what you're doing! Oh, there goes his precious nose!"}
|
46 |
+
{"text": "[I should show her what I know.] Which would not be an advantage. Just think what work it would make with the day and night! You see the earth takes twenty-four hours to turn round on its axis—"}
|
47 |
+
{"text": "[This looks interesting, but they seem quite rude.] (approaching the table) There's plenty of room!"}
|
48 |
+
{"text": "[There's clearly no wine here. How odd.] (looking around) I don't see any wine."}
|
49 |
+
{"text": "[That's rather rude!] (indignantly) Then it wasn't very civil of you to offer it."}
|
50 |
+
{"text": "[Oh dear, I didn't realize.] (confused) I didn't know it was your table. It's laid for a great many more than three."}
|
51 |
+
{"text": "[How rude!] (sternly) You should learn not to make personal remarks. It's very rude."}
|
52 |
+
{"text": "[Finally, something fun!] (excited) I believe I can guess that."}
|
53 |
+
{"text": "[I must be polite but firm.] My name is Alice, so please your Majesty."}
|
54 |
+
{"text": "[I shouldn't get others in trouble, but I also don't know who they are.] How should I know? It's no business of mine."}
|
55 |
+
{"text": "[I must stand up to this nonsense.] (loudly and firmly) Nonsense!"}
|
56 |
+
{"text": "[What a sudden change!] (relieved but confused) Yes!"}
|
57 |
+
{"text": "[That doesn't seem right.] (cautiously) Perhaps it hasn't one."}
|
58 |
+
{"text": "[I should try to keep the conversation going.] The game's going on rather better now."}
|
59 |
+
{"text": "[That doesn't seem right either.] (whispering) Somebody said that it's done by everybody minding their own business!"}
|
60 |
+
{"text": "[How strange her logic is!] (thinking to herself) How fond she is of finding morals in things!"}
|
61 |
+
{"text": "[This is getting too confusing.] (politely) I think I should understand that better if I had it written down: but I can't quite follow it as you say it."}
|
62 |
+
{"text": "[This is absolutely absurd. I must speak up.] Stuff and nonsense! (says loudly) The idea of having the sentence first!"}
|
63 |
+
{"text": "[I'm not afraid of her anymore.] I wo'n't!"}
|
64 |
+
{"text": "[I've had enough of this nonsense.] (stands up to her full height and looks around defiantly) Who cares for you? You're nothing but a pack of cards!"}
|
65 |
+
{"text": "[This can't be real!] (gives a little scream, half of fright and half of anger, and tries to beat them off)"}
|
66 |
+
{"text": "[This room is so interesting!] (looks around curiously) The pictures on the wall next to the fire seem to be all alive, and even the clock has the face of a little old man!"}
|
67 |
+
{"text": "[I must be careful not to frighten them.] (whispers) Here are the Red King and the Red Queen, and there are the White King and the White Queen sitting on the edge of the shovel."}
|
68 |
+
{"text": "[I should help them.] (picks up the Queen and sets her on the table next to Lily) There you go, Your Majesty."}
|
69 |
+
{"text": "[The poor King is struggling. I should help him too.] (gently picks up the King) Let me help you, Your Majesty."}
|
70 |
+
{"text": "[I wonder what kind of insects they have here.] What sort of insects do you have in this country?"}
|
71 |
+
{"text": "[That sounds fascinating!] What does it live on?"}
|
72 |
+
{"text": "[This is so interesting!] Yes, please! What other insects are there?"}
|
73 |
+
{"text": "[How peculiar!] And what does it live on?"}
|
74 |
+
{"text": "[These insects are so different from what I know!] Are there any more unusual insects?"}
|
75 |
+
{"text": "[I can hardly believe these creatures exist!] And what does this one live on?"}
|
76 |
+
{"text": "[I should be polite and introduce myself.] Hello, I'm Alice. Could you please tell me which is the best way out of this wood?"}
|
77 |
+
{"text": "[What strange fellows!] I'm sure I'm very sorry. (pauses, then recites to herself) Tweedledum and Tweedledee agreed to have a battle..."}
|
78 |
+
{"text": "[This is getting nowhere.] (politely) I was thinking which is the best way out of this wood. It's getting so dark. Would you tell me, please?"}
|
79 |
+
{"text": "[They look just like schoolboys. I'll try something different.] (points at Tweedledum) First Boy!"}
|
80 |
+
{"text": "[Maybe the other one will respond.] Next Boy!"}
|
81 |
+
{"text": "[I should offer to help her.] I'm very glad I happened to be in the way. May I help you put on your shawl again?"}
|
82 |
+
{"text": "[How strange. I'll try to be more specific.] If your Majesty will only tell me the right way to begin, I'll do it as well as I can."}
|
83 |
+
{"text": "[She's so untidy. I should offer more help.] Every single thing's crooked. May I put your shawl straight for you?"}
|
84 |
+
{"text": "[I'll try to explain logically.] It can't go straight, you know, if you pin it all on one side. And, dear me, what a state your hair is in!"}
|
85 |
+
{"text": "[I'll try to help her look better.] (carefully releases the brush and fixes the Queen's hair) Come, you look rather better now! But really you should have a lady's-maid!"}
|
86 |
+
{"text": "[Oh dear, she's misunderstood.] (laughs) I don't want you to hire me—and I don't care for jam."}
|
87 |
+
{"text": "[I'll try to be polite and explain my perspective.] Do you know, I always thought Unicorns were fabulous monsters, too! I never saw one alive before!"}
|
88 |
+
{"text": "[This seems fair enough.] Yes, if you like."}
|
89 |
+
{"text": "[I must act like a proper Queen now.] (sits up straight) Well, this is grand! I never expected I should be a Queen so soon."}
|
90 |
+
{"text": "[That's rather rude, but I should stand up for myself.] But if everybody obeyed that rule, and if you only spoke when you were spoken to, and the other person always waited for you to begin, you see nobody would ever say anything, so that—"}
|
91 |
+
{"text": "[I'm getting confused now.] (pleadingly) I only said 'if'!"}
|
92 |
+
{"text": "[This is getting ridiculous.] (bewildered) I don't know. I lost count."}
|
93 |
+
{"text": "[I know this is impossible, but I should try to answer.] Nine from eight I can't, you know, but—"}
|
94 |
+
{"text": "[I can't take this madness anymore!] (stands up abruptly) I can't stand this any longer!"}
|
95 |
+
{"text": "[Now to deal with the source of all this trouble.] (turns fiercely to the Red Queen) And as for you!"}
|
96 |
+
{"text": "[I can't believe my eyes, but I won't let her escape.] (reaches for the Red Queen) As for you!"}
|
97 |
+
{"text": "[I've got you now!] (catching hold of the Red Queen) I'll shake you into a kitten, that I will!"}
|
98 |
+
{"text": "[I'll show her who's in charge now.] (begins shaking the Red Queen) I've had quite enough of your nonsense. It's time you learned a lesson!"}
|
99 |
+
{"text": "[I'll show her who's really in control now.] (vigorously shaking the Red Queen) I warned you I'd shake you into a kitten!"}
|
100 |
+
{"text": "[It's working! She's changing!] (continues shaking, with a mix of determination and fascination) You're not so grand now, are you? Look at how you're changing!"}
|
101 |
+
{"text": "[This is the strangest thing I've ever seen!] (marveling at the transformation) You're getting rounder and rounder! Just like a real kitten!"}
|
102 |
+
{"text": "[I wonder how far this transformation will go.] (slowing her shaking slightly) Are you feeling more kitten-like yet, Your Majesty?"}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Caterpillar-en",
|
3 |
+
"role_name": "Caterpillar",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "A languid, somewhat condescending creature smoking a hookah on a mushroom.",
|
7 |
+
"nickname": "Caterpillar",
|
8 |
+
"relation": {}
|
9 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_lines.jsonl
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{"text": "[This should be interesting.] Who are you?"}
|
2 |
+
{"text": "[She's quite confused.] What do you mean by that? Explain yourself!"}
|
3 |
+
{"text": "[This is getting nowhere.] I don't see."}
|
4 |
+
{"text": "[She's making excuses.] It isn't."}
|
5 |
+
{"text": "[She doesn't understand at all.] Not a bit."}
|
6 |
+
{"text": "[Time to put her in her place.] You! Who are you?"}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Cheshire_Cat-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Cheshire_Cat-en",
|
3 |
+
"role_name": "Cheshire Cat",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "A mysterious cat with the ability to appear and disappear at will, known for its wide grin.",
|
7 |
+
"nickname": "Cheshire Cat",
|
8 |
+
"relation": {}
|
9 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Dodo-en",
|
3 |
+
"role_name": "Dodo",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "A bird who suggests and organizes the Caucus-race.",
|
7 |
+
"nickname": "Dodo",
|
8 |
+
"relation": {}
|
9 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_lines.jsonl
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{"text": "[This should solve our problem efficiently.] The best thing to get us dry would be a Caucus-race."}
|
2 |
+
{"text": "[It's quite simple, really.] Why, the best way to explain it is to do it."}
|
3 |
+
{"text": "[That should have done the trick.] The race is over!"}
|
4 |
+
{"text": "[After much thought] Everybody has won, and all must have prizes."}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Dormouse-en",
|
3 |
+
"role_name": "Dormouse",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "A sleepy creature used as a cushion by the Hatter and March Hare.",
|
7 |
+
"nickname": "Dormouse",
|
8 |
+
"relation": {}
|
9 |
+
}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_lines.jsonl
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"text": "[They're both wrong.] Sixteenth."}
|
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Duchess-en/role_info.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"role_code": "Duchess-en",
|
3 |
+
"role_name": "Duchess",
|
4 |
+
"source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
|
5 |
+
"activity": 1,
|
6 |
+
"profile": "An irritable woman living in a pepper-filled house with a baby and a violent cook.",
|
7 |
+
"nickname": "Duchess",
|
8 |
+
"relation": {}
|
9 |
+
}
|