Files

154 lines
6.4 KiB
Python
Raw Permalink Normal View History

2026-06-04 17:59:53 +08:00
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()