chore: initial commit — import worldmodel workspace (plans/, research/)

This commit is contained in:
gaojie
2026-05-20 21:43:57 +08:00
commit bec8a9a4a3
98 changed files with 44128 additions and 0 deletions
+275
View File
@@ -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}
+318
View File
@@ -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],
)
+271
View File
@@ -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]))
+324
View File
@@ -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 · "
"冷启动重定位成功率 &gt; 90% · 任务成功率 &gt; 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"&ldquo;{ONE_LINER}&rdquo;", 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.0014)")
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()
+4015
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -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"
}
}
+132
View File
@@ -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]),
)