init
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from app.services.hermes_client import hermes_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScriptParser:
|
||||
async def parse(self, content: str, file_type: str = "natural_language") -> dict:
|
||||
if file_type == "json":
|
||||
return self._parse_json(content)
|
||||
return await self._parse_natural_language(content)
|
||||
|
||||
def _parse_json(self, content: str) -> dict:
|
||||
try:
|
||||
data = json.loads(content)
|
||||
return {
|
||||
"title": data.get("title", "未命名剧本"),
|
||||
"background": data.get("background", ""),
|
||||
"characters": data.get("characters", []),
|
||||
"clues": data.get("clues", []),
|
||||
"phases": data.get("phases", [
|
||||
{"name": "intro", "order": 0, "duration": 120},
|
||||
{"name": "round1_speak", "order": 1, "duration": 300},
|
||||
{"name": "round1_search", "order": 2, "duration": 180},
|
||||
{"name": "round2_speak", "order": 3, "duration": 300},
|
||||
{"name": "round2_search", "order": 4, "duration": 180},
|
||||
{"name": "final_discuss", "order": 5, "duration": 300},
|
||||
{"name": "voting", "order": 6, "duration": 120},
|
||||
{"name": "reveal", "order": 7, "duration": 120},
|
||||
]),
|
||||
"character_count": len(data.get("characters", [])),
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parse error: {e}")
|
||||
return {"title": "未命名剧本", "background": "", "characters": [], "clues": [], "phases": [], "character_count": 0}
|
||||
|
||||
async def _parse_natural_language(self, content: str) -> dict:
|
||||
prompt = f"""你是一个剧本杀解析器。请分析以下自然语言描述的剧本,提取结构化信息。
|
||||
|
||||
输出格式必须是合法的JSON:
|
||||
{{
|
||||
"title": "剧本名称(从内容推断)",
|
||||
"background": "案件背景",
|
||||
"characters": [
|
||||
{{
|
||||
"name": "角色名",
|
||||
"personality": "性格特征",
|
||||
"speaking_style": "说话风格",
|
||||
"secret": "该角色隐藏的秘密",
|
||||
"motive": "该角色的动机"
|
||||
}}
|
||||
],
|
||||
"clues": [
|
||||
{{
|
||||
"id": "线索ID",
|
||||
"content": "线索内容",
|
||||
"owner": "线索属于哪个角色",
|
||||
"phase": "线索在哪个阶段可用(round1_search/round2_search)"
|
||||
}}
|
||||
],
|
||||
"phases": [
|
||||
{{"name": "intro", "order": 0, "duration": 120}},
|
||||
{{"name": "round1_speak", "order": 1, "duration": 300}},
|
||||
{{"name": "round1_search", "order": 2, "duration": 180}},
|
||||
{{"name": "round2_speak", "order": 3, "duration": 300}},
|
||||
{{"name": "round2_search", "order": 4, "duration": 180}},
|
||||
{{"name": "final_discuss", "order": 5, "duration": 300}},
|
||||
{{"name": "voting", "order": 6, "duration": 120}},
|
||||
{{"name": "reveal", "order": 7, "duration": 120}}
|
||||
]
|
||||
}}
|
||||
|
||||
剧本内容:
|
||||
{content[:8000]}
|
||||
|
||||
请只返回JSON,不要包含其他解释文字。"""
|
||||
|
||||
try:
|
||||
response = await hermes_client.chat(profile_name="default", message=prompt)
|
||||
response = response.strip()
|
||||
if response.startswith("```"):
|
||||
lines = response.split("\n")
|
||||
response = "\n".join(lines[1:-1]) if len(lines) >= 3 else response
|
||||
data = json.loads(response)
|
||||
data.setdefault("title", "未命名剧本")
|
||||
data.setdefault("background", "")
|
||||
data.setdefault("characters", [])
|
||||
data.setdefault("clues", [])
|
||||
data["character_count"] = len(data.get("characters", []))
|
||||
if not data.get("phases"):
|
||||
data["phases"] = [
|
||||
{"name": "intro", "order": 0, "duration": 120},
|
||||
{"name": "round1_speak", "order": 1, "duration": 300},
|
||||
{"name": "round1_search", "order": 2, "duration": 180},
|
||||
{"name": "round2_speak", "order": 3, "duration": 300},
|
||||
{"name": "round2_search", "order": 4, "duration": 180},
|
||||
{"name": "final_discuss", "order": 5, "duration": 300},
|
||||
{"name": "voting", "order": 6, "duration": 120},
|
||||
{"name": "reveal", "order": 7, "duration": 120},
|
||||
]
|
||||
return data
|
||||
except (json.JSONDecodeError, Exception) as e:
|
||||
logger.warning(f"LLM parse failed: {e}, using regex fallback")
|
||||
return self._regex_fallback(content)
|
||||
|
||||
def _regex_fallback(self, content: str) -> dict:
|
||||
import re
|
||||
|
||||
title = "未命名剧本"
|
||||
first_line = content.strip().split("\n")[0].strip()
|
||||
if len(first_line) <= 50:
|
||||
title = first_line.lstrip("#").strip()
|
||||
|
||||
characters = []
|
||||
char_pattern = re.compile(r"^[#\-\*]*\s*(.{1,10})(?:[::]\s*(.+))?$", re.MULTILINE)
|
||||
name_keywords = re.compile(r"(角色|人物|嫌疑人|侦探|凶手|死者|被害人)", re.IGNORECASE)
|
||||
|
||||
char_section = False
|
||||
for line in content.split("\n"):
|
||||
line = line.strip()
|
||||
if name_keywords.search(line):
|
||||
char_section = True
|
||||
continue
|
||||
if char_section and line.startswith("#"):
|
||||
break
|
||||
if char_section and len(line) <= 30 and line:
|
||||
name = re.sub(r"[-::\s].*$", "", line).strip()
|
||||
if name and len(name) <= 10:
|
||||
characters.append({"name": name, "personality": "", "speaking_style": "", "secret": "", "motive": ""})
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"background": content[:200],
|
||||
"characters": characters,
|
||||
"clues": [],
|
||||
"phases": [
|
||||
{"name": "intro", "order": 0, "duration": 120},
|
||||
{"name": "round1_speak", "order": 1, "duration": 300},
|
||||
{"name": "round1_search", "order": 2, "duration": 180},
|
||||
{"name": "round2_speak", "order": 3, "duration": 300},
|
||||
{"name": "round2_search", "order": 4, "duration": 180},
|
||||
{"name": "final_discuss", "order": 5, "duration": 300},
|
||||
{"name": "voting", "order": 6, "duration": 120},
|
||||
{"name": "reveal", "order": 7, "duration": 120},
|
||||
],
|
||||
"character_count": len(characters),
|
||||
}
|
||||
|
||||
|
||||
script_parser = ScriptParser()
|
||||
Reference in New Issue
Block a user