chore: initial commit — import worldmodel workspace (plans/, research/)
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
% =====================================================================
|
||||
% PRISM Book — Pandoc/XeLaTeX 模板
|
||||
% 适配:macOS + TinyTeX + ctex + xeCJK,可独立 xelatex 编译
|
||||
% =====================================================================
|
||||
\documentclass[11pt,a4paper,oneside,openany]{ctexbook}
|
||||
|
||||
% ── 几何 / 字体 / 中文 ────────────────────────────────────────────────
|
||||
\usepackage[a4paper,margin=2.4cm,headheight=24pt,headsep=0.5cm,footskip=1.2cm]{geometry}
|
||||
\usepackage{amssymb} % 提供 \boxtimes, \checkmark 等任务列表符号
|
||||
\usepackage{amsmath}
|
||||
\usepackage{xeCJK}
|
||||
\setCJKmainfont[BoldFont={STHeiti},ItalicFont={STKaiti}]{STSong}
|
||||
\setCJKsansfont{STHeiti}
|
||||
\setCJKmonofont{STHeiti}
|
||||
\setmainfont{Helvetica Neue}
|
||||
\setsansfont{Helvetica Neue}
|
||||
\setmonofont{Menlo}[Scale=0.85]
|
||||
\linespread{1.20}
|
||||
|
||||
% 让 xelatex 允许在 CJK 字符之间断行(关键!);
|
||||
% 否则一行长中文(常见于表格单元、长描述)即使在 p{w} 列里也不会自动换行,
|
||||
% 导致 Overfull \hbox 大量溢出。
|
||||
\XeTeXlinebreaklocale "zh"
|
||||
\XeTeXlinebreakskip = 0pt plus 1pt minus 0.1pt
|
||||
% URL 也允许任意位置断开,避免长链接溢出
|
||||
\usepackage{xurl}
|
||||
% sloppy 模式:允许 inter-word 间距稍微变化以避免溢出
|
||||
\sloppy
|
||||
\emergencystretch=3em
|
||||
|
||||
% 改用 hyphenat:允许在所有字符之间断行,主要为长下划线文件名/URL/路径。
|
||||
% 比 seqsplit 安全 — 它能在 \section / \texttt 中正常工作。
|
||||
\usepackage[htt]{hyphenat}
|
||||
% 对 etoolbox 提供的 \pretocmd 用 \texttt 包裹时启用低惩罚断字
|
||||
% (\texttt 内部已是 \ttfamily,hyphenat 的 htt 选项让 ttfamily 也能断字)
|
||||
|
||||
% ── 颜色 / 框 / 代码 ──────────────────────────────────────────────────
|
||||
\usepackage{xcolor}
|
||||
\definecolor{PrismRed}{HTML}{E63946}
|
||||
\definecolor{PrismOrange}{HTML}{F77F00}
|
||||
\definecolor{PrismYellow}{HTML}{FCBF49}
|
||||
\definecolor{PrismTeal}{HTML}{2A9D8F}
|
||||
\definecolor{PrismBlue}{HTML}{1D7AB8}
|
||||
\definecolor{PrismPurple}{HTML}{5A189A}
|
||||
\definecolor{InkDark}{HTML}{0E1116}
|
||||
\definecolor{InkMuted}{HTML}{4A5560}
|
||||
\definecolor{BoxBG}{HTML}{F4F2EB}
|
||||
\definecolor{CodeBG}{HTML}{F6F7F9}
|
||||
\definecolor{CodeBorder}{HTML}{D7DBE0}
|
||||
\definecolor{Accent}{HTML}{1D7AB8}
|
||||
\definecolor{GoldRule}{HTML}{C9A227}
|
||||
|
||||
\usepackage[most]{tcolorbox}
|
||||
\tcbset{
|
||||
enhanced, breakable, boxrule=0pt, arc=2pt,
|
||||
colback=BoxBG, colframe=Accent, fonttitle=\bfseries,
|
||||
before skip=4pt, after skip=4pt,
|
||||
left=8pt, right=8pt, top=4pt, bottom=4pt,
|
||||
beforeafter skip balanced
|
||||
}
|
||||
|
||||
\usepackage{listings}
|
||||
\lstdefinestyle{prism}{
|
||||
basicstyle=\ttfamily\footnotesize,
|
||||
backgroundcolor=\color{CodeBG},
|
||||
frame=single, rulecolor=\color{CodeBorder},
|
||||
framesep=4pt, framerule=0.4pt,
|
||||
xleftmargin=8pt, xrightmargin=8pt,
|
||||
showstringspaces=false,
|
||||
breaklines=true, breakatwhitespace=false,
|
||||
keywordstyle=\color{PrismPurple}\bfseries,
|
||||
commentstyle=\color{InkMuted}\itshape,
|
||||
stringstyle=\color{PrismTeal},
|
||||
numberstyle=\tiny\color{InkMuted}, numbers=none,
|
||||
upquote=true, columns=flexible,
|
||||
literate=
|
||||
{á}{{\'a}}1 {é}{{\'e}}1 {í}{{\'i}}1 {ó}{{\'o}}1 {ú}{{\'u}}1
|
||||
{→}{{$\rightarrow$}}1 {←}{{$\leftarrow$}}1
|
||||
{≥}{{$\geq$}}1 {≤}{{$\leq$}}1 {±}{{$\pm$}}1 {≈}{{$\approx$}}1
|
||||
{×}{{$\times$}}1 {⋯}{{$\cdots$}}1 {·}{{·}}1
|
||||
{…}{{\ldots}}1 {⌶}{{$\Box$}}1
|
||||
}
|
||||
\lstset{style=prism}
|
||||
|
||||
% pandoc 用 fancyvrb 转 verbatim;用 fvextra 扩展自动换行能力,避免长注释/长字符串超出页宽
|
||||
\usepackage{fvextra}
|
||||
\DefineVerbatimEnvironment{Verbatim}{Verbatim}{%
|
||||
fontsize=\footnotesize,%
|
||||
breaklines=true,%
|
||||
breakanywhere=true,%
|
||||
breaksymbolleft={},%
|
||||
breaksymbolright={},%
|
||||
}
|
||||
% 用 tcolorbox 来包代码块,既能套色又能正确处理换页
|
||||
\definecolor{CodeBoxBG}{HTML}{F6F7F9}
|
||||
\definecolor{CodeBoxRule}{HTML}{D7DBE0}
|
||||
\makeatletter
|
||||
\@ifundefined{Shaded}{%
|
||||
\newtcolorbox{ShadedBox}{
|
||||
enhanced, breakable,
|
||||
colback=CodeBoxBG, colframe=CodeBoxRule,
|
||||
boxrule=0.4pt, arc=2pt,
|
||||
left=6pt, right=6pt, top=4pt, bottom=4pt,
|
||||
before skip=4pt, after skip=4pt
|
||||
}
|
||||
\newenvironment{Shaded}{\begin{ShadedBox}}{\end{ShadedBox}}%
|
||||
}{}
|
||||
\makeatother
|
||||
|
||||
% pandoc syntax-highlight 需要的环境与命令
|
||||
\usepackage{upquote}
|
||||
% pandoc 用法:\begin{Shaded}\begin{Highlighting}[options]...\end{Highlighting}\end{Shaded}
|
||||
% 所以 Highlighting 内部 只做 Verbatim,背景已由外层 Shaded(=ShadedBox tcolorbox)负责
|
||||
\makeatletter
|
||||
\@ifundefined{Highlighting}{%
|
||||
\newenvironment{Highlighting}[1][]{%
|
||||
\VerbatimEnvironment%
|
||||
\begin{Verbatim}[fontsize=\footnotesize,commandchars=\\\{\},#1]%
|
||||
}{%
|
||||
\end{Verbatim}%
|
||||
}%
|
||||
}{}
|
||||
\makeatother
|
||||
|
||||
% ── 表格 / 列表 ────────────────────────────────────────────────────────
|
||||
\usepackage{booktabs}
|
||||
\usepackage{longtable}
|
||||
\usepackage{array}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{makecell}
|
||||
\usepackage{enumitem}
|
||||
\setlist{nosep,leftmargin=*}
|
||||
|
||||
% ── 图 / 链接 ──────────────────────────────────────────────────────────
|
||||
\usepackage{graphicx}
|
||||
\usepackage{float}
|
||||
\usepackage[unicode=true,colorlinks=true,
|
||||
linkcolor=Accent, citecolor=Accent, urlcolor=Accent,
|
||||
pdftitle={PRISM v__PRISM_VERSION__ — Prior-Registered Integrated Spatial Memory},
|
||||
pdfauthor={Project Planning Team},
|
||||
pdfsubject={PRISM v__PRISM_VERSION__, built __PRISM_BUILD_DATE__},
|
||||
pdfkeywords={PRISM, RoomPlan, ZED 2i, SLAM, Spatial Memory, Robot, World Model}]{hyperref}
|
||||
|
||||
% ── 页眉页脚 ──────────────────────────────────────────────────────────
|
||||
\usepackage{fancyhdr}
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\fancyhead[L]{\small\textsf{\textcolor{InkMuted}{PRISM \textbullet{} 机器人大脑空间记忆架构 \textbullet{} v__PRISM_VERSION__}}}
|
||||
\fancyhead[R]{\small\textsf{\textcolor{InkMuted}{\leftmark}}}
|
||||
\fancyfoot[L]{\small\textsf{\textcolor{InkMuted}{Build: __PRISM_BUILD_DATE__}}}
|
||||
\fancyfoot[C]{\small\textsf{\textcolor{InkMuted}{\thepage{} / \pageref*{LastPage}}}}
|
||||
\fancyfoot[R]{\small\textsf{\textcolor{InkMuted}{v__PRISM_VERSION__}}}
|
||||
\renewcommand{\headrulewidth}{0.3pt}
|
||||
\renewcommand{\headrule}{\hbox to\headwidth{\color{GoldRule}\leaders\hrule height \headrulewidth\hfill}}
|
||||
\renewcommand{\footrulewidth}{0pt}
|
||||
|
||||
\usepackage{lastpage}
|
||||
|
||||
% ── 章节样式 ──────────────────────────────────────────────────────────
|
||||
\ctexset{
|
||||
chapter={
|
||||
name={第,章},
|
||||
number=\arabic{chapter},
|
||||
format=\Large\bfseries\sffamily\raggedright,
|
||||
titleformat=\Large\bfseries\sffamily\raggedright,
|
||||
aftername={\hspace{0.6em}},
|
||||
beforeskip=1.2em, afterskip=1.0em
|
||||
},
|
||||
section={
|
||||
format=\large\bfseries\sffamily\raggedright,
|
||||
aftername={\hspace{0.5em}}
|
||||
},
|
||||
subsection={
|
||||
format=\normalsize\bfseries\sffamily\raggedright,
|
||||
},
|
||||
subsubsection={
|
||||
format=\normalsize\sffamily\raggedright,
|
||||
}
|
||||
}
|
||||
|
||||
% ── pandoc 兼容包 ─────────────────────────────────────────────────────
|
||||
\usepackage{calc} % pandoc 表格列宽需要 calc 包
|
||||
\providecommand{\tightlist}{\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
||||
\providecommand{\pandocbounded}[1]{#1}
|
||||
\providecommand{\real}[1]{#1} % pandoc:把 0.4545 原样传入 calc
|
||||
\providecommand{\textquotesingle}{\textsf{'}}
|
||||
% Pandoc 用 \def\LTcaptype{none};需要先定义 none counter 避免 \refstepcounter{none} 报错
|
||||
\newcounter{none}
|
||||
|
||||
% 一些 pandoc highlight 占位 (避免缺命令报错)
|
||||
\providecommand{\NormalTok}[1]{#1}
|
||||
\providecommand{\KeywordTok}[1]{\textcolor{PrismPurple}{\textbf{#1}}}
|
||||
\providecommand{\DataTypeTok}[1]{\textcolor{PrismTeal}{#1}}
|
||||
\providecommand{\DecValTok}[1]{\textcolor{PrismOrange}{#1}}
|
||||
\providecommand{\BaseNTok}[1]{\textcolor{PrismOrange}{#1}}
|
||||
\providecommand{\FloatTok}[1]{\textcolor{PrismOrange}{#1}}
|
||||
\providecommand{\CharTok}[1]{\textcolor{PrismTeal}{#1}}
|
||||
\providecommand{\StringTok}[1]{\textcolor{PrismTeal}{#1}}
|
||||
\providecommand{\CommentTok}[1]{\textcolor{InkMuted}{\textit{#1}}}
|
||||
\providecommand{\OtherTok}[1]{#1}
|
||||
\providecommand{\AlertTok}[1]{\textcolor{PrismRed}{\textbf{#1}}}
|
||||
\providecommand{\FunctionTok}[1]{\textcolor{Accent}{#1}}
|
||||
\providecommand{\RegionMarkerTok}[1]{#1}
|
||||
\providecommand{\ErrorTok}[1]{\textcolor{PrismRed}{\textbf{#1}}}
|
||||
\providecommand{\BuiltInTok}[1]{#1}
|
||||
\providecommand{\ExtensionTok}[1]{#1}
|
||||
\providecommand{\PreprocessorTok}[1]{\textcolor{PrismOrange}{#1}}
|
||||
\providecommand{\AttributeTok}[1]{#1}
|
||||
\providecommand{\InformationTok}[1]{\textcolor{InkMuted}{#1}}
|
||||
\providecommand{\WarningTok}[1]{\textcolor{PrismOrange}{\textbf{#1}}}
|
||||
\providecommand{\AnnotationTok}[1]{\textcolor{InkMuted}{\textit{#1}}}
|
||||
\providecommand{\VariableTok}[1]{#1}
|
||||
\providecommand{\ImportTok}[1]{\textcolor{PrismPurple}{#1}}
|
||||
\providecommand{\OperatorTok}[1]{#1}
|
||||
\providecommand{\ControlFlowTok}[1]{\textcolor{PrismPurple}{\textbf{#1}}}
|
||||
\providecommand{\SpecialCharTok}[1]{#1}
|
||||
\providecommand{\VerbatimStringTok}[1]{\textcolor{PrismTeal}{#1}}
|
||||
\providecommand{\SpecialStringTok}[1]{\textcolor{PrismTeal}{#1}}
|
||||
\providecommand{\ConstantTok}[1]{\textcolor{PrismOrange}{#1}}
|
||||
\providecommand{\DocumentationTok}[1]{\textcolor{InkMuted}{\textit{#1}}}
|
||||
\providecommand{\CommentVarTok}[1]{\textcolor{InkMuted}{\textit{#1}}}
|
||||
|
||||
% pandoc 引文环境
|
||||
\newenvironment{cslreferences}{}{}
|
||||
\newcommand{\citeproctext}{}
|
||||
\newcommand{\citeproc}[2]{#2}
|
||||
|
||||
% emoji / unicode 兜底 (xeCJK 大多直接显示;不显示的用 textsf 代替)
|
||||
\newcommand{\emoji}[1]{\texorpdfstring{#1}{}}
|
||||
|
||||
% ─────────────────────────────────────────────────────────────────────
|
||||
\title{\bfseries PRISM \\[6pt]
|
||||
\large Prior-Registered Integrated Spatial Memory \\[3pt]
|
||||
\normalsize 机器人大脑空间记忆架构 \\[6pt]
|
||||
\small\sffamily\textcolor{InkMuted}{v__PRISM_VERSION__ \textperiodcentered{} __PRISM_BUILD_DATE__}}
|
||||
\author{项目规划组 \textperiodcentered{} Project Planning Team}
|
||||
\date{__PRISM_BUILD_DATE__ \textperiodcentered{} v__PRISM_VERSION__}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\frontmatter
|
||||
\maketitle
|
||||
|
||||
% 简短前言 (一页)
|
||||
\thispagestyle{empty}
|
||||
\vspace*{2em}
|
||||
\begin{tcolorbox}[title={关于本书 / About this Book},
|
||||
colback=BoxBG, colframe=Accent]
|
||||
\textbf{PRISM} (Prior-Registered Integrated Spatial Memory) 是一种把
|
||||
\emph{iPhone RoomPlan 离线先验地图} 与 \emph{ZED 2i 双目+IMU 在线感知}
|
||||
融合进同一个机器人大脑的\textbf{四层空间记忆架构}。
|
||||
本书完整收录 PRISM v1.1 方案的 15 个章节
|
||||
(README + 14 章),含可立即复制运行的 Python schema、ROS 2 节点编排、
|
||||
8 周路线图、风险登记、评测体系、以及一份周末可跑通的最小 MVP。
|
||||
\end{tcolorbox}
|
||||
|
||||
\vfill
|
||||
\noindent\textsf{\textcolor{InkMuted}{\small
|
||||
\textbf{License}: CC BY-NC 4.0 \hfill
|
||||
\textbf{Repo}: \texttt{github.com/<org>/prism}\\
|
||||
\textbf{文档版本}: v1.1 \hfill
|
||||
\textbf{最后更新}: 2026-05-16
|
||||
}}
|
||||
|
||||
\cleardoublepage
|
||||
|
||||
\tableofcontents
|
||||
\cleardoublepage
|
||||
|
||||
\mainmatter
|
||||
|
||||
% ─────────── 正文由 pandoc 注入 ───────────
|
||||
__PRISM_BODY_PLACEHOLDER__
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 PRISM 出版级 PDF 全书
|
||||
# 1. 预处理 14 个 markdown(剥离 emoji、改链接)
|
||||
# 2. pandoc 转 LaTeX body
|
||||
# 3. 注入模板 → XeLaTeX 编译两次(目录)
|
||||
# 4. 用 pypdf 合并 cover + body → PRISM_Whole.pdf
|
||||
#
|
||||
# 用法:
|
||||
# bash plans/PRISM/tools/build_book.sh
|
||||
#
|
||||
# 输出:
|
||||
# plans/PRISM/PRISM_Book.pdf 正文部分
|
||||
# plans/PRISM/PRISM_Whole.pdf 封面 + 正文 完整版
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
PRISM_DIR="$ROOT/plans/PRISM"
|
||||
TOOLS_DIR="$PRISM_DIR/tools"
|
||||
BUILD_DIR="$PRISM_DIR/.build"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# ── 版本与构建日期(注入封面/页眉/章节正文)──────────────────────────
|
||||
if [[ -f "$PRISM_DIR/VERSION" ]]; then
|
||||
VERSION="$(tr -d '[:space:]' < "$PRISM_DIR/VERSION")"
|
||||
else
|
||||
VERSION="0.0.0"
|
||||
fi
|
||||
BUILD_DATE="$(date +'%Y-%m-%d')"
|
||||
echo "==> Building PRISM Book v${VERSION} (${BUILD_DATE})"
|
||||
|
||||
echo "==> 1a) 生成统计章节 16_stats.md"
|
||||
python3 "$TOOLS_DIR/gen_stats.py" "$PRISM_DIR" "$PRISM_DIR/16_stats.md"
|
||||
|
||||
echo "==> 1a') 生成变更日志章节 17_changelog.md (从 CHANGELOG.md)"
|
||||
python3 "$TOOLS_DIR/gen_changelog_chapter.py" \
|
||||
"$PRISM_DIR/CHANGELOG.md" \
|
||||
"$PRISM_DIR/17_changelog.md" \
|
||||
"$VERSION" "$BUILD_DATE"
|
||||
|
||||
echo "==> 1) 预处理 markdown 文件"
|
||||
# 章节顺序
|
||||
CHAPTERS=(
|
||||
"README.md"
|
||||
"00_overview.md"
|
||||
"01_capability_decomposition.md"
|
||||
"02_architecture.md"
|
||||
"03_data_schema.md"
|
||||
"04_pipeline_A_iphone_offline.md"
|
||||
"05_pipeline_B_relocalization.md"
|
||||
"06_pipeline_C_online_perception.md"
|
||||
"07_pipeline_D_consolidation.md"
|
||||
"08_runtime_timeline.md"
|
||||
"09_roadmap.md"
|
||||
"10_tech_stack.md"
|
||||
"11_world_model_bridge.md"
|
||||
"12_risks.md"
|
||||
"13_evaluation.md"
|
||||
"14_mvp.md"
|
||||
"15_glossary.md"
|
||||
"16_stats.md"
|
||||
"18_lyra_inspirations.md"
|
||||
"17_changelog.md"
|
||||
)
|
||||
|
||||
# Python 预处理:把每个 md 的一级标题改成 LaTeX \chapter,
|
||||
# 移除 emoji / 修复链接 / 删除 raw HTML
|
||||
python3 - <<'PYEOF'
|
||||
import re, sys, pathlib
|
||||
PRISM_DIR = pathlib.Path("plans/PRISM")
|
||||
OUT = PRISM_DIR / ".build" / "merged.md"
|
||||
|
||||
# 章节顺序
|
||||
order = [
|
||||
"README.md",
|
||||
"00_overview.md",
|
||||
"01_capability_decomposition.md",
|
||||
"02_architecture.md",
|
||||
"03_data_schema.md",
|
||||
"04_pipeline_A_iphone_offline.md",
|
||||
"05_pipeline_B_relocalization.md",
|
||||
"06_pipeline_C_online_perception.md",
|
||||
"07_pipeline_D_consolidation.md",
|
||||
"08_runtime_timeline.md",
|
||||
"09_roadmap.md",
|
||||
"10_tech_stack.md",
|
||||
"11_world_model_bridge.md",
|
||||
"12_risks.md",
|
||||
"13_evaluation.md",
|
||||
"14_mvp.md",
|
||||
"15_glossary.md",
|
||||
"16_stats.md",
|
||||
"18_lyra_inspirations.md",
|
||||
"17_changelog.md",
|
||||
]
|
||||
|
||||
# 章节显示名(替换原一级标题,使目录整齐)
|
||||
chapter_titles = {
|
||||
"README.md": "项目总览 (README)",
|
||||
"00_overview.md": "第 0 章 总览与命名由来",
|
||||
"01_capability_decomposition.md": "第 1 章 两方案能力解构",
|
||||
"02_architecture.md": "第 2 章 四层空间记忆架构",
|
||||
"03_data_schema.md": "第 3 章 统一数据模型",
|
||||
"04_pipeline_A_iphone_offline.md": "第 4 章 管线 A:iPhone 离线建图",
|
||||
"05_pipeline_B_relocalization.md": "第 5 章 管线 B:ZED 重定位握手",
|
||||
"06_pipeline_C_online_perception.md": "第 6 章 管线 C:在线感知与差异检测",
|
||||
"07_pipeline_D_consolidation.md": "第 7 章 管线 D:记忆巩固",
|
||||
"08_runtime_timeline.md": "第 8 章 端到端运行时序",
|
||||
"09_roadmap.md": "第 9 章 实施路线图",
|
||||
"10_tech_stack.md": "第 10 章 技术栈选型",
|
||||
"11_world_model_bridge.md": "第 11 章 与世界模型衔接",
|
||||
"12_risks.md": "第 12 章 风险与对策",
|
||||
"13_evaluation.md": "第 13 章 评测指标",
|
||||
"14_mvp.md": "第 14 章 最小可复现 demo",
|
||||
"15_glossary.md": "第 15 章 缩略语表",
|
||||
"16_stats.md": "第 16 章 文档与代码统计",
|
||||
"18_lyra_inspirations.md": "第 18 章 Lyra 2.0 启发的设计原则",
|
||||
"17_changelog.md": "第 17 章 变更日志",
|
||||
}
|
||||
|
||||
# 常见 emoji / 装饰符号 → 文字或删除
|
||||
EMOJI_MAP = {
|
||||
"🌈":"", "📱":"", "📷":"", "🤖":"", "🚀":"", "💻":"", "📊":"", "📋":"",
|
||||
"📚":"", "🎓":"", "🎬":"", "🏗️":"", "🏨":"", "💰":"", "📐":"", "🌟":"",
|
||||
"⚠️":"[!]", "✅":"[OK]", "❌":"[X]", "🟢":"[L]", "🟡":"[M]", "🔴":"[H]",
|
||||
"🎯":"", "📈":"", "📞":"", "📍":"", "🗺️":"", "📦":"", "🔧":"", "🤝":"",
|
||||
"📝":"", "🌐":"", "📄":"", "📧":"", "💡":"", "💬":"", "📤":"", "📥":"",
|
||||
"▶":">", "◀":"<", "⭐":"★",
|
||||
"↑":"↑", "↓":"↓",
|
||||
# 第二轮补充:xelatex_pass2.log 中报告的缺失字符
|
||||
"🔬":"", "🧑":"", "💼":"", "🎤":"", "📹":"",
|
||||
"→":"->", "↔":"<->",
|
||||
"①":"(1)", "②":"(2)", "③":"(3)", "④":"(4)",
|
||||
"✓":"[v]",
|
||||
"・":"·",
|
||||
}
|
||||
|
||||
def preprocess(text: str, source_name: str) -> str:
|
||||
# 1) emoji → 替换
|
||||
for k, v in EMOJI_MAP.items():
|
||||
text = text.replace(k, v)
|
||||
# 2) 移除起首 yaml/front matter(不会有,跳过)
|
||||
# 3) 删除 raw HTML 注释 / details
|
||||
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
|
||||
text = re.sub(r"</?details[^>]*>", "", text)
|
||||
text = re.sub(r"</?summary[^>]*>", "", text)
|
||||
# 4) 替换文件名链接 [`xx.md`](path) → \emph{xx.md} 等;
|
||||
# Pandoc 默认会把这些保留为超链接(指向不存在的文件),
|
||||
# 我们把 .md 链接收敛为只显示文本,避免大量"红色断链"。
|
||||
# 直接降级为反引号代码标记,让 pandoc 自己处理转义(它会输出 \texttt{}+下划线转义)
|
||||
def md_link_replace(m):
|
||||
token = m.group(1)
|
||||
# 保留为 inline-code: `xxx`
|
||||
return f"`{token}`"
|
||||
text = re.sub(r"\[`([^`\]]+)`\]\([^)]+\)", md_link_replace, text)
|
||||
# 普通 [text](xxx.md...) 不带反引号的:仅保留文本(纯文本无需转义)
|
||||
text = re.sub(r"\[([^\]]+?)\]\([^)]*\.md[^)]*\)",
|
||||
lambda m: m.group(1), text)
|
||||
# 5) 把第一行 H1 替换为指定章节标题
|
||||
title = chapter_titles.get(source_name, source_name)
|
||||
lines = text.splitlines()
|
||||
for i, l in enumerate(lines):
|
||||
if l.startswith("# "):
|
||||
lines[i] = f"# {title}"
|
||||
break
|
||||
else:
|
||||
lines.insert(0, f"# {title}")
|
||||
# 6) 降级:把 ## 起的所有标题保留;一级 # 后面紧跟的 "## —— xxx" 类副标题
|
||||
# 保持原样,但避免 pandoc 把它认成另一个 chapter
|
||||
out = "\n".join(lines)
|
||||
# 7) 把 horizontal rule 上下的"章节版本/估计阅读时间/关键收获"等元信息
|
||||
# 保留(它们是有用尾页内容)
|
||||
return out
|
||||
|
||||
# 拼接
|
||||
parts = []
|
||||
for fn in order:
|
||||
p = PRISM_DIR / fn
|
||||
if not p.exists():
|
||||
print(f"[WARN] missing {p}")
|
||||
continue
|
||||
body = p.read_text(encoding="utf-8")
|
||||
body = preprocess(body, fn)
|
||||
# 每章前换页
|
||||
parts.append("\n\n\\newpage\n\n" + body + "\n")
|
||||
|
||||
OUT.write_text("\n".join(parts), encoding="utf-8")
|
||||
print(f"[ok] merged → {OUT} ({OUT.stat().st_size/1024:.1f} KB)")
|
||||
PYEOF
|
||||
|
||||
echo "==> 1b) 渲染 mermaid 图块 (mmdc → PNG)"
|
||||
FIG_DIR="$BUILD_DIR/figs"
|
||||
if [[ ! -x "$TOOLS_DIR/node_modules/.bin/mmdc" ]]; then
|
||||
echo "[ERROR] mmdc 未安装,请先执行:"
|
||||
echo " cd $TOOLS_DIR && npm install @mermaid-js/mermaid-cli"
|
||||
exit 1
|
||||
fi
|
||||
python3 "$TOOLS_DIR/render_mermaid.py" \
|
||||
"$BUILD_DIR/merged.md" \
|
||||
"$BUILD_DIR/merged.md" \
|
||||
"$FIG_DIR"
|
||||
|
||||
echo "==> 2) pandoc → LaTeX body"
|
||||
pandoc "$BUILD_DIR/merged.md" \
|
||||
--from=markdown+pipe_tables+grid_tables+raw_tex+task_lists+yaml_metadata_block \
|
||||
--to=latex \
|
||||
--top-level-division=chapter \
|
||||
--listings=false \
|
||||
--wrap=preserve \
|
||||
--highlight-style=tango \
|
||||
-o "$BUILD_DIR/body.tex"
|
||||
|
||||
# 把 body 注入模板,同时把 __PRISM_VERSION__ / __PRISM_BUILD_DATE__ 占位符替换为真实值;
|
||||
# 并对 pandoc 生成的"自然宽度 longtable"做自适应换行后处理。
|
||||
export PRISM_VERSION="$VERSION"
|
||||
export PRISM_BUILD_DATE="$BUILD_DATE"
|
||||
python3 - <<'PYEOF'
|
||||
import os, pathlib, re
|
||||
tpl = pathlib.Path("plans/PRISM/tools/book_template.tex").read_text(encoding="utf-8")
|
||||
body = pathlib.Path("plans/PRISM/.build/body.tex").read_text(encoding="utf-8")
|
||||
|
||||
version = os.environ.get("PRISM_VERSION", "0.0.0")
|
||||
build_date = os.environ.get("PRISM_BUILD_DATE", "unknown")
|
||||
tpl = tpl.replace("__PRISM_VERSION__", version)
|
||||
tpl = tpl.replace("__PRISM_BUILD_DATE__", build_date)
|
||||
|
||||
# pandoc 默认生成 \begin{longtable}[]{@{}lll@{}} 这种自然宽列规范,
|
||||
# 长内容(URL/中文长句/代码)会冲出页面。把所有形如 @{}[lcr]+@{} 的
|
||||
# 自然宽列规范替换为等宽 p{w} 列,启用单元格内换行。
|
||||
LT_HEAD_RE = re.compile(
|
||||
r"\\begin\{longtable\}(\[[^\]]*\])?\{@\{\}([lcr]+)@\{\}\}"
|
||||
)
|
||||
|
||||
def _replace_lt(m):
|
||||
opts = m.group(1) or ""
|
||||
cols = m.group(2)
|
||||
n = len(cols)
|
||||
col_spec = " ".join(
|
||||
r">{\raggedright\arraybackslash}p{(\linewidth - "
|
||||
f"{2*n}" + r"\tabcolsep)/" + f"{n}" + r"}"
|
||||
for _ in range(n)
|
||||
)
|
||||
return f"\\begin{{longtable}}{opts}{{@{{}}{col_spec}@{{}}}}"
|
||||
|
||||
n_fixed = len(LT_HEAD_RE.findall(body))
|
||||
body = LT_HEAD_RE.sub(_replace_lt, body)
|
||||
print(f"[longtable] auto-wrapped {n_fixed} natural-width tables -> p columns")
|
||||
|
||||
out = tpl.replace("__PRISM_BODY_PLACEHOLDER__", body)
|
||||
pathlib.Path("plans/PRISM/.build/book.tex").write_text(out, encoding="utf-8")
|
||||
print("[ok] injected -> plans/PRISM/.build/book.tex")
|
||||
PYEOF
|
||||
|
||||
echo "==> 3) XeLaTeX 编译 (两次,生成目录)"
|
||||
cd "$BUILD_DIR"
|
||||
# 用 -interaction=nonstopmode 跑两遍;第二遍解决 \tableofcontents 引用
|
||||
for i in 1 2; do
|
||||
echo " pass $i ..."
|
||||
xelatex -interaction=nonstopmode -halt-on-error -file-line-error \
|
||||
book.tex > "xelatex_pass${i}.log" 2>&1 || {
|
||||
echo "[ERROR] XeLaTeX pass $i failed; tail of log:"
|
||||
tail -60 "xelatex_pass${i}.log"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
cd - >/dev/null
|
||||
|
||||
cp "$BUILD_DIR/book.pdf" "$PRISM_DIR/PRISM_Book.pdf"
|
||||
echo "[ok] body PDF → $PRISM_DIR/PRISM_Book.pdf ($(du -h "$PRISM_DIR/PRISM_Book.pdf" | cut -f1))"
|
||||
|
||||
echo "==> 4) 合并 cover + body → PRISM_Whole.pdf"
|
||||
python3 - <<PYEOF
|
||||
from pypdf import PdfWriter, PdfReader
|
||||
import pathlib
|
||||
prism = pathlib.Path("plans/PRISM")
|
||||
cover = prism / "PRISM_Cover.pdf"
|
||||
body = prism / "PRISM_Book.pdf"
|
||||
out = prism / "PRISM_Whole.pdf"
|
||||
|
||||
w = PdfWriter()
|
||||
if cover.exists():
|
||||
for p in PdfReader(str(cover)).pages:
|
||||
w.add_page(p)
|
||||
print(f" + cover: {len(PdfReader(str(cover)).pages)} pages")
|
||||
else:
|
||||
print(f" [WARN] cover not found: {cover}")
|
||||
|
||||
# 从正文 PDF 继承 metadata(Title/Author/Subject/Keywords),
|
||||
# 这样合并后的 Whole.pdf 也带 v__VERSION__ 标识。
|
||||
body_reader = PdfReader(str(body))
|
||||
for p in body_reader.pages:
|
||||
w.add_page(p)
|
||||
print(f" + body: {len(body_reader.pages)} pages")
|
||||
|
||||
src_meta = body_reader.metadata or {}
|
||||
w.add_metadata({
|
||||
"/Title": src_meta.get("/Title", "PRISM"),
|
||||
"/Author": src_meta.get("/Author", "Project Planning Team"),
|
||||
"/Subject": src_meta.get("/Subject", "PRISM"),
|
||||
"/Keywords": src_meta.get("/Keywords", "PRISM"),
|
||||
"/Creator": src_meta.get("/Creator", "LaTeX + pypdf"),
|
||||
})
|
||||
|
||||
with open(out, "wb") as f:
|
||||
w.write(f)
|
||||
print(f"[ok] merged → {out} ({out.stat().st_size/1024/1024:.2f} MB)")
|
||||
print(f" Title: {src_meta.get('/Title', '<none>')}")
|
||||
print(f" Subject: {src_meta.get('/Subject', '<none>')}")
|
||||
PYEOF
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ✅ PRISM 全书构建完成"
|
||||
echo "------------------------------------------------------------"
|
||||
echo " 封面: $PRISM_DIR/PRISM_Cover.pdf"
|
||||
echo " 正文: $PRISM_DIR/PRISM_Book.pdf"
|
||||
echo " 整合全本: $PRISM_DIR/PRISM_Whole.pdf"
|
||||
echo "============================================================"
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gen_changelog_chapter.py
|
||||
========================
|
||||
把 CHANGELOG.md 转换为 PRISM 全书的 "第 17 章 变更日志" 章节文件,
|
||||
便于 build_book.sh 把它当作普通章节加入 PDF。
|
||||
|
||||
用法:
|
||||
python3 gen_changelog_chapter.py <changelog.md> <out_md> <version> <build_date>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
|
||||
def main(changelog: pathlib.Path, out_md: pathlib.Path,
|
||||
version: str, build_date: str) -> None:
|
||||
raw = changelog.read_text(encoding="utf-8")
|
||||
|
||||
# 给章节加一个统一的标题(由 build_book.sh 的 chapter_titles 覆盖,
|
||||
# 但这里也写一个 H1 以保证 md 渲染时仍是一个完整章节)。
|
||||
header = "\n".join([
|
||||
"# Chapter 17 — 变更日志 (Changelog)",
|
||||
"",
|
||||
f"> 本章直接镜像项目根的 [`CHANGELOG.md`](CHANGELOG.md);**当前版本 v{version}, 构建于 {build_date}**。"
|
||||
" 每次发版时由 [`tools/gen_changelog_chapter.py`](tools/gen_changelog_chapter.py) 自动同步到 PDF。",
|
||||
"",
|
||||
])
|
||||
|
||||
# 把原 CHANGELOG.md 的 H1 (`# Changelog`) + 顶层引言 + 首个 --- 全部跳过,
|
||||
# 从第一个真正的 ## 二级标题开始拼接,避免重复说明。
|
||||
lines = raw.splitlines()
|
||||
body_lines: list[str] = []
|
||||
in_body = False
|
||||
for ln in lines:
|
||||
if not in_body:
|
||||
# 第一个 ## 之前的所有内容都跳过
|
||||
if ln.startswith("## "):
|
||||
in_body = True
|
||||
body_lines.append(ln)
|
||||
continue
|
||||
body_lines.append(ln)
|
||||
|
||||
full = header + "\n".join(body_lines).rstrip() + "\n"
|
||||
out_md.write_text(full, encoding="utf-8")
|
||||
print(f"[changelog] wrote {out_md} (v{version}, {build_date})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 5:
|
||||
sys.stderr.write(
|
||||
"usage: gen_changelog_chapter.py <changelog.md> <out.md> <version> <build_date>\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
main(
|
||||
pathlib.Path(sys.argv[1]),
|
||||
pathlib.Path(sys.argv[2]),
|
||||
sys.argv[3],
|
||||
sys.argv[4],
|
||||
)
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gen_stats.py
|
||||
============
|
||||
统计 PRISM 全书章节的:
|
||||
- 中文字数 / 英文词数 / 标点数
|
||||
- 各语言代码块行数
|
||||
- Mermaid 图数 / 表格数 / 标题层级数
|
||||
|
||||
生成 16_stats.md(LaTeX longtable 版),供 build_book.sh 注入到 PDF 末尾。
|
||||
|
||||
用法:
|
||||
python3 gen_stats.py <prism_dir> <out_md>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
CJK_RE = re.compile(r"[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]")
|
||||
EN_WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9_'-]*")
|
||||
PUNCT_RE = re.compile(r"[,。;;::!!??、()()\[\]【】「」“”\"']")
|
||||
|
||||
# 章节顺序(同 build_book.sh)
|
||||
ORDER = [
|
||||
"README.md",
|
||||
"00_overview.md",
|
||||
"01_capability_decomposition.md",
|
||||
"02_architecture.md",
|
||||
"03_data_schema.md",
|
||||
"04_pipeline_A_iphone_offline.md",
|
||||
"05_pipeline_B_relocalization.md",
|
||||
"06_pipeline_C_online_perception.md",
|
||||
"07_pipeline_D_consolidation.md",
|
||||
"08_runtime_timeline.md",
|
||||
"09_roadmap.md",
|
||||
"10_tech_stack.md",
|
||||
"11_world_model_bridge.md",
|
||||
"12_risks.md",
|
||||
"13_evaluation.md",
|
||||
"14_mvp.md",
|
||||
"15_glossary.md",
|
||||
"18_lyra_inspirations.md",
|
||||
]
|
||||
|
||||
|
||||
def analyze(text: str) -> dict:
|
||||
"""对一篇 md 做综合统计。先把代码块抠掉,然后再统计正文。"""
|
||||
# 抠出代码块(``` 包围)
|
||||
code_blocks: list[tuple[str, int]] = [] # (lang, line_count)
|
||||
def _strip_code(m: re.Match) -> str:
|
||||
fence_info = m.group(1) or ""
|
||||
body = m.group(2)
|
||||
lang = fence_info.strip().split()[0] if fence_info.strip() else "text"
|
||||
# 行数 = 内容行数(不含闭合 ```)
|
||||
n_lines = body.count("\n")
|
||||
code_blocks.append((lang, n_lines))
|
||||
return "\n[[CODE_REMOVED]]\n"
|
||||
|
||||
body = re.sub(r"```([^\n]*)\n(.*?)\n```", _strip_code, text, flags=re.S)
|
||||
|
||||
# 统计 mermaid 块(已被剥离,从 code_blocks 找)
|
||||
n_mermaid = sum(1 for lang, _ in code_blocks if lang == "mermaid")
|
||||
n_code_total = sum(n for lang, n in code_blocks if lang not in ("mermaid",))
|
||||
|
||||
# 各语言代码行数
|
||||
lang_counter: Counter[str] = Counter()
|
||||
for lang, n in code_blocks:
|
||||
if lang == "mermaid":
|
||||
continue
|
||||
# 归一化
|
||||
lang_norm = lang.lower() if lang else "text"
|
||||
if lang_norm in ("py", "python3"):
|
||||
lang_norm = "python"
|
||||
if lang_norm in ("sh", "shell", "zsh"):
|
||||
lang_norm = "bash"
|
||||
lang_counter[lang_norm] += n
|
||||
|
||||
# 表格行(markdown |...| 风格)
|
||||
n_table_rows = sum(1 for ln in body.splitlines()
|
||||
if re.match(r"^\s*\|.+\|\s*$", ln))
|
||||
|
||||
# 标题
|
||||
n_h1 = sum(1 for ln in body.splitlines() if ln.startswith("# "))
|
||||
n_h2 = sum(1 for ln in body.splitlines() if ln.startswith("## "))
|
||||
n_h3 = sum(1 for ln in body.splitlines() if ln.startswith("### "))
|
||||
|
||||
# 中英标点
|
||||
cjk_chars = len(CJK_RE.findall(body))
|
||||
en_words = len(EN_WORD_RE.findall(body))
|
||||
punct = len(PUNCT_RE.findall(body))
|
||||
|
||||
return dict(
|
||||
cjk_chars=cjk_chars,
|
||||
en_words=en_words,
|
||||
punct=punct,
|
||||
n_mermaid=n_mermaid,
|
||||
n_code_blocks=len(code_blocks) - n_mermaid,
|
||||
n_code_lines=n_code_total,
|
||||
lang_counter=lang_counter,
|
||||
n_table_rows=n_table_rows,
|
||||
n_h1=n_h1,
|
||||
n_h2=n_h2,
|
||||
n_h3=n_h3,
|
||||
raw_lines=text.count("\n"),
|
||||
)
|
||||
|
||||
|
||||
def fmt_int(n: int) -> str:
|
||||
return f"{n:,}"
|
||||
|
||||
|
||||
def main(prism_dir: pathlib.Path, out_md: pathlib.Path) -> None:
|
||||
total = defaultdict(int)
|
||||
total_lang: Counter[str] = Counter()
|
||||
per_chap: list[tuple[str, dict]] = []
|
||||
|
||||
for fn in ORDER:
|
||||
p = prism_dir / fn
|
||||
if not p.exists():
|
||||
continue
|
||||
text = p.read_text(encoding="utf-8")
|
||||
s = analyze(text)
|
||||
per_chap.append((fn, s))
|
||||
for k, v in s.items():
|
||||
if isinstance(v, int):
|
||||
total[k] += v
|
||||
elif isinstance(v, Counter):
|
||||
total_lang.update(v)
|
||||
|
||||
# 生成 markdown(内嵌 LaTeX)
|
||||
lines: list[str] = []
|
||||
lines.append("# Chapter 16 — 文档与代码统计 (Stats)")
|
||||
lines.append("")
|
||||
lines.append("> 本章自动生成,反映本 PRISM 文档集在**构建时**的体量、结构与代码资产。每次 `bash build_book.sh` 都会刷新。")
|
||||
lines.append("")
|
||||
lines.append("## 16.1 总览")
|
||||
lines.append("")
|
||||
# ── 总览表
|
||||
lines.append("\\begin{center}")
|
||||
lines.append("\\begin{tabular}{@{}lr@{}}")
|
||||
lines.append("\\toprule")
|
||||
lines.append("\\textbf{指标} & \\textbf{数值} \\\\")
|
||||
lines.append("\\midrule")
|
||||
lines.append(f"章节数 & {fmt_int(len(per_chap))} \\\\")
|
||||
lines.append(f"中文字数 & {fmt_int(total['cjk_chars'])} \\\\")
|
||||
lines.append(f"英文词数 & {fmt_int(total['en_words'])} \\\\")
|
||||
lines.append(f"标点数 & {fmt_int(total['punct'])} \\\\")
|
||||
lines.append(f"Markdown 行数 & {fmt_int(total['raw_lines'])} \\\\")
|
||||
lines.append(f"一级标题 (\\#) & {fmt_int(total['n_h1'])} \\\\")
|
||||
lines.append(f"二级标题 (\\#\\#) & {fmt_int(total['n_h2'])} \\\\")
|
||||
lines.append(f"三级标题 (\\#\\#\\#) & {fmt_int(total['n_h3'])} \\\\")
|
||||
lines.append(f"Markdown 表格行数 & {fmt_int(total['n_table_rows'])} \\\\")
|
||||
lines.append(f"Mermaid 图块数 & {fmt_int(total['n_mermaid'])} \\\\")
|
||||
lines.append(f"代码块数(非 mermaid) & {fmt_int(total['n_code_blocks'])} \\\\")
|
||||
lines.append(f"代码总行数 & {fmt_int(total['n_code_lines'])} \\\\")
|
||||
lines.append("\\bottomrule")
|
||||
lines.append("\\end{tabular}")
|
||||
lines.append("\\end{center}")
|
||||
lines.append("")
|
||||
|
||||
# ── 各章字数/行数 用宽列章节名(28%)+ 6 个等宽数值列(各 10%)
|
||||
lines.append("## 16.2 各章字数与代码分布")
|
||||
lines.append("")
|
||||
lines.append("\\begin{longtable}{@{}")
|
||||
lines.append(" >{\\raggedright\\arraybackslash}p{0.28\\linewidth}")
|
||||
for _ in range(6):
|
||||
lines.append(" >{\\raggedleft\\arraybackslash}p{0.10\\linewidth}")
|
||||
lines.append("@{}}")
|
||||
lines.append("\\toprule")
|
||||
header = "\\textbf{章节} & \\textbf{中文字} & \\textbf{英文词} & \\textbf{md 行} & \\textbf{mmd 图} & \\textbf{代码块} & \\textbf{代码行} \\\\"
|
||||
lines.append(header)
|
||||
lines.append("\\midrule")
|
||||
lines.append("\\endfirsthead")
|
||||
lines.append("\\toprule")
|
||||
lines.append(header)
|
||||
lines.append("\\midrule")
|
||||
lines.append("\\endhead")
|
||||
lines.append("\\bottomrule")
|
||||
lines.append("\\endfoot")
|
||||
|
||||
def short_label(fn: str) -> str:
|
||||
stem = fn.replace(".md", "")
|
||||
if stem == "README":
|
||||
return "README"
|
||||
m = re.match(r"(\d+)_", stem)
|
||||
if m:
|
||||
return f"ch{m.group(1)}"
|
||||
return stem.replace("_", "\\_")
|
||||
|
||||
for fn, s in per_chap:
|
||||
lines.append(
|
||||
f"{short_label(fn)} & {fmt_int(s['cjk_chars'])} & {fmt_int(s['en_words'])} & "
|
||||
f"{fmt_int(s['raw_lines'])} & {s['n_mermaid']} & {s['n_code_blocks']} & "
|
||||
f"{fmt_int(s['n_code_lines'])} \\\\"
|
||||
)
|
||||
lines.append(
|
||||
f"\\midrule\\textbf{{合计}} & \\textbf{{{fmt_int(total['cjk_chars'])}}} & "
|
||||
f"\\textbf{{{fmt_int(total['en_words'])}}} & \\textbf{{{fmt_int(total['raw_lines'])}}} & "
|
||||
f"\\textbf{{{total['n_mermaid']}}} & \\textbf{{{total['n_code_blocks']}}} & "
|
||||
f"\\textbf{{{fmt_int(total['n_code_lines'])}}} \\\\"
|
||||
)
|
||||
lines.append("\\end{longtable}")
|
||||
lines.append("")
|
||||
lines.append("> 章节短名:`ch00` = `00\\_overview.md`,依此类推;`ch15` 缩略语表,`ch16` 即本章。")
|
||||
lines.append("")
|
||||
|
||||
# ── 各语言代码行数
|
||||
lines.append("## 16.3 代码块语言分布")
|
||||
lines.append("")
|
||||
lines.append("\\begin{center}")
|
||||
lines.append("\\begin{tabular}{@{}lrr@{}}")
|
||||
lines.append("\\toprule")
|
||||
lines.append("\\textbf{语言} & \\textbf{代码块数} & \\textbf{代码行数} \\\\")
|
||||
lines.append("\\midrule")
|
||||
lang_block_counter: Counter[str] = Counter()
|
||||
# 重算块数(分语言)
|
||||
for _, s in per_chap:
|
||||
# 这里需要拿原 lang_counter — 但 analyze() 没把"块数"按语言留下,只留了行数。
|
||||
# 直接重新跑一遍代码块抽取(快速,正则一遍)
|
||||
pass
|
||||
|
||||
# 重新统计每语言的"块数"(独立循环)
|
||||
lang_block_n: Counter[str] = Counter()
|
||||
for fn in ORDER:
|
||||
p = prism_dir / fn
|
||||
if not p.exists():
|
||||
continue
|
||||
for m in re.finditer(r"```([^\n]*)\n(.*?)\n```", p.read_text(encoding="utf-8"), flags=re.S):
|
||||
lang = (m.group(1).strip() or "text").split()[0].lower()
|
||||
if lang in ("py", "python3"):
|
||||
lang = "python"
|
||||
if lang in ("sh", "shell", "zsh"):
|
||||
lang = "bash"
|
||||
if lang == "mermaid":
|
||||
continue
|
||||
lang_block_n[lang] += 1
|
||||
|
||||
for lang, n_lines in total_lang.most_common():
|
||||
lang_safe = lang.replace("_", "\\_")
|
||||
n_blk = lang_block_n.get(lang, 0)
|
||||
lines.append(f"{lang_safe} & {n_blk} & {fmt_int(n_lines)} \\\\")
|
||||
lines.append("\\midrule")
|
||||
lines.append(
|
||||
f"\\textbf{{合计}} & \\textbf{{{sum(lang_block_n.values())}}} & "
|
||||
f"\\textbf{{{fmt_int(sum(total_lang.values()))}}} \\\\"
|
||||
)
|
||||
lines.append("\\bottomrule")
|
||||
lines.append("\\end{tabular}")
|
||||
lines.append("\\end{center}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("**章节版本**:auto-generated ")
|
||||
lines.append("**生成时间**:由 `gen_stats.py` 在每次构建时即时计算 ")
|
||||
lines.append("**关键收获**:文档不只是文字,它本身也是一份可量化的工程产物。")
|
||||
lines.append("")
|
||||
|
||||
out_md.write_text("\n".join(lines), encoding="utf-8")
|
||||
print(f"[stats] wrote {out_md}")
|
||||
print(f" 中文 {total['cjk_chars']:,} 字, 英文 {total['en_words']:,} 词, "
|
||||
f"代码 {total['n_code_lines']:,} 行, mermaid {total['n_mermaid']} 图")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
sys.stderr.write("usage: gen_stats.py <prism_dir> <out_md>\n")
|
||||
sys.exit(1)
|
||||
main(pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]))
|
||||
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Generate the A4 cover PDF for the PRISM project.
|
||||
|
||||
Output: plans/PRISM/PRISM_Cover.pdf
|
||||
|
||||
Layout (A4, 210 × 297 mm):
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 彩色棱镜装饰 (左上 / 右下) │
|
||||
│ │
|
||||
│ 🌈 │
|
||||
│ PRISM │
|
||||
│ Prior-Registered Integrated │
|
||||
│ Spatial Memory │
|
||||
│ ──── 一行 tagline ──── │
|
||||
│ │
|
||||
│ [一句话摘要框] │
|
||||
│ │
|
||||
│ 项目介绍 (3-4 段) │
|
||||
│ │
|
||||
│ 版本 / 日期 / 作者 / 仓库 │
|
||||
└──────────────────────────────────────────┘
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.colors import HexColor, white, Color
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.platypus import Paragraph, Frame
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
||||
|
||||
# ────────────── 元数据 ──────────────
|
||||
PROJECT_NAME = "PRISM"
|
||||
PROJECT_FULL = "Prior-Registered Integrated Spatial Memory"
|
||||
PROJECT_SUBTITLE = "机器人大脑空间记忆架构"
|
||||
TAGLINE = "融合 iPhone 先验地图 与 ZED 2i 在线感知 的四层空间记忆系统"
|
||||
|
||||
ONE_LINER = ("iPhone 是教科书,ZED 2i 是眼睛,"
|
||||
"PRISM 大脑是一名既会查书又会观察、还会在书页空白处做笔记的学生。")
|
||||
|
||||
DESCRIPTION_PARAS = [
|
||||
("<b>PRISM</b> 是一种面向室内服务机器人(酒店 / 家居 / 办公)的"
|
||||
"<b>空间记忆架构</b>。它把<b>偶发的、高质量的离线扫描数据</b>"
|
||||
"(iPhone RoomPlan + LiDAR)与<b>持续的、视锥级的在线感知数据</b>"
|
||||
"(ZED 2i 双目+IMU)融合进同一套<b>四层记忆体系</b>:"
|
||||
"L1 感知缓冲 → L2 度量 → L3 拓扑 → L4 语义,"
|
||||
"并通过 <b>CLIP+ICP 配准</b>与<b>充电时的记忆巩固</b>形成闭环。"),
|
||||
|
||||
("<b>关键创新</b>:(1) 首次把消费级 iPhone RoomPlan 作为先验地图源;"
|
||||
"(2) 分层而非分库,所有数据共享 SpatialMemory 单一根对象;"
|
||||
"(3) 仿人脑睡眠的延迟巩固机制,避免单次错误观测污染长期记忆;"
|
||||
"(4) 通过锚点持续校正 VIO 长时漂移。"),
|
||||
|
||||
("<b>文档结构</b>:15 个 Markdown 文件(README + 14 章),含可立即复制运行的 "
|
||||
"Python schema、ROS 2 节点编排、8 周路线图、国产替代矩阵、"
|
||||
"风险登记表、评测指标、以及一份周末可跑通的最小 MVP。"),
|
||||
|
||||
("<b>预期产出</b>:8 周原型 → 4 个月生产 · MTBF ≥ 48h · "
|
||||
"冷启动重定位成功率 > 90% · 任务成功率 > 90%。"),
|
||||
]
|
||||
|
||||
VERSION = "v1.1"
|
||||
DATE = "2026-05-16"
|
||||
AUTHOR = "项目规划组"
|
||||
LICENSE = "CC BY-NC 4.0"
|
||||
REPO = "github.com/<org>/prism (TBD)"
|
||||
|
||||
# ────────────── 字体注册(macOS 中文) ──────────────
|
||||
def register_chinese_fonts() -> tuple[str, str]:
|
||||
"""Returns (regular_font_name, bold_font_name)."""
|
||||
candidates = [
|
||||
# (regular_path, regular_subfontidx, bold_path, bold_subfontidx, name_base)
|
||||
("/System/Library/Fonts/PingFang.ttc", 2,
|
||||
"/System/Library/Fonts/PingFang.ttc", 5, "PingFang"),
|
||||
("/System/Library/Fonts/STHeiti Light.ttc", 0,
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc", 0, "STHeiti"),
|
||||
("/System/Library/Fonts/Hiragino Sans GB.ttc", 0,
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc", 1, "Hiragino"),
|
||||
]
|
||||
for reg, ri, bold, bi, base in candidates:
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont(f"{base}-R", reg, subfontIndex=ri))
|
||||
pdfmetrics.registerFont(TTFont(f"{base}-B", bold, subfontIndex=bi))
|
||||
return f"{base}-R", f"{base}-B"
|
||||
except Exception as e:
|
||||
print(f" 字体 {base} 注册失败: {e}")
|
||||
continue
|
||||
# 全失败:回退到内置 Helvetica(可能无法显示中文)
|
||||
print(" ⚠️ 未找到中文字体,中文可能显示不全")
|
||||
return "Helvetica", "Helvetica-Bold"
|
||||
|
||||
|
||||
# ────────────── 色彩(棱镜光谱) ──────────────
|
||||
PRISM_COLORS = [
|
||||
HexColor("#E63946"), # red
|
||||
HexColor("#F77F00"), # orange
|
||||
HexColor("#FCBF49"), # yellow
|
||||
HexColor("#2A9D8F"), # teal-green
|
||||
HexColor("#1D7AB8"), # blue
|
||||
HexColor("#5A189A"), # purple
|
||||
]
|
||||
INK_DARK = HexColor("#0E1116")
|
||||
INK_MUTED = HexColor("#4A5560")
|
||||
INK_BG = HexColor("#FBFAF7")
|
||||
ACCENT = HexColor("#1D7AB8")
|
||||
GOLD_LINE = HexColor("#C9A227")
|
||||
|
||||
|
||||
def draw_prism_decoration(c: canvas.Canvas, page_w: float, page_h: float):
|
||||
"""左上角:三角形棱镜分光示意 + 右下角:光谱条带"""
|
||||
# ── 左上 棱镜三角形 + 入射/出射光 ──
|
||||
cx, cy = 30 * mm, page_h - 35 * mm
|
||||
size = 18 * mm
|
||||
# 入射"白光"
|
||||
c.setLineWidth(1.4); c.setStrokeColor(INK_DARK)
|
||||
c.line(cx - size*1.5, cy + 2*mm, cx - 1*mm, cy + 1*mm)
|
||||
# 三角棱镜
|
||||
c.setFillColor(HexColor("#EAEAEA")); c.setStrokeColor(INK_DARK)
|
||||
p = c.beginPath()
|
||||
p.moveTo(cx, cy + size)
|
||||
p.lineTo(cx + size*0.866, cy - size*0.5)
|
||||
p.lineTo(cx - size*0.866, cy - size*0.5)
|
||||
p.close()
|
||||
c.drawPath(p, stroke=1, fill=1)
|
||||
# 七色出射
|
||||
for i, col in enumerate(PRISM_COLORS):
|
||||
c.setStrokeColor(col); c.setLineWidth(1.6)
|
||||
y_off = (i - len(PRISM_COLORS)/2 + 0.5) * 2.0 * mm
|
||||
x_start = cx + size*0.6
|
||||
x_end = cx + size*3.0
|
||||
y_start = cy
|
||||
y_end = cy + y_off
|
||||
c.line(x_start, y_start, x_end, y_end)
|
||||
|
||||
# ── 右下 光谱条带(放在 footer 上方,避免与 footer 文字重叠)──
|
||||
bar_w = 60 * mm
|
||||
bar_h = 3 * mm
|
||||
bx = page_w - 20*mm - bar_w
|
||||
by = 21 * mm # 上移到 meta 与 footer 之间的留白处
|
||||
seg = bar_w / len(PRISM_COLORS)
|
||||
for i, col in enumerate(PRISM_COLORS):
|
||||
c.setFillColor(col); c.setStrokeColor(col)
|
||||
c.rect(bx + i*seg, by, seg, bar_h, fill=1, stroke=0)
|
||||
|
||||
|
||||
def draw_letter_glyphs(c: canvas.Canvas, x: float, y: float,
|
||||
letters: list[tuple[str, str]],
|
||||
font: str, font_size: float, gap: float):
|
||||
"""彩色大字母 P R I S M ,每个字母带颜色,下方写英文 token"""
|
||||
for (letter, _), col, i in zip(letters, PRISM_COLORS, range(len(letters))):
|
||||
# 这里我们让 P R I S M 取前 5 个颜色;Hex 不取第 6 个
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
out = Path(__file__).resolve().parents[1] / "PRISM_Cover.pdf"
|
||||
print(f"→ output: {out}")
|
||||
|
||||
reg, bold = register_chinese_fonts()
|
||||
print(f" fonts: regular={reg}, bold={bold}")
|
||||
|
||||
page_w, page_h = A4
|
||||
c = canvas.Canvas(str(out), pagesize=A4)
|
||||
c.setTitle("PRISM — Prior-Registered Integrated Spatial Memory")
|
||||
c.setAuthor(AUTHOR)
|
||||
c.setSubject(PROJECT_SUBTITLE)
|
||||
c.setKeywords("PRISM, spatial memory, robot, iPhone RoomPlan, ZED 2i, SLAM, world model")
|
||||
c.setCreator("PRISM make_cover_pdf.py")
|
||||
|
||||
# ── 背景纸色 ──
|
||||
c.setFillColor(INK_BG)
|
||||
c.rect(0, 0, page_w, page_h, fill=1, stroke=0)
|
||||
|
||||
# ── 装饰 ──
|
||||
draw_prism_decoration(c, page_w, page_h)
|
||||
|
||||
# ── 顶部小标签 ──
|
||||
c.setFont(reg, 9)
|
||||
c.setFillColor(INK_MUTED)
|
||||
c.drawString(20*mm, page_h - 15*mm, "ROBOT · SPATIAL · MEMORY")
|
||||
c.drawRightString(page_w - 20*mm, page_h - 15*mm,
|
||||
f"VERSION {VERSION} · {DATE}")
|
||||
|
||||
# ── 主标题 PRISM ──
|
||||
title_y = page_h - 75 * mm
|
||||
c.setFont(bold, 78)
|
||||
title = "PRISM"
|
||||
# 等距彩色 5 个字母
|
||||
spectrum5 = PRISM_COLORS[:5]
|
||||
title_total_w = c.stringWidth(title, bold, 78)
|
||||
start_x = (page_w - title_total_w) / 2
|
||||
cur_x = start_x
|
||||
for ch, col in zip(title, spectrum5):
|
||||
c.setFillColor(col)
|
||||
c.drawString(cur_x, title_y, ch)
|
||||
cur_x += c.stringWidth(ch, bold, 78)
|
||||
|
||||
# ── 副标题英文全称 ──
|
||||
c.setFont(bold, 14)
|
||||
c.setFillColor(INK_DARK)
|
||||
sub_en = PROJECT_FULL
|
||||
c.drawCentredString(page_w/2, title_y - 13*mm, sub_en)
|
||||
|
||||
# ── 中文副标题 ──
|
||||
c.setFont(reg, 16)
|
||||
c.setFillColor(INK_DARK)
|
||||
c.drawCentredString(page_w/2, title_y - 23*mm, PROJECT_SUBTITLE)
|
||||
|
||||
# ── 分隔线 + tagline ──
|
||||
rule_y = title_y - 33*mm
|
||||
c.setStrokeColor(GOLD_LINE); c.setLineWidth(0.8)
|
||||
rule_half = 60*mm
|
||||
c.line(page_w/2 - rule_half, rule_y, page_w/2 + rule_half, rule_y)
|
||||
c.setFont(reg, 11); c.setFillColor(INK_MUTED)
|
||||
c.drawCentredString(page_w/2, rule_y - 7*mm, TAGLINE)
|
||||
|
||||
# ── 字母含义注解 P-R-I-S-M ──
|
||||
legend_y = rule_y - 23*mm
|
||||
legend_items = [
|
||||
("P", "Prior"),
|
||||
("R", "Registered"),
|
||||
("I", "Integrated"),
|
||||
("S", "Spatial"),
|
||||
("M", "Memory"),
|
||||
]
|
||||
item_w = 28*mm
|
||||
total_w = item_w * len(legend_items)
|
||||
start_x = (page_w - total_w) / 2
|
||||
for i, (letter, word) in enumerate(legend_items):
|
||||
x = start_x + i*item_w + item_w/2
|
||||
c.setFont(bold, 22); c.setFillColor(PRISM_COLORS[i])
|
||||
c.drawCentredString(x, legend_y, letter)
|
||||
c.setFont(reg, 9); c.setFillColor(INK_DARK)
|
||||
c.drawCentredString(x, legend_y - 6*mm, word)
|
||||
|
||||
# ── 一句话摘要框 ──
|
||||
box_y_top = legend_y - 18*mm
|
||||
box_h = 28*mm
|
||||
box_x = 25*mm
|
||||
box_w = page_w - 50*mm
|
||||
# 浅色背景
|
||||
c.setFillColor(HexColor("#F0EEE6")); c.setStrokeColor(ACCENT)
|
||||
c.setLineWidth(0.6)
|
||||
c.roundRect(box_x, box_y_top - box_h, box_w, box_h, 4*mm, fill=1, stroke=1)
|
||||
# 标签
|
||||
c.setFont(bold, 9); c.setFillColor(ACCENT)
|
||||
c.drawString(box_x + 6*mm, box_y_top - 6*mm, "一句话摘要 · ONE-LINER")
|
||||
# 引言文字
|
||||
quote_style = ParagraphStyle(
|
||||
"quote", fontName=reg, fontSize=12.5, leading=18,
|
||||
textColor=INK_DARK, alignment=TA_LEFT,
|
||||
firstLineIndent=0, leftIndent=0, rightIndent=0)
|
||||
para = Paragraph(f"“{ONE_LINER}”", quote_style)
|
||||
frame = Frame(box_x + 6*mm, box_y_top - box_h + 3*mm,
|
||||
box_w - 12*mm, box_h - 12*mm,
|
||||
showBoundary=0, leftPadding=0, rightPadding=0,
|
||||
topPadding=0, bottomPadding=0)
|
||||
frame.addFromList([para], c)
|
||||
|
||||
# ── 项目介绍正文 ──
|
||||
intro_y_top = box_y_top - box_h - 8*mm
|
||||
intro_x = 25*mm
|
||||
intro_w = page_w - 50*mm
|
||||
intro_h = intro_y_top - 50*mm # 底部留 50mm 给元信息
|
||||
|
||||
body_style = ParagraphStyle(
|
||||
"body", fontName=reg, fontSize=9.5, leading=14,
|
||||
textColor=INK_DARK, alignment=TA_LEFT,
|
||||
spaceAfter=5, firstLineIndent=0)
|
||||
|
||||
# 给底部 meta 块预留 45mm:meta 顶在 45mm,文字行底 40mm,值底 33mm
|
||||
META_TOP = 45 * mm
|
||||
INTRO_BOT = META_TOP + 5*mm # intro frame 底部留 5mm 空白
|
||||
intro_h = intro_y_top - INTRO_BOT
|
||||
|
||||
paragraphs = [Paragraph(p, body_style) for p in DESCRIPTION_PARAS]
|
||||
frame = Frame(intro_x, INTRO_BOT,
|
||||
intro_w, intro_h,
|
||||
showBoundary=0,
|
||||
leftPadding=0, rightPadding=0,
|
||||
topPadding=0, bottomPadding=0)
|
||||
frame.addFromList(paragraphs, c)
|
||||
|
||||
# ── 底部元信息块 ──
|
||||
meta_y = 40 * mm # 标签行
|
||||
val_y = meta_y - 5.5*mm # 值行
|
||||
c.setStrokeColor(INK_MUTED); c.setLineWidth(0.4)
|
||||
c.line(20*mm, META_TOP, page_w - 20*mm, META_TOP)
|
||||
|
||||
meta_items = [
|
||||
("VERSION", VERSION),
|
||||
("DATE", DATE),
|
||||
("AUTHOR", AUTHOR),
|
||||
("LICENSE", LICENSE),
|
||||
]
|
||||
col_w = (page_w - 40*mm) / len(meta_items)
|
||||
for i, (k, v) in enumerate(meta_items):
|
||||
x = 20*mm + i*col_w
|
||||
c.setFont(bold, 8); c.setFillColor(INK_MUTED)
|
||||
c.drawString(x, meta_y, k)
|
||||
c.setFont(reg, 10); c.setFillColor(INK_DARK)
|
||||
c.drawString(x, val_y, v)
|
||||
|
||||
# ── 底部 footer(左:仓库;右:slogan;两者避开右下光谱条所在的 21mm 处)──
|
||||
c.setFont(reg, 7.5); c.setFillColor(INK_MUTED)
|
||||
c.drawString(20*mm, 12*mm,
|
||||
"plans/PRISM/ · 15 documents (README + Ch.00–14)")
|
||||
c.drawString(20*mm, 8*mm,
|
||||
"Repo: " + REPO)
|
||||
# 右侧 slogan 放在光谱条上方(避免覆盖)
|
||||
c.setFont(reg, 8); c.setFillColor(INK_MUTED)
|
||||
c.drawRightString(page_w - 20*mm, 12*mm,
|
||||
"Prism splits perception into memory.")
|
||||
|
||||
c.showPage()
|
||||
c.save()
|
||||
print(f"✅ done: {out} ({out.stat().st_size/1024:.1f} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+4015
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "tools",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mermaid-js/mermaid-cli": "^11.15.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
render_mermaid.py
|
||||
=================
|
||||
扫描合并后的 markdown 中的 ```mermaid 代码块,用 mmdc(@mermaid-js/mermaid-cli)
|
||||
渲染为 PNG,并把这些块替换为可被 pandoc 直接吃下的 LaTeX includegraphics 命令。
|
||||
|
||||
用法(被 build_book.sh 调用):
|
||||
python3 render_mermaid.py <input_md> <out_md> <fig_dir>
|
||||
|
||||
特性:
|
||||
- 内容 hash 缓存,二次构建不重复渲染
|
||||
- 单个块渲染失败不会中断整体构建,会降级为 verbatim 源码 + 警告
|
||||
- 详细错误信息打到 stderr 方便调试
|
||||
|
||||
依赖:
|
||||
mmdc 必须在 plans/PRISM/tools/node_modules/.bin/mmdc
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
TOOLS_DIR = pathlib.Path(__file__).resolve().parent
|
||||
MMDC = TOOLS_DIR / "node_modules" / ".bin" / "mmdc"
|
||||
|
||||
|
||||
def render_one(code: str, fig_dir: pathlib.Path) -> pathlib.Path:
|
||||
"""渲染一个 mermaid 代码块,基于内容 hash 缓存。"""
|
||||
digest = hashlib.sha1(code.encode("utf-8")).hexdigest()[:12]
|
||||
out_png = fig_dir / f"mmd_{digest}.png"
|
||||
if out_png.exists():
|
||||
return out_png
|
||||
|
||||
src_mmd = fig_dir / f"mmd_{digest}.mmd"
|
||||
src_mmd.write_text(code, encoding="utf-8")
|
||||
|
||||
cmd = [
|
||||
str(MMDC),
|
||||
"-i", str(src_mmd),
|
||||
"-o", str(out_png),
|
||||
"-b", "white",
|
||||
"-s", "2",
|
||||
"--width", "1400",
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
# 抽取 mermaid 的 Parse / Lexical error 行
|
||||
err_lines = [ln for ln in (proc.stdout + proc.stderr).splitlines()
|
||||
if "Error" in ln or "error" in ln]
|
||||
err_head = "\n ".join(err_lines[:5]) or "(no error line found)"
|
||||
sys.stderr.write(
|
||||
f"\n[mmdc ERROR] block {digest}:\n"
|
||||
f" {err_head}\n"
|
||||
f" --- mermaid source ---\n{code}\n"
|
||||
f" ----------------------\n"
|
||||
)
|
||||
raise RuntimeError(f"mmdc failed on block {digest}")
|
||||
return out_png
|
||||
|
||||
|
||||
MERMAID_RE = re.compile(
|
||||
r"```mermaid\s*\n(.*?)\n```",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def process(input_md: pathlib.Path, out_md: pathlib.Path, fig_dir: pathlib.Path) -> int:
|
||||
if not MMDC.exists():
|
||||
sys.stderr.write(
|
||||
f"[ERROR] mmdc not found at {MMDC}\n"
|
||||
f" run: cd plans/PRISM/tools && npm install @mermaid-js/mermaid-cli\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
fig_dir.mkdir(parents=True, exist_ok=True)
|
||||
text = input_md.read_text(encoding="utf-8")
|
||||
|
||||
ok = 0
|
||||
fail = 0
|
||||
failures: list[str] = []
|
||||
|
||||
def _sub(m: re.Match) -> str:
|
||||
nonlocal ok, fail
|
||||
code = m.group(1)
|
||||
try:
|
||||
png = render_one(code, fig_dir)
|
||||
except RuntimeError:
|
||||
fail += 1
|
||||
digest = hashlib.sha1(code.encode("utf-8")).hexdigest()[:12]
|
||||
failures.append(digest)
|
||||
# 降级:渲染失败 → 在 PDF 中插入"渲染失败"标记 + 源码 verbatim,
|
||||
# 这样书构建不会因为一个块挂掉而中断。
|
||||
return (
|
||||
f"\n\\begin{{quote}}\\textbf{{[mermaid 渲染失败 {digest}]}}"
|
||||
f"\\end{{quote}}\n\n"
|
||||
f"\\begin{{verbatim}}\n{code}\n\\end{{verbatim}}\n"
|
||||
)
|
||||
ok += 1
|
||||
rel = png.relative_to(input_md.parent)
|
||||
# 同时限制宽 (0.92 linewidth) 与高 (0.82 textheight),
|
||||
# keepaspectratio 自动取较紧者,保证任何极端长宽比的图都不溢出 A4 页面。
|
||||
return (
|
||||
f"\n\\begin{{center}}\n"
|
||||
f"\\includegraphics[width=0.92\\linewidth,"
|
||||
f"height=0.82\\textheight,keepaspectratio]{{{rel.as_posix()}}}\n"
|
||||
f"\\end{{center}}\n"
|
||||
)
|
||||
|
||||
new_text = MERMAID_RE.sub(_sub, text)
|
||||
out_md.write_text(new_text, encoding="utf-8")
|
||||
if fail:
|
||||
print(f"[mermaid] rendered {ok} ok, {fail} FAILED → 降级为 verbatim")
|
||||
print(f"[mermaid] failed digests: {', '.join(failures)}")
|
||||
else:
|
||||
print(f"[mermaid] rendered {ok} block(s) → {fig_dir}")
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
sys.stderr.write(
|
||||
"usage: render_mermaid.py <input_md> <out_md> <fig_dir>\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
process(
|
||||
pathlib.Path(sys.argv[1]),
|
||||
pathlib.Path(sys.argv[2]),
|
||||
pathlib.Path(sys.argv[3]),
|
||||
)
|
||||
Reference in New Issue
Block a user