feat: 初始提交 v1.2.0 - 钢琴练习方案生成系统

This commit is contained in:
hmo
2026-04-21 20:00:33 +08:00
commit fd593bddf4
44 changed files with 10936 additions and 0 deletions
+315
View File
@@ -0,0 +1,315 @@
# PDF生成服务 - 支持中文
import os
import re
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
PageBreak,
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
def register_chinese_font():
"""注册中文字体"""
# 尝试注册系统自带的中文字体
font_paths = [
# Windows常用字体路径
r"C:\Windows\Fonts\simsun.ttc", # 宋体
r"C:\Windows\Fonts\msyh.ttc", # 微软雅黑
r"C:\Windows\Fonts\STSONG.TTF", # 华文宋体
r"C:\Windows\Fonts\simhei.ttf", # 黑体
# Linux容器字体路径
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
# 尝试注册一个可用的字体
for font_path in font_paths:
if os.path.exists(font_path):
try:
pdfmetrics.registerFont(TTFont("Chinese", font_path))
return True
except Exception as e:
continue
return False
# 尝试注册中文字体
_has_chinese_font = register_chinese_font()
def clean_text(text):
"""清理文本,移除不支持的字符"""
if not text:
return ""
# 移除多余空白
text = re.sub(r"\s+", " ", text)
return text.strip()
class PianoPDF:
def __init__(self):
self.elements = []
self.styles = getSampleStyleSheet()
# 创建中文字体样式
if _has_chinese_font:
self.base_font = "Chinese"
else:
self.base_font = "Helvetica"
# 标题样式
self.title_style = ParagraphStyle(
"CustomTitle",
parent=self.styles["Heading1"],
fontName=self.base_font,
fontSize=18,
spaceAfter=10 * mm,
alignment=TA_CENTER,
)
# 副标题样式
self.heading_style = ParagraphStyle(
"CustomHeading",
parent=self.styles["Heading2"],
fontName=self.base_font,
fontSize=14,
spaceAfter=6 * mm,
spaceBefore=6 * mm,
textColor=colors.HexColor("#2c3e50"),
)
# 正文样式
self.body_style = ParagraphStyle(
"CustomBody",
parent=self.styles["Normal"],
fontName=self.base_font,
fontSize=10,
spaceAfter=4 * mm,
leading=14,
)
# 表格样式
self.table_style = TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#3498db")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
("FONTNAME", (0, 0), (-1, 0), self.base_font),
("FONTSIZE", (0, 0), (-1, 0), 10),
("BOTTOMPADDING", (0, 0), (-1, 0), 8),
("BACKGROUND", (0, 1), (-1, -1), colors.white),
("FONTNAME", (0, 1), (-1, -1), self.base_font),
("FONTSIZE", (0, 1), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
(
"ROWBACKGROUNDS",
(0, 1),
(-1, -1),
[colors.white, colors.HexColor("#f8f9fa")],
),
]
)
def add_title(self, text):
"""添加标题"""
self.elements.append(Paragraph(clean_text(text), self.title_style))
self.elements.append(Spacer(1, 5 * mm))
def add_heading(self, text):
"""添加副标题"""
self.elements.append(Paragraph(clean_text(text), self.heading_style))
def add_paragraph(self, text):
"""添加段落"""
if text:
self.elements.append(Paragraph(clean_text(text), self.body_style))
self.elements.append(Spacer(1, 3 * mm))
def add_list(self, items):
"""添加列表"""
for item in items:
if item:
self.elements.append(
Paragraph(f"{clean_text(item)}", self.body_style)
)
self.elements.append(Spacer(1, 3 * mm))
def add_table(self, data):
"""添加表格"""
if not data or len(data) < 2:
return
# 确保所有数据都是字符串
table_data = []
for row in data:
table_data.append([clean_text(str(cell)) for cell in row])
if table_data:
table = Table(table_data)
table.setStyle(self.table_style)
self.elements.append(table)
self.elements.append(Spacer(1, 5 * mm))
def add_spacer(self, height=5 * mm):
"""添加间距"""
self.elements.append(Spacer(1, height))
def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=None):
"""
使用reportlab生成PDF文件
Args:
plan_id: 方案ID
student_name: 学员姓名
content: 方案内容字典
output_dir: 输出目录
rendered_report: 可选的预渲染报告(Markdown格式),如果提供则使用模板渲染整个报告
Returns:
str: PDF文件路径
"""
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, f"plan_{plan_id}.pdf")
# 创建PDF文档
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
rightMargin=20 * mm,
leftMargin=20 * mm,
topMargin=20 * mm,
bottomMargin=20 * mm,
)
pdf = PianoPDF()
# 如果提供了预渲染的报告,使用模板渲染整个报告
if rendered_report:
# 解析Markdown并添加到PDF
lines = rendered_report.split('\n')
in_table = False
table_data = []
for line in lines:
line = line.strip()
if not line or line.startswith('---'):
if in_table and table_data:
pdf.add_table(table_data)
table_data = []
in_table = False
continue
if line.startswith('# ') and not line.startswith('## '):
# 一级标题作为主标题
title = line.replace('# ', '').replace('*', '')
pdf.add_title(title)
elif line.startswith('## '):
# 二级标题
if in_table and table_data:
pdf.add_table(table_data)
table_data = []
in_table = False
heading = line.replace('## ', '').replace('**', '')
pdf.add_heading(heading)
elif line.startswith('|') and '|' in line[1:]:
# 表格行
in_table = True
cells = [c.strip() for c in line.split('|')[1:-1]]
if cells and any(cells):
# 跳过表头分隔行
if not all(c.strip().startswith('-') for c in cells if c.strip()):
table_data.append(cells)
elif line.startswith('- '):
# 列表项
pdf.add_paragraph(line)
elif line and not in_table:
# 普通段落
pdf.add_paragraph(line.replace('**', '').replace('*', ''))
# 表格收尾
if in_table and table_data:
pdf.add_table(table_data)
else:
# 使用原有结构化方式生成PDF
# 标题
pdf.add_title(f"钢琴练习方案 - {student_name}")
# 学员信息
pdf.add_heading("📋 学员信息")
pdf.add_paragraph(f"学员姓名:{student_name}")
pdf.add_paragraph(f"每日练习时间:{content.get('practice_time', 'N/A')}")
pdf.add_paragraph(f"生成时间:{content.get('generated_at', '')}")
# 问题诊断
pdf.add_heading("🔍 问题诊断")
for problem in content.get("problems", []):
name = problem.get("name", "")
severity = problem.get("severity", "")
focus = problem.get("focus", {})
pdf.add_paragraph(
f"{name} ({severity}) - 基础练习: {focus.get('basic', 0)}分钟, 技术练习: {focus.get('tech', 0)}分钟"
)
# AI报告(如果有)
if content.get("ai_report"):
pdf.add_heading("📝 AI个性化练习报告")
# 将AI报告分段添加
for line in content["ai_report"].split("\n"):
line = line.strip()
if line.startswith("## "):
pdf.add_heading(line.replace("## ", ""))
elif line.startswith("# "):
pdf.add_title(line.replace("# ", ""))
elif line.startswith("- "):
pdf.add_paragraph(line)
elif line:
pdf.add_paragraph(line)
# 每日练习计划
pdf.add_heading("📅 每日练习计划")
total_minutes = content.get("total_daily_minutes", 0)
pdf.add_paragraph(f"总时长:{total_minutes} 分钟")
# 表格
table_data = [["阶段", "时长", "内容", "目的"]]
for item in content.get("daily_schedule", []):
table_data.append(
[
item.get("phase", ""),
item.get("duration", ""),
item.get("content", ""),
item.get("purpose", ""),
]
)
pdf.add_table(table_data)
# 练习建议
pdf.add_heading("💡 练习建议")
tips = [
"每天在固定时间练习,养成良好习惯",
"练习前先热身,放松手指",
"先难后易,先练习重点部分",
"录音回听,发现问题",
"保持耐心,进步需要时间",
]
pdf.add_list(tips)
# 生成PDF
doc.build(pdf.elements)
return output_path