1.模板填充功能的实现
下面是一个整体的感觉
def fill_template_with_ai(self, lesson_data: Dict[str, Any]) -> Dict[str, Any]:
"""
遍历所有单元格,若内容为字段名,则将其右侧所有合并单元格都写入AI内容,彻底解决合并单元格错位和内容丢失问题。
"""
try:
logger.info("开始AI填充自定义Word模板(右侧合并单元格填充)...")
template_path = os.path.join(os.path.dirname(__file__), '备课教学教案设计.docx')
doc = Document(template_path)
tables = doc.tables
# 需要的字段,顺序与AI输出一致
fields = [
"教学课题", "授课教师", "授课课型", "授课时间",
"知识与技能", "过程与方法", "情感态度与价值观",
"教学重点", "教学难点", "教学方法", "教学准备",
"教学过程", "作业设计", "板书设计", "教学反思"
]
logger.info(f"目标字段名: {fields}")
# 构建AI prompt,要求输出所有字段内容
prompt = (
f"请根据如下教案信息,生成详细的教案内容,依次包含如下部分:{fields}。\n"
f"每个部分用【字段名】单独一行作为标题,下一行紧跟内容。\n"
f"每个字段内容要详细展开,不少于3句话,包含具体描述、示例或步骤。\n"
f"不要加粗、不要编号、不要markdown符号、不要'详细说明'子标题。\n"
f"多项内容用分号或换行分隔。\n"
f"不要输出任何多余的格式或解释。\n"
f"对于教学目标部分,分为【知识与技能】【过程与方法】【情感态度与价值观】分别输出,每项内容详细展开。\n"
f"教案信息:{lesson_data}"
)
messages = [
{"role": "system", "content": "你是专业教案设计AI,输出结构化教案内容,每部分用【字段名】作为标题。"},
{"role": "user", "content": prompt}
]
response = self.client.chat.completions.create(
model="deepseek-reasoner",
messages=messages,
temperature=0.2,
max_tokens=4096,
timeout=60
)
ai_raw_content = getattr(response.choices[0].message, 'content', None) if response.choices else None
logger.info(f"AI接口content字段: {ai_raw_content}")
# 用正则提取各字段内容
ai_json = {}
for field in fields:
# 允许前后有0~2个星号和空格
pattern = rf'\*{{0,2}}[ \t]*【{field}】\*{{0,2}}[ \t]*\n*([\s\S]*?)(?=\n*\*{{0,2}}[ \t]*【|$)'
match = re.search(pattern, ai_raw_content or '', re.MULTILINE)
if match:
# 去除每行行首的*、-、-和多余空格
value = match.group(1)
value = '\n'.join([re.sub(r'^[\*\--\s]+', '', l) for l in value.splitlines()])
ai_json[field] = value.strip()
else:
ai_json[field] = ''
logger.info(f"自解析提取内容: {ai_json}")
# 遍历所有表格和单元格,右侧所有合并单元格精准填充
debug_map = {}
for table in tables:
for row in table.rows:
for idx, cell in enumerate(row.cells):
key = cell.text.replace('\n', '').replace(' ', '').strip()
if key in ai_json and ai_json[key]:
# 只填充第一个右侧空单元格,防止合并单元格内容丢失
for j in range(idx + 1, len(row.cells)):
if not row.cells[j].text.strip():
row.cells[j].text = ai_json[key]
debug_map[f"{key}_col{j}"] = ai_json[key]
break # 只填充一次
logger.info(f"最终填充字段及内容: {debug_map}")
# 保存新文档
file_id = uuid.uuid4().hex[:8]
topic = lesson_data.get('topic', '教案')
filename = f"{topic}_AI模板_{file_id}.docx"
doc_path = os.path.join(self.upload_folder, filename)
doc.save(doc_path)
logger.info(f"AI模板教案生成成功:{doc_path}")
return {
"file_path": doc_path,
"filename": filename,
"status": "success",
"debug_map": debug_map,
"fields": fields,
"ai_json": ai_json,
"ai_raw_content": ai_raw_content
}
except Exception as e:
logger.error(f"AI模板教案生成失败: {str(e)}", exc_info=True)
return {
"error": str(e),
"status": "error"
}
下面来仔细讲解一下,给予特殊的prompt
需要的字段,顺序与AI输出一致
fields = [
"教学课题", "授课教师", "授课课型", "授课时间",
"知识与技能", "过程与方法", "情感态度与价值观",
"教学重点", "教学难点", "教学方法", "教学准备",
"教学过程", "作业设计", "板书设计", "教学反思"
]
logger.info(f"目标字段名: {fields}")
# 构建AI prompt,要求输出所有字段内容
prompt = (
f"请根据如下教案信息,生成详细的教案内容,依次包含如下部分:{fields}。\n"
f"每个部分用【字段名】单独一行作为标题,下一行紧跟内容。\n"
f"每个字段内容要详细展开,不少于3句话,包含具体描述、示例或步骤。\n"
f"不要加粗、不要编号、不要markdown符号、不要'详细说明'子标题。\n"
f"多项内容用分号或换行分隔。\n"
f"不要输出任何多余的格式或解释。\n"
f"对于教学目标部分,分为【知识与技能】【过程与方法】【情感态度与价值观】分别输出,每项内容详细展开。\n"
f"教案信息:{lesson_data}"
)
正则表达式等来处理文法,有点梦回编译原理与web数据开发
# 构建AI prompt,要求输出所有字段内容
prompt = (
f"请根据如下教案信息,生成详细的教案内容,依次包含如下部分:{fields}。\n"
f"每个部分用【字段名】单独一行作为标题,下一行紧跟内容。\n"
f"每个字段内容要详细展开,不少于3句话,包含具体描述、示例或步骤。\n"
f"不要加粗、不要编号、不要markdown符号、不要'详细说明'子标题。\n"
f"多项内容用分号或换行分隔。\n"
f"不要输出任何多余的格式或解释。\n"
f"对于教学目标部分,分为【知识与技能】【过程与方法】【情感态度与价值观】分别输出,每项内容详细展开。\n"
f"教案信息:{lesson_data}"
)
使用正则表达式等,对模板进行填充,是一个需要非常仔细细心的过程。
2.Markdown转word的实现
pandoc可以完成多种文件类型的相互转换,支持的文件格式参见官方网站:Pandoc - index。本博客参考了官方文档以及社区讨论,将介绍如何利用pandoc将markdown转换为word文档。
def generate_lesson_plan_word(self, lesson_data: Dict[str, Any], plain_text: bool = False) -> dict:
"""生成Word格式的教案,支持纯文本和富格式两种模式"""
try:
logger.info("开始生成Word格式教案...")
filename = f"lesson_plan_{uuid.uuid4().hex[:8]}.docx"
file_path = os.path.join(self.upload_folder, filename)
doc = Document()
self._setup_document_styles(doc)
# 根据plain_text参数选择prompt和处理方式
enhanced_data = self._enhance_with_deepseek(lesson_data, force_plaintext=plain_text)
ai_content = enhanced_data.get('ai_enhanced_content')
if not ai_content:
error_msg = enhanced_data.get('ai_error', 'AI生成的内容为空,请检查输入内容或稍后重试')
logger.error(error_msg)
raise ValueError(error_msg)
if plain_text:
# 纯文本模式,直接分段写入
for para in ai_content.split('\n'):
if para.strip():
doc.add_paragraph(para.strip())
elif lesson_data.get('isRichText'):
# 富文本模式,使用pandoc将markdown转换为Word文档
import subprocess
import tempfile
with tempfile.NamedTemporaryFile(suffix='.md', delete=False) as temp_md:
temp_md.write(ai_content.encode('utf-8'))
temp_md_path = temp_md.name
try:
subprocess.run(['pandoc', temp_md_path, '-o', file_path, '--reference-doc=template.docx'], check=True)
logger.info(f"使用pandoc将markdown转换为Word文档成功:{file_path}")
except subprocess.CalledProcessError as e:
logger.error(f"pandoc转换失败: {str(e)}")
raise
finally:
os.unlink(temp_md_path)
else:
# 其它情况,原有简单清洗
lines = ai_content.split('\n')
for line in lines:
clean_line = re.sub(r'^[#>*\-\s]+', '', line).strip()
if clean_line:
doc.add_paragraph(clean_line)
doc.save(file_path)
logger.info(f"Word文档已保存到: {file_path}")
return {"filename": filename, "file_path": file_path, "enhanced_content": enhanced_data}
except Exception as e:
logger.error(f"生成Word文档失败: {str(e)}")
raise
- 富文本模式:当isRichText为True时,系统会使用pandoc将生成的markdown内容转换为Word文档。
- 临时文件:系统会创建一个临时的markdown文件,将AI生成的内容写入该文件。
- pandoc转换:调用pandoc命令将markdown文件转换为Word文档,并指定参考文档为template.docx。
- 错误处理:如果pandoc转换失败,系统会记录错误并抛出异常。
- 清理临时文件:转换完成后,系统会删除临时markdown文件。