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()