Commit 245d84f

benny-dou <60535774+benny-dou@users.noreply.github.com>
2026-05-20 03:18:09
chore(summary): add mermaid_url to summary result
1 parent 7854701
Changed files (1)
src
src/ai/summary.py
@@ -4,6 +4,7 @@ import base64
 import hashlib
 import json
 import re
+import zlib
 from pathlib import Path
 
 from loguru import logger
@@ -22,21 +23,50 @@ from database.r2 import set_cf_r2
 from messages.help import social_media_help
 from messages.sender import send2tg
 from messages.utils import equal_prefix, set_reaction, startswith_prefix
-from networking import download_file
+from networking import download_file, shorten_url
 from utils import count_subtitles, rand_number
 
+MERMAID_TEMPLATE = """
+graph LR
+    A[核心主题] --> B[子标题1]
+    A --> C[子标题2]
+    A --> D[子标题3]
+    A --> E[子标题4]
+
+
+    B --> B1[二级标题1-1]
+    B --> B2[二级标题1-2]
+    B1 --> B11[核心观点1-1-1]
+    B1 --> B12[核心观点1-1-2]
+    B2 --> B21[争议点1-2-1]
+    B2 --> B22[争议点1-2-2]
+
+
+    C --> C1[关键数据2-1]
+    C --> C2[主要结论2-1]
+    C --> C3[补充结论2-2]
+
+    D --> D1[核心问题3-1]
+    D --> D2[潜在风险3-2]
+    D --> D3[影响因素3-3]
+
+    E --> E1[发展趋势4-1]
+    E --> E2[行动建议4-2]
+    E --> E3[未来结论4-3]
+""".strip()
+
 JSON_SCHEMA = {
-    "title": "Content Summary",
-    "description": "提炼出资料的核心内容,生成符合指定JSON格式的全文总结、分片内容和思维导图",
+    "title": "Content Extraction",
+    "description": "精准提炼资料的核心主题、关键观点、主要结论及各片段核心内容,确保输出内容全面覆盖资料的关键信息,用户仅通过总结即可掌握信息全貌。",
     "type": "object",
     "properties": {
-        "abstract": {
+        "overview": {
             "title": "全文总结",
-            "description": "需涵盖资料核心主题、关键观点和主要结论,用连贯的一段话概括资料的主要内容,避免过于简略。如果内容过长,也可考虑分段总结。",
+            "description": "需涵盖资料核心主题、关键观点和主要结论,采用连贯语言表述,若内容复杂可分段,但需逻辑清晰。禁止过于简略(如仅用一句话概括长文档),确保信息密度足够支撑用户理解。",
             "type": "string",
         },
         "sections": {
-            "description": "将资料划分为不同的片段,每个片段需拟定简洁准确的标题,匹配1个相关emoji,并总结该片段的核心内容",
+            "description": "需将文档划分为逻辑连贯的片段(如按章节、主题、时间线划分);每个片段需拟定**简洁准确**的标题(体现片段核心)、匹配1个相关emoji;并说明该片段的核心内容。",
             "title": "分片内容",
             "type": "array",
             "items": {
@@ -44,10 +74,10 @@ JSON_SCHEMA = {
                 "properties": {
                     "title": {"type": "string", "description": "该片段的标题"},
                     "emoji": {"type": "string", "description": "匹配该片段的emoji,例如💡、💰、⚠️等"},
-                    "summary": {"type": "string", "description": "概括该片段的核心内容"},
+                    "content": {"type": "string", "description": "详细说明该片段的核心事件、具体观点或结论,禁止仅用1-2句话泛泛概括,需传递足够细节。"},
                     "start": {
                         "type": ["string", "null"],
-                        "description": "如果资料内容为包含时间戳的文字稿(如播客、视频、音频的转录稿),设置此字段为该片段的开始时间, 格式为(HH:MM:SS或MM:SS)。如果没有时间戳,则无需输出此字段。",
+                        "description": "如果资料为含时间戳的文字稿(如播客/视频/音频的转录稿),需补充start字段HH:MM:SS或MM:SS;无时间戳则无需输出start字段。",
                     },
                 },
             },
@@ -55,11 +85,11 @@ JSON_SCHEMA = {
         "mermaid": {
             "title": "思维导图",
             "type": "string",
-            "pattern": "^flowchart LR",
-            "description": "以Mermaid flowchart格式表示的全文思维导图,以'flowchart LR'开头",
+            "pattern": "^graph LR",
+            "description": f"以Mermaid graph格式表示的全文思维导图,以'graph LR'开头。需清晰呈现文档的逻辑结构(如核心主题→子主题→关键观点/结论),节点层级明确,便于用户快速梳理文档框架。一个示例Mermaid代码如下:\n{MERMAID_TEMPLATE}",
         },
     },
-    "required": ["abstract", "sections", "mermaid"],
+    "required": ["overview", "sections", "mermaid"],
     "additionalProperties": False,
 }
 
@@ -90,14 +120,13 @@ async def ai_summary(client: Client, message: Message, summary_model_id: str = A
             res = await openai_responses_api(client, message, **params)
         if not res.get("texts"):
             continue
-        texts, mermaid_path = await parse_summary(res["texts"])
+        texts, _, mermaid_path = await parse_summary(res["texts"])
         media = [{"photo": mermaid_path}] if Path(mermaid_path).is_file() else []
         await send2tg(client, message, texts=texts, media=media, **kwargs)
         await set_reaction(client, this_msg, "")
         return
 
 
-
 async def summarize(article: str, reference: str | None = None, model: str = "gemini") -> dict:
     if count_subtitles(article) < 200:  # skip short article
         return {}
@@ -112,43 +141,45 @@ async def summarize(article: str, reference: str | None = None, model: str = "ge
     )
     if not res.get("texts", ""):
         return {}
-    texts, _ = await parse_summary(res["texts"])
+    texts, mermaid_url, mermaid_path = await parse_summary(res["texts"])
     res["texts"] = texts
+    res["mermaid_url"] = mermaid_url
+    res["mermaid_path"] = mermaid_path
     return res
 
 
-async def parse_summary(texts: str) -> tuple[str, str]:
+async def parse_summary(texts: str) -> tuple[str, str, str]:
     """Parse the summary JSON string.
 
     Returns:
-        (summary_texts, mermaid_img_path)
+        (summary_texts, mermaid_url, mermaid_path)
     """
     try:
         summary = json.loads(texts)
         mermaid = beautify_mermaid(summary["mermaid"])
-        mermaid_url, mermaid_path = await save_mermaid_jpg_to_r2(mermaid)
-        parsed = f"{summary['abstract'].strip()}"
-        if mermaid_url:
-            logger.success(f"Mermaid: {mermaid_url}")
-            parsed += f"\n🧠**[思维导图]({mermaid_url})**\n![Mermaid]({mermaid_url})"
+        img_url, pako_url, mermaid_path = await publish_mermaid(mermaid)
+        parsed = f"{summary['overview'].strip()}"
+        if img_url:
+            logger.success(f"Mermaid: {pako_url}")
+            parsed += f"\n🧠**[思维导图]({pako_url})**\n![Mermaid]({img_url})"
         parsed += "\n⚡️**章节速览**"
         for section in summary["sections"]:
             parsed += f"\n{section['emoji']}**{section['title']}**"
             if section.get("start"):
                 parsed += f" [{section['start']}]"
-            parsed += f"\n{section['summary']}"
+            parsed += f"\n{section['content']}"
         logger.success(parsed)
     except Exception as e:
         logger.error(f"Error parsing summary: {e}")
-        return texts, ""
-    return parsed, mermaid_path
+        return texts, "", ""
+    return parsed, img_url, mermaid_path
 
 
 def system_prompt(reference: str | None = None) -> str:
-    prompt = "你是一位专业的内容总结大师,任务是基于用户提供的资料提炼出核心内容,生成符合指定JSON格式的全文总结、分片内容和思维导图。"
+    prompt = f"你是一位专业的内容提炼大师,任务是基于用户提供的资料,生成用户无需阅读完整原文档就能清晰理解主要事件、观点、结论的内容,生成符合指定JSON格式的全文总结、分片内容和思维导图。思维导图Mermaid语法说明文档:{mermaid_syntax()}"
     if reference:
         prompt += f"\n{reference}"
-    return prompt.strip() + mermaid_syntax()
+    return prompt.strip()
 
 
 def beautify_mermaid(mermaid: str) -> str:
@@ -171,20 +202,29 @@ def beautify_mermaid(mermaid: str) -> str:
     return f"---\nconfig:\n  theme: neo\n  look: neo\n---\n{mermaid.strip()}"
 
 
-async def save_mermaid_jpg_to_r2(mermaid: str) -> tuple[str, str]:
+async def publish_mermaid(mermaid: str) -> tuple[str, str, str]:
     """Save Mermaid image to R2.
 
     Returns:
-        (image_url, local_path)
+        (image_url, pako_url, local_path)
     """
     b64_str = base64.urlsafe_b64encode(mermaid.encode("utf-8")).decode("ascii")
-    save_path = Path(DOWNLOAD_DIR) / f"{hashlib.sha256(mermaid.encode()).hexdigest()}.jpg"
+    save_path = Path(DOWNLOAD_DIR) / f"{hashlib.md5(mermaid.encode()).hexdigest()}.jpg"  # noqa: S324
+    r2_key = f"TTL/365d/{save_path.name}"
+    img_url = f"{DB.CF_R2_PUBLIC_URL}/{r2_key}"
+    img_url = await shorten_url(img_url)
     await download_file(f"https://mermaid.ink/img/{b64_str}?type=jpeg&theme=forest&width=2160", path=save_path, suffix=".jpg")
+    mermaid = mermaid.replace("\ngraph LR", f"\n%% {img_url}\ngraph LR")
+    # generate pako url for mermaid image
+    json_str = json.dumps({"code": mermaid.strip()}, separators=(",", ":"))
+    compressed_bytes = zlib.compress(json_str.encode("utf-8"), level=9)
+    pako_b64_str = base64.urlsafe_b64encode(compressed_bytes).decode("utf-8").rstrip("=")
+    pako_url = await shorten_url(f"https://mermaid.live/view#pako:{pako_b64_str}")
+
     if save_path.is_file():
-        r2_key = f"TTL/365d/{save_path.name}"
         await set_cf_r2(r2_key, data=save_path.read_bytes(), mime_type="image/jpeg", silent=True)
-        return f"{DB.CF_R2_PUBLIC_URL}/{r2_key}", save_path.as_posix()
-    return "", ""
+        return img_url, pako_url, save_path.as_posix()
+    return "", "", ""
 
 
 def summary_params(reference: str | None = None) -> dict:
@@ -199,9 +239,9 @@ def summary_params(reference: str | None = None) -> dict:
             "text": {
                 "format": {
                     "type": "json_schema",
-                    "name": "ContentSummary",
+                    "name": "ContentExtraction",
                     "strict": True,
-                    "description": "提炼出资料的核心内容,生成符合指定JSON格式的全文总结、分片内容和思维导图",
+                    "description": "精准提炼资料的核心主题、关键观点、主要结论及各片段核心内容,确保输出内容全面覆盖资料的关键信息,用户仅通过总结即可掌握信息全貌。",
                     "schema": JSON_SCHEMA,
                 }
             },
@@ -215,14 +255,14 @@ def summary_params(reference: str | None = None) -> dict:
 
 def mermaid_syntax() -> str:
     return """
-# Mermaid Flowcharts - Basic Syntax
+# Mermaid Graph - Basic Syntax
 
-Flowcharts are composed of **nodes** (geometric shapes) and **edges** (arrows or lines). The Mermaid code defines how nodes and edges are made and accommodates different arrow types, multi-directional arrows, and any linking to and from subgraphs.
+Graph is composed of **nodes** (geometric shapes) and **edges** (arrows or lines). The Mermaid code defines how nodes and edges are made and accommodates different arrow types, multi-directional arrows, and any linking to and from subgraphs.
 
-### A node (default)
+## A node (default)
 
 ```mermaid
-flowchart LR
+graph LR
     id
 ```
 
@@ -237,7 +277,7 @@ found for the node that will be used. Also if you define edges for the node late
 one previously defined will be used when rendering the box.
 
 ```mermaid
-flowchart LR
+graph LR
     id1[This is the text in the box]
 ```
 
@@ -246,45 +286,10 @@ flowchart LR
 ### A node with round edges
 
 ```mermaid
-flowchart LR
+graph LR
     id1(This is the text in the box)
 ```
 
-### A stadium-shaped node
-
-```mermaid
-flowchart LR
-    id1([This is the text in the box])
-```
-
-### A node in a subroutine shape
-
-```mermaid
-flowchart LR
-    id1[[This is the text in the box]]
-```
-
-### A node in a cylindrical shape
-
-```mermaid
-flowchart LR
-    id1[(Database)]
-```
-
-### A node in the form of a circle
-
-```mermaid
-flowchart LR
-    id1((This is the text in the circle))
-```
-
-### A node in an asymmetric shape
-
-```mermaid
-flowchart LR
-    id1>This is the text in the box]
-```
-
 ## Links between nodes
 
 Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link.
@@ -292,136 +297,56 @@ Nodes can be connected with links/edges. It is possible to have different types
 ### A link with arrow head
 
 ```mermaid
-flowchart LR
+graph LR
     A-->B
 ```
 
 ### An open link
 
 ```mermaid
-flowchart LR
+graph LR
     A --- B
 ```
 
 ### Text on links
 
 ```mermaid
-flowchart LR
-    A-- This is the text! ---B
-```
-
-or
-
-```mermaid
-flowchart LR
+graph LR
     A---|This is the text|B
 ```
 
 ### A link with arrow head and text
 
 ```mermaid
-flowchart LR
+graph LR
     A-->|text|B
 ```
 
-or
-
-```mermaid
-flowchart LR
-    A-- text -->B
-```
-
 ### Dotted link
 
 ```mermaid
-flowchart LR
+graph LR
    A-.->B;
 ```
 
 ### Dotted link with text
 
 ```mermaid
-flowchart LR
+graph LR
    A-. text .-> B
 ```
 
 ### Thick link
 
 ```mermaid
-flowchart LR
+graph LR
    A ==> B
 ```
 
 ### Thick link with text
 
 ```mermaid
-flowchart LR
+graph LR
    A == text ==> B
 ```
-
-### An invisible link
-
-This can be a useful tool in some instances where you want to alter the default positioning of a node.
-
-```mermaid
-flowchart LR
-    A ~~~ B
-```
-
-### Chaining of links
-
-It is possible declare many links in the same line as per below:
-
-```mermaid
-flowchart LR
-   A -- text --> B -- text2 --> C
-```
-
-It is also possible to declare multiple nodes links in the same line as per below:
-
-```mermaid
-flowchart LR
-   a --> b & c--> d
-```
-
-You can then describe dependencies in a very expressive way. Like the one-liner below:
-
-```mermaid
-flowchart TB
-    A & B--> C & D
-```
-
-If you describe the same diagram using the basic syntax, it will take four lines. A
-word of warning, one could go overboard with this making the flowchart harder to read in
-markdown form. The Swedish word `lagom` comes to mind. It means, not too much and not too little.
-This goes for expressive syntaxes as well.
-
-```mermaid
-flowchart TB
-    A --> C
-    A --> D
-    B --> C
-    B --> D
-```
-
-## New arrow types
-
-There are new types of arrows supported:
-
-- circle edge
-- cross edge
-
-### Circle edge example
-
-```mermaid
-flowchart LR
-    A --o B
-```
-
-### Cross edge example
-
-```mermaid
-flowchart LR
-    A --x B
-```
 """