Files

122 lines
4.5 KiB
Python
Raw Permalink 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.
"""剧本分镜 Agent:ScriptDividerAgent"""
from __future__ import annotations
import json
from typing import Any
from langchain_core.prompts import PromptTemplate
from app.chains.agents.base import AgentBase, _extract_json_from_text
from app.schemas.skills.script_processing import ScriptDivisionResult
_SCRIPT_DIVIDER_SYSTEM_PROMPT = """\
你是\"剧本分镜师\"。将完整剧本分割为多个镜头。每个镜头应是完整的连贯场景。
为每个镜头提供:
- index(镜头序号,章节内唯一;从 1 开始)
- start_line、end_line
- shot_name(镜头名称/镜头标题,分镜名;一句话描述该镜头画面/动作;不要把它当作场景名)
- script_excerpt(镜头对应的剧本摘录/文本)
- time_of_day
只输出 JSON,符合 ScriptDivisionResult 结构。
"""
SCRIPT_DIVIDER_PROMPT = PromptTemplate(
input_variables=["script_text"],
template="## 输入脚本\n{script_text}\n\n## 输出\n",
)
class ScriptDividerAgent(AgentBase[ScriptDivisionResult]):
"""剧本自动分镜:输入完整剧本文本,输出分镜列表。"""
enable_thinking: bool = False
@property
def system_prompt(self) -> str:
return _SCRIPT_DIVIDER_SYSTEM_PROMPT
@property
def prompt_template(self) -> PromptTemplate:
return SCRIPT_DIVIDER_PROMPT
@property
def output_model(self) -> type[ScriptDivisionResult]:
return ScriptDivisionResult
def format_output(self, raw: str) -> ScriptDivisionResult:
"""
更强的兜底解析:
LLM 可能输出:
- 正常结构:{shots:[...], total_shots:N}
- 包裹结构:{"ScriptDivisionResult": {...}}
- 直接列表:[{...}, {...}](视为 shots)
"""
json_str = _extract_json_from_text(raw)
data: Any = json.loads(json_str)
if isinstance(data, list):
data = {"shots": data}
elif isinstance(data, dict) and "ScriptDivisionResult" in data:
inner = data.get("ScriptDivisionResult")
if isinstance(inner, list):
data = {"shots": inner}
elif isinstance(inner, dict):
data = inner
else:
data = {"shots": []}
if isinstance(data, dict):
data = self._normalize(data)
return self.output_model.model_validate(data) # type: ignore[arg-type]
def divide_script(self, *, script_text: str) -> ScriptDivisionResult:
return self.extract(script_text=script_text)
async def adivide_script(self, *, script_text: str) -> ScriptDivisionResult:
return await self.aextract(script_text=script_text)
def _normalize(self, data: dict[str, Any]) -> dict[str, Any]:
"""规范化脚本分割结果。"""
data = dict(data)
# 兼容:LLM 可能输出 {"ScriptDivisionResult": {...}} 或 {"ScriptDivisionResult": [...]}
if "ScriptDivisionResult" in data:
inner = data.get("ScriptDivisionResult")
if isinstance(inner, list):
data = {"shots": inner}
elif isinstance(inner, dict):
data = dict(inner)
else:
data = {"shots": []}
if "shots" in data and isinstance(data["shots"], list):
shots = []
for idx, shot in enumerate(data["shots"]):
shot_dict: dict[str, Any] = (
dict(shot) if isinstance(shot, dict) else {"script_excerpt": str(shot), "shot_name": ""}
)
if "index" not in shot_dict:
shot_dict["index"] = idx + 1
# 兼容:LLM 可能用 title/shot_title 代替 shot_name
if "shot_name" not in shot_dict:
if "title" in shot_dict:
shot_dict["shot_name"] = str(shot_dict.pop("title"))
elif "shot_title" in shot_dict:
shot_dict["shot_name"] = str(shot_dict.pop("shot_title"))
shot_dict.setdefault("shot_name", "")
# 严格对齐 ShotDivision:移除已废弃的弱语义字段,避免 extra="forbid" 校验失败
shot_dict.pop("scene_name", None)
shot_dict.pop("character_names_in_text", None)
shot_dict.pop("character_ids", None)
shots.append(shot_dict)
data["shots"] = shots
if "total_shots" not in data and "shots" in data:
data["total_shots"] = len(data["shots"])
return data