Files
worldmodel/plans/PRISM/tools/make_cover_pdf.py
T

325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()