import os import re from docx import Document from docx.shared import Pt, Inches, Cm, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn def generate_person_doc(person_name: str, person_info: str, photo_path: str, stories: list[dict], output_path: str): """ 为一位讲述者生成最终 DOCX 文档 person_info: 个人信息文本 photo_path: 照片路径(可为空) stories: [{title, content}] """ doc = Document() # 设置默认字体 style = doc.styles['Normal'] font = style.font font.name = '宋体' font.size = Pt(12) style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') # 照片 if photo_path and os.path.exists(photo_path): try: doc.add_picture(photo_path, width=Inches(1.5)) last_paragraph = doc.paragraphs[-1] last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER except Exception: pass # 姓名 name_para = doc.add_paragraph() name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER name_run = name_para.add_run(person_name) name_run.font.size = Pt(18) name_run.font.bold = True name_run.font.name = '黑体' name_run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') # 个人信息 if person_info: info_para = doc.add_paragraph() info_para.alignment = WD_ALIGN_PARAGRAPH.CENTER info_run = info_para.add_run(person_info.strip()) info_run.font.size = Pt(11) info_run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) # 分隔线 doc.add_paragraph("─" * 30).alignment = WD_ALIGN_PARAGRAPH.CENTER # 故事 for i, story in enumerate(stories): # 故事标题 title_para = doc.add_paragraph() title_run = title_para.add_run(story.get("title", f"故事 {i + 1}")) title_run.font.size = Pt(14) title_run.font.bold = True title_run.font.name = '黑体' title_run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') # 故事梗概 summary = story.get("summary", "") if summary: p = doc.add_paragraph() run = p.add_run("【故事梗概】") run.font.size = Pt(11) run.font.bold = True run.font.color.rgb = RGBColor(0x33, 0x33, 0x99) p = doc.add_paragraph(summary.strip()) p.paragraph_format.first_line_indent = Cm(0.74) p.paragraph_format.line_spacing = 1.5 for run in p.runs: run.font.size = Pt(11) # 关键词标签 tags = story.get("tags", []) if tags: p = doc.add_paragraph() run = p.add_run("【关键词标签】") run.font.size = Pt(11) run.font.bold = True run.font.color.rgb = RGBColor(0x33, 0x33, 0x99) tag_text = "、".join(tags) p = doc.add_paragraph(tag_text) p.paragraph_format.line_spacing = 1.5 for run in p.runs: run.font.size = Pt(10) run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) # 故事内容(支持 Markdown 格式) content = story.get("content", "") if content: _add_markdown_content(doc, content) # 多个故事之间加分隔 if i < len(stories) - 1: doc.add_paragraph("─" * 30).alignment = WD_ALIGN_PARAGRAPH.CENTER # 保存 os.makedirs(os.path.dirname(output_path), exist_ok=True) doc.save(output_path) return output_path def _add_markdown_content(doc, content: str): """将 Markdown 格式的内容添加到 DOCX,支持标题、引用、列表等""" lines = content.split("\n") i = 0 while i < len(lines): line = lines[i].strip() if not line: i += 1 continue # ## 二级标题 if line.startswith("## "): title = line[3:].strip() p = doc.add_paragraph() run = p.add_run(title) run.font.size = Pt(13) run.font.bold = True run.font.name = '黑体' run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') p.paragraph_format.space_before = Pt(12) # ### 三级标题 elif line.startswith("### "): title = line[4:].strip() p = doc.add_paragraph() run = p.add_run(title) run.font.size = Pt(12) run.font.bold = True run.font.name = '黑体' run.element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') p.paragraph_format.space_before = Pt(6) # > 引用 elif line.startswith("> "): quote_text = line[2:].strip() p = doc.add_paragraph() p.paragraph_format.left_indent = Cm(1) run = p.add_run(quote_text) run.font.italic = True run.font.color.rgb = RGBColor(0x55, 0x55, 0x55) run.font.size = Pt(11) # - 列表项 elif line.startswith("- "): item_text = line[2:].strip() p = doc.add_paragraph(style='List Bullet') run = p.add_run(item_text) run.font.size = Pt(12) # 普通段落 else: p = doc.add_paragraph(line) p.paragraph_format.first_line_indent = Cm(0.74) p.paragraph_format.line_spacing = 1.5 i += 1