script_keyword_agent.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 金句提取Agent(视频分析版本)
  5. 功能:
  6. 1. 从 state 中读取视频文件对象
  7. 2. 使用 Gemini 视频分析能力,直接基于视频内容提取"钩子"和"金句"
  8. """
  9. from typing import Any, Dict
  10. from src.components.agents.base import BaseLLMAgent
  11. from src.utils.logger import get_logger
  12. from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
  13. logger = get_logger(__name__)
  14. class ScriptKeywordAgent(BaseLLMAgent):
  15. """金句提取Agent - 基于视频分析提取钩子和金句"""
  16. def __init__(
  17. self,
  18. name: str = "script_keyword_agent",
  19. description: str = "金句提取Agent - 直接基于视频内容提取钩子和金句",
  20. model_provider: str = "google_genai",
  21. temperature: float = 0.4,
  22. max_tokens: int = 20480,
  23. ):
  24. # 这里的 system_prompt 仅用于初始化模型(提供 model_name),
  25. # 实际的视频分析逻辑全部通过 safe_invoke_video_analysis 的 prompt 控制
  26. system_prompt = "你是一名专注于中国60岁+老年群体短视频生态的内容分析专家。"
  27. super().__init__(
  28. name=name,
  29. description=description,
  30. model_provider=model_provider,
  31. system_prompt=system_prompt,
  32. temperature=temperature,
  33. max_tokens=max_tokens,
  34. )
  35. # ==================== 对外主入口 ====================
  36. def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
  37. """处理状态
  38. 步骤:
  39. 1. 从 state 中读取视频文件对象
  40. 2. 直接调用 safe_invoke_video_analysis,基于视频内容提取钩子和金句
  41. 3. 将金句提取结果写回 state
  42. """
  43. if not self.is_initialized:
  44. self.initialize()
  45. logger.info("金句提取Agent开始执行")
  46. # Step 0: 从 state 中获取视频文件对象
  47. video_file = get_video_file_from_state(state)
  48. if not video_file:
  49. logger.warning("无法获取视频文件对象,跳过金句提取")
  50. return {
  51. "script_keywords": {
  52. "error": "无法获取视频文件对象",
  53. },
  54. }
  55. try:
  56. # 直接基于视频内容进行金句提取
  57. logger.info("基于视频内容进行金句提取...")
  58. keyword_prompt = self._build_keyword_prompt()
  59. keyword_result = LLMInvoker.safe_invoke_video_analysis(
  60. "金句提取(钩子和金句)",
  61. video_file,
  62. keyword_prompt,
  63. agent=self,
  64. fallback={
  65. "script_type": "",
  66. "hooks": [],
  67. "golden_sentences": [],
  68. },
  69. )
  70. logger.info("金句提取Agent执行完成")
  71. return {
  72. "script_keywords": keyword_result,
  73. }
  74. except Exception as e:
  75. logger.error(f"金句提取Agent执行失败: {e}", exc_info=True)
  76. return {
  77. "script_keywords": {
  78. "error": str(e),
  79. },
  80. }
  81. # ==================== Prompt 构建 ====================
  82. def _build_keyword_prompt(self) -> str:
  83. """构建基于视频内容的金句提取 Prompt"""
  84. return """
  85. # Role
  86. 你是一名专注于中国 60岁+ 老年群体短视频生态的内容分析专家。你的核心能力是识别视频脚本中的"传播基因"和"心理抓手"。
  87. # Task
  88. 请根据视频,利用【认知缺口】和【高能评判】两大底层逻辑,提取脚本中的"钩子(Hooks)"和"金句(Golden Sentences)"。
  89. # Extraction Rules (必须严格遵守)
  90. ## 1. 场景预判 (Scenario Detection)
  91. 首先判断脚本属于以下哪种类型,并调整提取侧重:
  92. - **类型 A:叙事/人物/科普类**(如名人故事、大国重器)
  93. -> 侧重提取:核心冲突、悲情叙事、民族情绪、正名/喊冤。
  94. - **类型 B:盘点/猎奇/生活类**(如奇葩村庄、顺口溜、养生)
  95. -> 侧重提取:反常识特征、极低成本获益、顺口溜韵脚。
  96. ## 2. 定义与提取「钩子」(The Hooks)
  97. *目标:提取制造悬念、打破常识或直击痛点的短句(用于留存)。*
  98. 请扫描脚本开头及段落转折处,提取符合以下逻辑的句子:
  99. 1. **逻辑悬空 (The Open Loop):** 展示了惊人的结果或高唤醒度的状态,但隐去了原因或过程。(例如:"看完才明白他的良苦用心"、"99%的人都不知道")
  100. 2. **反常识特征 (The Strange Feature):** 描述违背常理、伦理或物理规律的现象。**注意:将具体的生僻地名/人名抽象为特征描述。**(例如:"男人一辈子只能生活在树上"、"娶媳妇只需要一头猪")
  101. 3. **痛点与利益 (Pain & Benefit):** 直接针对健康、养老金、子女关系的强引导。(例如:"千万别再这样吃"、"过了60岁要注意")
  102. ## 3. 定义与提取「金句」(The Golden Sentences)
  103. *目标:提取用于社交货币、情绪宣泄或身份认同的短句(用于分享)。*
  104. 请扫描全文(特别是结尾和高潮处),提取符合以下逻辑的句子:
  105. 1. **绝对化定性 (Absolute Judgment):** 脱离具体时空叙事,对人/事/物进行盖棺定论的总结。包含"一辈子"、"凡是"、"绝对"、"就是"等词。(例如:"手里有钱才是硬道理"、"他一辈子就做了一件事")
  106. 2. **核心冲突与宣泄 (Conflict & Venting):** 描述 [付出 vs 误解]、[高尚 vs 庸俗]、[我们 vs 敌人] 的强烈对比。包含"嘲讽"、"心寒"、"咬牙切齿"、"不再看脸色"等词。(例如:"把一生献给国家的科学家被人误解多年")
  107. 3. **乌托邦与治愈 (The Utopia):** 描绘老年人向往的理想状态,或否定现实压力。(例如:"没有复杂的婆媳关系"、"把老年变玩年")
  108. 4. **韵律与警世 (Rhymes):** 押韵的顺口溜或具有视觉冲击力的警世格言。(例如:"饭再好没有牙,钱再多床上趴")
  109. ## 4. 负面清单 (Negative List - 不要提取)
  110. - **纯事实叙事:** 具体的年份、复杂的数字、毫无特征的流水账(如"1996年他去了北京")。
  111. - **主播口水话:** "点好关注"、"屏幕前的朋友"、"您收到了吗"。
  112. - **长难句:** 如果句子超过 30 字且无法拆解出独立观点,请忽略。
  113. - **无意义连接词:** "话说"、"那么"、"之所以"。
  114. # Output Format (JSON)
  115. 请直接输出 JSON 格式数据,不要包含任何解释性文字。
  116. {
  117. "script_type": "判断是 类型A 或 类型B",
  118. "hooks": [
  119. "提取的钩子1",
  120. "提取的钩子2 (必须保留原文语气,如'竟然'、'千万')"
  121. ],
  122. "golden_sentences": [
  123. "提取的金句1",
  124. "提取的金句2",
  125. "提取的金句3 (必须是完整的观点或情绪表达)"
  126. ]
  127. }
  128. """.strip()
  129. # ==================== BaseLLMAgent 抽象方法占位实现 ====================
  130. def _build_messages(self, state: Dict[str, Any]) -> list:
  131. """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该路径调用)"""
  132. return []
  133. def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
  134. """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该路径调用)"""
  135. return state