Files
matrix-show/backend/app/services/script_parser.py
T
2026-06-04 17:59:53 +08:00

154 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()