初始化医疗报告生成项目,添加核心代码文件
This commit is contained in:
267
backend/services/pdf_service.py
Normal file
267
backend/services/pdf_service.py
Normal file
@@ -0,0 +1,267 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
class PDFService:
|
||||
"""PDF报告生成服务"""
|
||||
|
||||
def __init__(self):
|
||||
# 模板目录
|
||||
self.template_dir = Path(__file__).parent.parent / "templates"
|
||||
self.template_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 输出目录
|
||||
self.output_dir = Path(__file__).parent.parent / "generated_reports"
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 初始化Jinja2环境
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(str(self.template_dir)))
|
||||
|
||||
def generate_report(
|
||||
self,
|
||||
filename: str,
|
||||
analysis: Dict[str, Any],
|
||||
llm_type: str = "Coze Workflow"
|
||||
) -> str:
|
||||
"""
|
||||
生成PDF报告
|
||||
|
||||
Args:
|
||||
filename: 原始文件名
|
||||
analysis: 分析结果
|
||||
llm_type: 使用的LLM类型
|
||||
|
||||
Returns:
|
||||
生成的PDF文件路径
|
||||
"""
|
||||
try:
|
||||
# 准备模板数据
|
||||
template_data = self._prepare_template_data(filename, analysis, llm_type)
|
||||
|
||||
# 渲染HTML
|
||||
html_content = self._render_html(template_data)
|
||||
|
||||
# 生成PDF
|
||||
pdf_path = self._generate_pdf(html_content, filename)
|
||||
|
||||
return pdf_path
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"PDF生成失败: {str(e)}")
|
||||
|
||||
def _prepare_template_data(
|
||||
self,
|
||||
filename: str,
|
||||
analysis: Dict[str, Any],
|
||||
llm_type: str
|
||||
) -> Dict[str, Any]:
|
||||
"""准备模板数据"""
|
||||
# 处理 key_findings
|
||||
key_findings = analysis.get("key_findings", [])
|
||||
if key_findings:
|
||||
# 如果是对象数组,提取文本
|
||||
key_findings = [
|
||||
item.get("finding", item.get("text", str(item)))
|
||||
if isinstance(item, dict) else str(item)
|
||||
for item in key_findings
|
||||
]
|
||||
|
||||
# 处理 abnormal_items
|
||||
abnormal_items = analysis.get("abnormal_items", [])
|
||||
if abnormal_items:
|
||||
processed_items = []
|
||||
for item in abnormal_items:
|
||||
if isinstance(item, dict):
|
||||
processed_items.append(item)
|
||||
else:
|
||||
processed_items.append({"name": str(item)})
|
||||
abnormal_items = processed_items
|
||||
|
||||
# 处理 risk_assessment
|
||||
risk_assessment = analysis.get("risk_assessment", "未提供")
|
||||
if isinstance(risk_assessment, dict):
|
||||
# 如果是对象,转换为文本
|
||||
parts = []
|
||||
if risk_assessment.get("high_risk"):
|
||||
parts.append(f"【高风险】{'; '.join(risk_assessment['high_risk'])}")
|
||||
if risk_assessment.get("medium_risk"):
|
||||
parts.append(f"【中风险】{'; '.join(risk_assessment['medium_risk'])}")
|
||||
if risk_assessment.get("low_risk"):
|
||||
parts.append(f"【低风险】{'; '.join(risk_assessment['low_risk'])}")
|
||||
risk_assessment = "\n".join(parts) if parts else "未检测到明确风险"
|
||||
|
||||
# 处理 recommendations
|
||||
recommendations = analysis.get("recommendations", [])
|
||||
if recommendations:
|
||||
recommendations = [
|
||||
item.get("recommendation", item.get("text", str(item)))
|
||||
if isinstance(item, dict) else str(item)
|
||||
for item in recommendations
|
||||
]
|
||||
|
||||
return {
|
||||
"filename": filename,
|
||||
"analysis_date": datetime.now().strftime("%Y年%m月%d日"),
|
||||
"llm_type": llm_type,
|
||||
"summary": analysis.get("summary", "暂无摘要"),
|
||||
"key_findings": key_findings,
|
||||
"abnormal_items": abnormal_items,
|
||||
"risk_assessment": risk_assessment,
|
||||
"recommendations": recommendations,
|
||||
"generation_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def _render_html(self, template_data: Dict[str, Any]) -> str:
|
||||
"""渲染HTML模板"""
|
||||
template = self.jinja_env.get_template("report_template.html")
|
||||
return template.render(**template_data)
|
||||
|
||||
def _generate_pdf(self, html_content: str, original_filename: str) -> str:
|
||||
"""将HTML转换为PDF"""
|
||||
try:
|
||||
# 生成PDF文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
base_name = Path(original_filename).stem
|
||||
pdf_filename = f"{base_name}_分析报告_{timestamp}.pdf"
|
||||
pdf_path = self.output_dir / pdf_filename
|
||||
|
||||
# 尝试使用 WeasyPrint(推荐,质量更好)
|
||||
try:
|
||||
from weasyprint import HTML, CSS
|
||||
HTML(string=html_content).write_pdf(
|
||||
str(pdf_path),
|
||||
stylesheets=[CSS(string='@page { size: A4; margin: 1cm; }')]
|
||||
)
|
||||
except ImportError:
|
||||
# 降级到 xhtml2pdf(更简单,无需额外依赖)
|
||||
print(" WeasyPrint 未安装,使用 xhtml2pdf 生成PDF...")
|
||||
from xhtml2pdf import pisa
|
||||
with open(pdf_path, "wb") as pdf_file:
|
||||
pisa_status = pisa.CreatePDF(html_content, dest=pdf_file)
|
||||
if pisa_status.err:
|
||||
raise Exception("xhtml2pdf 生成失败")
|
||||
|
||||
return str(pdf_path)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"PDF转换失败: {str(e)}")
|
||||
|
||||
def get_pdf_file(self, pdf_path: str) -> bytes:
|
||||
"""读取PDF文件内容"""
|
||||
if not os.path.exists(pdf_path):
|
||||
raise FileNotFoundError("PDF文件不存在")
|
||||
|
||||
with open(pdf_path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def generate_comprehensive_report(
|
||||
self,
|
||||
patient_name: str,
|
||||
template_data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
生成综合健康报告(多份报告整合)
|
||||
|
||||
Args:
|
||||
patient_name: 患者姓名
|
||||
template_data: 包含所有报告数据和分析结果的字典
|
||||
|
||||
Returns:
|
||||
生成的PDF文件路径
|
||||
"""
|
||||
try:
|
||||
# 准备综合报告模板数据
|
||||
comprehensive_data = self._prepare_comprehensive_data(patient_name, template_data)
|
||||
|
||||
# 渲染HTML(使用综合报告模板)
|
||||
html_content = self._render_comprehensive_html(comprehensive_data)
|
||||
|
||||
# 生成PDF
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
pdf_filename = f"{patient_name}_综合健康报告_{timestamp}.pdf"
|
||||
pdf_path = self.output_dir / pdf_filename
|
||||
|
||||
try:
|
||||
from weasyprint import HTML, CSS
|
||||
HTML(string=html_content).write_pdf(
|
||||
str(pdf_path),
|
||||
stylesheets=[CSS(string='@page { size: A4; margin: 1cm; }')]
|
||||
)
|
||||
except ImportError:
|
||||
# 如果 WeasyPrint 不可用,使用 xhtml2pdf
|
||||
from xhtml2pdf import pisa
|
||||
with open(pdf_path, "wb") as pdf_file:
|
||||
pisa_status = pisa.CreatePDF(html_content, dest=pdf_file)
|
||||
if pisa_status.err:
|
||||
raise Exception("xhtml2pdf 生成失败")
|
||||
|
||||
return str(pdf_path)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"综合报告生成失败: {str(e)}")
|
||||
|
||||
def _prepare_comprehensive_data(
|
||||
self,
|
||||
patient_name: str,
|
||||
template_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""准备综合报告模板数据"""
|
||||
analysis = template_data.get("analysis", {})
|
||||
|
||||
# 处理分析结果(与单报告相同的逻辑)
|
||||
key_findings = analysis.get("key_findings", [])
|
||||
if key_findings:
|
||||
key_findings = [
|
||||
item.get("finding", item.get("text", str(item)))
|
||||
if isinstance(item, dict) else str(item)
|
||||
for item in key_findings
|
||||
]
|
||||
|
||||
abnormal_items = analysis.get("abnormal_items", [])
|
||||
if abnormal_items:
|
||||
processed_items = []
|
||||
for item in abnormal_items:
|
||||
if isinstance(item, dict):
|
||||
processed_items.append(item)
|
||||
else:
|
||||
processed_items.append({"name": str(item)})
|
||||
abnormal_items = processed_items
|
||||
|
||||
risk_assessment = analysis.get("risk_assessment", "未提供")
|
||||
if isinstance(risk_assessment, dict):
|
||||
parts = []
|
||||
if risk_assessment.get("high_risk"):
|
||||
parts.append(f"【高风险】{'; '.join(risk_assessment['high_risk'])}")
|
||||
if risk_assessment.get("medium_risk"):
|
||||
parts.append(f"【中风险】{'; '.join(risk_assessment['medium_risk'])}")
|
||||
if risk_assessment.get("low_risk"):
|
||||
parts.append(f"【低风险】{'; '.join(risk_assessment['low_risk'])}")
|
||||
risk_assessment = "\n".join(parts) if parts else "未检测到明确风险"
|
||||
|
||||
recommendations = analysis.get("recommendations", [])
|
||||
if recommendations:
|
||||
recommendations = [
|
||||
item.get("recommendation", item.get("text", str(item)))
|
||||
if isinstance(item, dict) else str(item)
|
||||
for item in recommendations
|
||||
]
|
||||
|
||||
return {
|
||||
"patient_name": patient_name,
|
||||
"report_count": template_data.get("report_count", 0),
|
||||
"report_list": template_data.get("report_list", []),
|
||||
"generation_date": template_data.get("generation_date", datetime.now().strftime("%Y年%m月%d日")),
|
||||
"summary": analysis.get("summary", "暂无摘要"),
|
||||
"key_findings": key_findings,
|
||||
"abnormal_items": abnormal_items,
|
||||
"risk_assessment": risk_assessment,
|
||||
"recommendations": recommendations,
|
||||
"generation_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def _render_comprehensive_html(self, template_data: Dict[str, Any]) -> str:
|
||||
"""渲染综合报告HTML模板"""
|
||||
template = self.jinja_env.get_template("comprehensive_report_template.html")
|
||||
return template.render(**template_data)
|
||||
Reference in New Issue
Block a user