Spaces:
Sleeping
Sleeping
| import json | |
| import re | |
| import textwrap | |
| from graphviz import Digraph | |
| import os | |
| def wrap_text(text, max_chars): | |
| """ | |
| 对文本进行自动换行包装,每行最大字符数为 max_chars。 | |
| """ | |
| return textwrap.fill(text, width=max_chars) | |
| def parse_md_refs(md_content): | |
| """ | |
| 解析 Markdown 内容,提取以 x.y.z 格式标题对应的引用。 | |
| 对于每个满足格式的 section,其内容中所有形如 [数字] 的引用 | |
| 将被抽取出来,去重后按数字升序排序,生成类似 "[1,2,3]" 的引用字符串。 | |
| 如果遇到 undesired header(如 "6 Future Directions" 或 "7 Conclusion"), | |
| 则停止后续内容的解析,确保最后一个 section 仅包含到该 header 之前的内容。 | |
| 返回字典,键为 section 编号(例如 "3.1.1"),值为引用字符串(例如 "[1,2,3]")。 | |
| """ | |
| ref_dict = {} | |
| # 处理 Markdown 内容(按行拆分) | |
| lines = md_content.split("\n") if md_content else [] | |
| # 匹配 Markdown 标题中以 x.y.z 开头的叶节点(例如 "5.1.1 Neural Topic...") | |
| section_header_regex = re.compile(r'^\s*#+\s*(\d+\.\d+\.\d+).*') | |
| # 匹配 undesired header,如 "6 Future Directions" 或 "7 Conclusion" | |
| undesired_header_regex = re.compile(r'^\s*#+\s*(6 Future Directions|7 Conclusion)\b') | |
| # 匹配引用,例如 [数字] | |
| ref_pattern = re.compile(r'\[(\d+)\]') | |
| current_section = None | |
| current_content = [] | |
| for line in lines: | |
| # 如果检测到 undesired header,则先处理当前 section,再退出循环 | |
| if undesired_header_regex.match(line): | |
| break | |
| header_match = section_header_regex.match(line) | |
| if header_match: | |
| # 处理上一个 section | |
| if current_section is not None: | |
| all_refs = [int(num) for content_line in current_content for num in ref_pattern.findall(content_line)] | |
| if all_refs: | |
| ref_dict[current_section] = "[" + ",".join(map(str, sorted(set(all_refs)))) + "]" | |
| # 更新当前 section | |
| current_section = header_match.group(1) | |
| current_content = [] | |
| else: | |
| if current_section is not None: | |
| current_content.append(line) | |
| # 处理最后一个 section | |
| if current_section is not None and current_content: | |
| all_refs = [int(num) for content_line in current_content for num in ref_pattern.findall(content_line)] | |
| if all_refs: | |
| ref_dict[current_section] = "[" + ",".join(map(str, sorted(set(all_refs)))) + "]" | |
| return ref_dict | |
| def generate_graphviz_png(json_path, output_png_path, md_content=None, title="Document Outline", max_root_chars=20): | |
| """ | |
| 从 JSON 文件中读取大纲,构造树状结构,并生成 mindmap 的 PNG 图片。 | |
| 如果提供了 md_content,则根据 Markdown 内容中以 x.y.z 格式标题对应的引用, | |
| 在生成 mindmap 时,对于叶节点(没有子节点且标题以 x.y.z 开头)的标签, | |
| 在原文本后追加一个换行,然后添加引用信息(例如 "[1,2,3]"), | |
| 且引用经过数字排序。 | |
| 同时,仅对根节点文本进行自动换行包装,以限制根节点的最大宽度, | |
| 其它节点保持原始文本格式。 | |
| 参数: | |
| json_path: JSON 文件路径(包含大纲) | |
| output_png_path: 输出 PNG 文件路径(不带后缀) | |
| md_content: Markdown 文本内容(字符串),可选 | |
| title: 用于替换 mindmap 中根节点的标题,默认 "Document Outline" | |
| max_root_chars: 限制根节点每行最大字符数,默认 20 | |
| """ | |
| # 解析 Markdown 内容的引用 | |
| ref_dict = parse_md_refs(md_content) if md_content else {} | |
| # 读取 JSON 大纲 | |
| with open(json_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| outline_str = data.get("outline", "") | |
| # 解析形如 [层级, '标题'] 的项 | |
| pattern = re.compile(r"\[(\d+),\s*'([^']+)'\]") | |
| items = pattern.findall(outline_str) | |
| items = [(int(level), title) for level, title in items] | |
| # 不需要的标题关键词 | |
| undesired_keywords = {"Abstract", "Introduction", "Future Directions", "Conclusion"} | |
| # 过滤掉不需要的条目 | |
| filtered_items = [ | |
| (lvl, title) for lvl, title in items | |
| if not re.match(r"^\d+\s+(.+)", title) or re.match(r"^\d+\s+(.+)", title).group(1) not in undesired_keywords | |
| ] | |
| # 构造树状结构 | |
| tree = [] | |
| stack = [] | |
| for lvl, title_item in filtered_items: | |
| node = {"title": title_item, "children": []} | |
| while stack and lvl <= stack[-1][0]: | |
| stack.pop() | |
| if stack: | |
| stack[-1][1]["children"].append(node) | |
| else: | |
| tree.append(node) | |
| stack.append((lvl, node)) | |
| # 生成 Mindmap | |
| dot = Digraph(comment=title, format='png', engine='dot') | |
| dot.graph_attr.update(rankdir='LR', splines='ortho', bgcolor='white', dpi="150") | |
| dot.attr('node', shape='box', style='rounded,filled', fillcolor='white', color='gray') | |
| dot.edge_attr.update(arrowhead='none', color="black") | |
| # 处理根节点 | |
| wrapped_title = wrap_text(title, max_root_chars) | |
| dot.node('root', label=wrapped_title, shape='ellipse', style='filled', fillcolor='lightgray') | |
| node_counter = [0] | |
| section_pattern = re.compile(r'^(\d+\.\d+\.\d+)') | |
| def add_nodes(node, parent_id): | |
| current_id = f'node_{node_counter[0]}' | |
| node_counter[0] += 1 | |
| safe_label = node['title'].replace('"', r'\"') | |
| # 如果是叶节点且标题以 x.y.z 开头,则追加引用信息(如果存在) | |
| if not node["children"]: | |
| m = section_pattern.match(safe_label) | |
| if m: | |
| section_id = m.group(1) | |
| if section_id in ref_dict: | |
| safe_label += "\n" + ref_dict[section_id] | |
| dot.node(current_id, label=safe_label) | |
| dot.edge(parent_id, current_id) | |
| for child in node.get("children", []): | |
| add_nodes(child, current_id) | |
| for top_node in tree: | |
| add_nodes(top_node, "root") | |
| dot.render(output_png_path, cleanup=True) | |
| print("生成 PNG 文件:", output_png_path + ".png") | |
| return output_png_path + ".png" | |
| def insert_outline_image(png_path, md_content, survey_title): | |
| """ | |
| 在给定的 Markdown 内容字符串中查找 "2 Introduction" 这一行, | |
| 然后在该位置之前插入 outline 图片的 HTML 代码块,确保渲染时 | |
| HTML 块与后续 Markdown 内容间有足够空行分隔开。 | |
| 参数: | |
| png_path: 要插入的 PNG 图片路径,将作为 img 的 src 属性值。 | |
| md_content: Markdown 文件内容字符串。 | |
| survey_title: 用于生成图片说明文字的问卷标题。 | |
| 插入的 HTML 格式如下: | |
| <div style="text-align:center"> | |
| <img src="{png_path}" alt="Outline" style="width:100%;"/> | |
| </div> | |
| <div style="text-align:center"> | |
| Fig 1. The outline of the {survey_title} | |
| </div> | |
| 函数返回更新后的 Markdown 内容字符串。 | |
| """ | |
| # 将 Markdown 内容字符串分割成行(保留换行符) | |
| lines = md_content.splitlines(keepends=True) | |
| print(lines) | |
| # 查找包含 "2 Introduction" 的行的索引 | |
| intro_index = None | |
| for i, line in enumerate(lines): | |
| if '2 Introduction' in line: | |
| intro_index = i | |
| break | |
| if intro_index is None: | |
| print("没有找到 '2 Introduction' 这一行!") | |
| return md_content | |
| # 确保路径中的反斜杠被替换成正斜杠 | |
| png_path_fixed = png_path.replace("\\", "/") | |
| # 构造需要插入的 HTML 代码块,在前后增加空行 | |
| html_snippet = ( | |
| "\n\n" # 添加换行确保与上文/下文分隔 | |
| f'<div style="text-align:center">\n' | |
| f' <img src="{png_path_fixed}" alt="Outline" style="width:100%;"/>\n' | |
| f'</div>\n' | |
| f'<div style="text-align:center">\n' | |
| f' Fig 1. The outline of the {survey_title}\n' | |
| f'</div>\n' | |
| "\n" # 再添加一个空行确保与下方内容分隔 | |
| ) | |
| print(f"将在第 {intro_index} 行插入如下 HTML 代码块(插入在 '2 Introduction' 之前):\n{html_snippet}") | |
| # 在找到的 "2 Introduction" 这一行之前插入 html_snippet | |
| lines.insert(intro_index, html_snippet) | |
| # 合并所有行,构造更新后的 Markdown 内容 | |
| updated_md = "".join(lines) | |
| print("已在 Markdown 内容中插入 outline 图片。") | |
| return updated_md | |
| def insert_outline_figure(png_path, tex_content, survey_title): | |
| """ | |
| 在给定的 TeX 文件内容字符串中查找 "2 Introduction" 这一行, | |
| 然后在其之前插入一个跨页(双栏)的 figure* 环境,包括整页显示的图片。 | |
| 它将生成类似如下 LaTeX 片段: | |
| \begin{figure*}[htbp] | |
| \centering | |
| \includegraphics[width=\textwidth]{path/to/xxx.png} | |
| \caption{Fig 1. The outline of the XXX} | |
| \end{figure*} | |
| 参数: | |
| png_path: 要插入的 PNG 图片路径 | |
| tex_content: TeX 文件内容字符串 | |
| survey_title: 用于生成图片 caption 的文献/问卷标题 | |
| 返回: | |
| 更新后的 TeX 文本字符串 | |
| """ | |
| # 将 TeX 内容逐行分割(保留换行符) | |
| lines = tex_content.splitlines(keepends=True) | |
| # 查找包含 "2 Introduction" 的行索引 | |
| intro_index = None | |
| for i, line in enumerate(lines): | |
| if 'Introduction' in line: | |
| intro_index = i | |
| break | |
| # 如果找不到,就直接返回原文 | |
| if intro_index is None: | |
| print("没有找到 'Introduction' 这一行,未执行插入。") | |
| return tex_content | |
| # 构造 TeX 的 figure* 代码块 | |
| # 为确保整页,可用 [p] 或者 [htbp],具体可根据排版需要调整 | |
| # 也可替换成普通 \begin{figure} ... \end{figure},如果不需要跨双栏 | |
| figure_block = ( | |
| "\n" # 加一个空行,确保与上文分隔 | |
| "\\begin{figure*}[htbp]\n" | |
| " \\centering\n" | |
| f" \\includegraphics[width=\\textwidth]{{{png_path}}}\n" | |
| f" \\caption{{The outline of our survey: {survey_title}}}\n" | |
| "\\end{figure*}\n\n" # 再留一个空行分隔 | |
| ) | |
| # 在找到的 "2 Introduction" 所在行之前插入 figure 环境 | |
| lines.insert(intro_index, figure_block) | |
| # 重新拼接所有行 | |
| updated_tex = "".join(lines) | |
| return updated_tex | |
| # 使用示例: | |
| # if __name__ == "__main__": | |
| # png_path = 'src/static/data/info/test_4/outline.png' | |
| # md_content = '' | |
| # survey_title = "My Survey Title" | |
| # updated_md = insert_outline_image(png_path, md_content, survey_title) | |
| # -------------------------- | |
| # 使用示例 | |
| # -------------------------- | |
| if __name__ == "__main__": | |
| json_path = os.path.join("src", "static", "data", "txt", 'test_2', "outline.json") | |
| output_png_path = os.path.join("src", "static", "data", "info", 'test_2', "outline") | |
| md_path = os.path.join("src", "static", "data", "info", 'test_2', f"survey_{'test_2'}_processed.md") | |
| flowchart_results_path = os.path.join("src", "static", "data", "info", 'test_2', "flowchart_results.json") | |
| png_path = generate_graphviz_png( | |
| json_path=json_path, | |
| output_png_path=output_png_path, | |
| md_path=md_path, | |
| title='test', | |
| max_root_chars=30 | |
| ) | |
| # generate_graphviz_png(json_file_path, output_png_file, md_file_path, title=mindmap_title, max_root_chars=20) |