fix:添加UI界面,完善功能

This commit is contained in:
2025-12-11 11:16:09 +08:00
parent 81e3c40abb
commit f437842a81
17 changed files with 939 additions and 406 deletions

View File

@@ -10,12 +10,13 @@ from config.config import load_config
config = load_config("config.toml")
def generate_comment(name, age_group, traits):
def generate_comment(name, age_group, traits,sex):
"""
生成评语
:param name: 学生姓名
:param age_group: 所在班级
:param traits: 表现特征
:param sex: 性别
:return: 评语
"""
@@ -29,7 +30,7 @@ def generate_comment(name, age_group, traits):
# 2. 构建 Prompt Template
prompt = ChatPromptTemplate.from_messages([
("system", ai_config["prompt"]),
("human", "学生姓名:{name}\n所在班级:{age_group}\n表现特征:{traits}\n\n请开始撰写评语:")
("human", "学生姓名:{name}\n所在班级:{age_group}\n性别:{sex}\n表现特征:{traits}\n\n请开始撰写评语:")
])
# 3. 组装链 (Prompt -> Model -> OutputParser)
@@ -40,11 +41,12 @@ def generate_comment(name, age_group, traits):
comment = chain.invoke({
"name": name,
"age_group": age_group,
"traits": traits
"traits": traits,
"sex": sex
})
cleaned_text = re.sub(r'\s+', '', comment)
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
return cleaned_text
except Exception as e:
print(f"生成评语失败: {e}")
logger.error(f"生成评语失败: {e}")
return "生成失败请检查网络或Key。"

121
utils/file_utils.py Normal file
View File

@@ -0,0 +1,121 @@
import shutil
import os
import time
from loguru import logger
def export_data_folder(source_folder="data", output_folder="backup"):
"""
将指定文件夹压缩为 zip 包
:param source_folder: 要压缩的文件夹路径 (默认 'data')
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
"""
try:
# 1. 检查源文件夹是否存在
if not os.path.exists(source_folder):
logger.error(f"备份失败: 找不到源文件夹 '{source_folder}'")
return
# 2. 确保输出目录存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
logger.info(f"创建备份目录: {output_folder}")
# 3. 生成带时间戳的文件名 (例如: data_backup_20251211_103000)
timestamp = time.strftime("%Y%m%d_%H%M%S")
# 注意: make_archive 不需要写后缀 .zip它会自动加
base_name = os.path.join(output_folder, f"data_备份_{timestamp}")
logger.info(f"正在压缩 '{source_folder}' ...")
# 4. 执行压缩
# format="zip": 压缩格式
# root_dir: 要压缩的根目录的上级目录 (通常填 None)
# base_dir: 要压缩的具体目录
zip_path = shutil.make_archive(
base_name=base_name,
format="zip",
root_dir=os.path.dirname(
os.path.abspath(source_folder)
), # 这里为了安全,定位到父级
base_dir=os.path.basename(
os.path.abspath(source_folder)
), # 只压缩 data 文件夹本身
)
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
return zip_path
except Exception as e:
logger.error(f"❌ 备份出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
def initialize_project(root_dir="."):
"""
初始化项目:清空 data重建目录复制模板
:param root_dir: 项目根目录
"""
# 定义路径
data_dir = os.path.join(root_dir, "data")
images_dir = os.path.join(data_dir, "images")
output_dir = os.path.join(root_dir, "output")
# 模板源文件路径
template_dir = os.path.join(root_dir, "templates")
src_excel = os.path.join(template_dir, "names.xlsx")
dst_excel = os.path.join(data_dir, "names.xlsx")
logger.info("开始初始化/重置项目...")
# --- 1. 清空 data 文件夹 ---
if os.path.exists(data_dir):
try:
# 暴力删除整个 data 文件夹及其内容
shutil.rmtree(data_dir)
logger.info(f"已清理旧数据: {data_dir}")
except PermissionError:
logger.error(
f"❌ 删除失败!请检查 '{data_dir}' 下的文件(如 Excel是否正在被打开。"
)
return
except Exception as e:
logger.error(f"❌ 删除出错: {e}")
return
# --- 2. 清空 output_dir 文件夹 ---
if os.path.exists(output_dir):
try:
# 暴力删除整个 output 文件夹及其内容
shutil.rmtree(output_dir)
logger.info(f"已清理旧数据: {output_dir}")
except PermissionError:
logger.error(f"❌ 删除失败!请检查 '{output_dir}' 下的文件是否正在被打开。")
return
except Exception as e:
logger.error(f"❌ 删除出错: {e}")
return
# --- 2. 新建目录结构 ---
try:
# makedirs 会自动创建父级目录 (data 和 data/images 都会被创建)
os.makedirs(images_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
logger.info(f"✅ 已创建目录: {images_dir}")
except Exception as e:
logger.error(f"创建目录失败: {e}")
return
# --- 3. 复制模板文件 ---
if os.path.exists(src_excel):
try:
shutil.copy(src_excel, dst_excel)
logger.success(f"✅ 成功从模板恢复: {dst_excel}")
logger.success("🎉 初始化完成!一切已恢复出厂设置。")
except Exception as e:
logger.error(f"复制模板文件失败: {e}")
else:
logger.warning(
f"⚠️ 警告: 模板文件不存在 ({src_excel})data 文件夹内将没有 Excel 文件。"
)

358
utils/generate_utils.py Normal file
View File

@@ -0,0 +1,358 @@
import os
import time
import pandas as pd
from loguru import logger
from pptx import Presentation
from rich.console import Console
import comtypes.client
from config.config import load_config
from utils.agent_utils import generate_comment
from utils.font_utils import install_fonts_from_directory
from utils.image_utils import find_image_path
from utils.zodiac_utils import calculate_zodiac
from utils.growt_utils import replace_one_page, replace_two_page, replace_three_page, replace_four_page, replace_five_page
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
# ==========================================
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ==========================================
def generate_template():
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# --- 修改点开始 ---
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
datas = df["姓名"].values.tolist()
# --- 修改点结束 ---
logger.info(f"开始生成学生模版文件,共 {len(datas)} 位学生...")
# 3. 循环处理
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
for i, name in enumerate(datas):
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 确保 name 是字符串且去除了空格 (增加健壮性)
name = str(name).strip()
student_folder = os.path.join(config["image_folder"], name)
if os.path.exists(student_folder):
logger.info(f"学生图片文件夹已存在 {student_folder}")
else:
logger.info(f"正在生成学生图片文件夹 {student_folder}")
os.makedirs(student_folder, exist_ok=True)
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 2. 生成评语(根据names.xlsx文件生成评价)
# ==========================================
def generate_comment_all():
try:
# 1. 读取数据
excel_path = config["excel_file"]
df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 检查是否存在"评价"列,不存在则新建(防止报错)
if "评价" not in df.columns:
df["评价"] = ""
# --- 获取总行数,用于日志 ---
# 强制将“评价”列转换为 object 类型
total_count = len(df)
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
df["评价"] = df["评价"].astype("object")
# --- 遍历 DataFrame 的索引 (index) ---
# 这样我们可以通过索引 i 精准地把数据写回某一行
for i in df.index:
name = df.at[i, "姓名"] # 获取当前行的姓名
sex = df.at[i,"性别"]
if pd.isna(sex):
sex = ""
else:
sex = str(sex).strip()
# 健壮性处理
if pd.isna(name): continue # 跳过空行
name = str(name).strip()
# 获取当前行的特征如果Excel里有“特征”这一列就读没有就用默认值
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
traits = df.at[i, "表现特征"] if "表现特征" in df.columns and not pd.isna(
df.at[i, "表现特征"]) else "有礼貌、守纪律"
# 优化如果“评价”列已经有内容了跳过不生成节省API费用
current_comment = df.at[i, "评价"]
if not pd.isna(current_comment) and str(current_comment).strip() != "":
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
continue
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
try:
# 调用你的生成函数,并【接收返回值】
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
generated_text = generate_comment(name, config["age_group"], traits,sex)
# --- 将结果写入 DataFrame ---
df.at[i, "评价"] = generated_text
logger.success(f"学生:{name},评语生成完毕")
# 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失
if (i + 1) % 5 == 0:
df.to_excel(excel_path, index=False)
logger.info("--- 阶段性保存成功 ---")
time.sleep(1) # 避免触发API速率限制
except Exception as e:
logger.error(f"学生:{name},生成评语出错: {str(e)}")
# --- 修改点 4: 循环结束后最终保存文件 ---
# index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列
df.to_excel(excel_path, index=False)
logger.success(f"所有评语已生成并写入文件:{excel_path}")
except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 3. 生成成长报告(根据names.xlsx文件生成)
# ==========================================
def generate_report():
# 1. 资源准备
if install_fonts_from_directory(config["fonts_dir"]):
logger.info("等待系统识别新安装的字体...")
time.sleep(2)
os.makedirs(config["output_folder"], exist_ok=True)
if not os.path.exists(config["source_file"]):
logger.info(f"错误: 找不到模版文件 {config['source_file']}")
return
if not os.path.exists(config["excel_file"]):
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
return
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# 确保列名对应
columns = ["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
"评价"]
datas = df[columns].values.tolist()
teacher_names_str = " ".join(config["teachers"])
logger.info(f"开始处理,共 {len(datas)} 位学生...")
# 3. 循环处理
for i, row_data in enumerate(datas):
# 解包数据
(name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) = row_data
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 每次循环重新加载模版
template_source_file = os.path.join("templates", config["source_file"])
prs = Presentation(template_source_file)
# --- 页面 1 ---
replace_one_page(prs, name, config["class_name"])
# --- 页面 2 ---
replace_two_page(prs, comments, teacher_names_str)
# --- 页面 3 ---
student_image_folder = os.path.join(config["image_folder"], name)
logger.info(f"学生图片文件夹: {student_image_folder}")
if os.path.exists(student_image_folder):
me_image_path = find_image_path(student_image_folder, "me_image")
# 构造信息字典供 helper 使用
info_dict = {
"name": name, "english_name": english_name, "sex": sex,
"birthday": birthday, "zodiac": zodiac, "friend": friend,
"hobby": hobby, "game": game, "food": food
}
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
if me_image_path and isinstance(me_image_path, str) and os.path.exists(me_image_path):
logger.info(f"学生:{name},图片路径有效: {me_image_path},正在替换...")
replace_three_page(prs, info_dict, me_image_path)
else:
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
replace_three_page(prs, info_dict, None)
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 页面 4 ---
class_image_path = find_image_path(config["image_folder"], config["class_name"])
if os.path.exists(class_image_path):
replace_four_page(prs, class_image_path)
else:
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
# --- 页面 5 ---
if os.path.exists(student_image_folder):
img1_path = find_image_path(student_image_folder, "1")
img2_path = find_image_path(student_image_folder, "2")
# 逻辑优化:
# 情况A: 两张都找到了 -> 正常插入
if img1_path and img2_path:
replace_five_page(prs, img1_path, img2_path)
# 情况B: 只找到了 1 -> 两张图都用 1 (避免报错)
elif img1_path and not img2_path:
replace_five_page(prs, img1_path, img1_path)
# 情况C: 一张都没找到
else:
logger.warning(f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]")
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 保存文件 ---
file_ext = os.path.splitext(config["source_file"])[1]
safe_name = str(name).strip()
new_filename = f"{config['class_name']} {safe_name} 幼儿成长报告{file_ext}"
output_path = os.path.join(config["output_folder"], new_filename)
try:
prs.save(output_path)
logger.success(f"学生:{name},保存成功: {new_filename}")
except PermissionError:
logger.error(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
logger.success("所有报告生成完毕!")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ==========================================
def batch_convert_folder(folder_path):
"""
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快)
"""
folder_path = os.path.abspath(folder_path)
if not os.path.exists(folder_path):
print("文件夹不存在")
return
# 获取所有 ppt/pptx 文件
files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.ppt', '.pptx'))]
if not files:
print("没有找到 PPT 文件")
return
print(f"发现 {len(files)} 个文件,准备开始转换...")
powerpoint = None
try:
# 1. 启动应用 (只启动一次)
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
# 某些环境下需要设为可见,否则无法运行
# powerpoint.Visible = 1
for filename in files:
ppt_path = os.path.join(folder_path, filename)
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
# 如果 PDF 已存在,可以选择跳过
if os.path.exists(pdf_path):
print(f"[跳过] 已存在: {filename}")
continue
print(f"正在转换: {filename} ...")
try:
# 打开 -> 另存为 -> 关闭
deck = powerpoint.Presentations.Open(ppt_path)
deck.SaveAs(pdf_path, 32)
deck.Close()
except Exception as e:
print(f"文件 {filename} 转换出错: {e}")
except Exception as e:
print(f"PowerPoint 进程出错: {e}")
finally:
# 2. 退出应用
if powerpoint:
powerpoint.Quit()
print("PowerPoint 已关闭,批量转换完成。")
# ==========================================
# 5. 生成属相(根据names.xlsx文件生成属相)
# ==========================================
def generate_zodiac():
try:
# 1. 读取数据
excel_path = config["excel_file"]
# sheet_name 根据实际情况修改,如果不确定可以用 sheet_name=0 读取第一个
df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 2. 检查必要的列
date_column = '生日'
target_column = '属相'
if date_column not in df.columns:
logger.error(f"Excel中找不到列名{date_column}】,请检查表头。")
return
# 检查是否存在"属相"列,不存在则新建
if target_column not in df.columns:
df[target_column] = ""
# --- 获取总行数,用于日志 ---
total_count = len(df)
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
# 3. 数据清洗与计算
temp_dates = pd.to_datetime(df[date_column], errors='coerce')
df[target_column] = temp_dates.apply(calculate_zodiac)
# 5. 保存结果
save_path = excel_path
try:
df.to_excel(save_path, index=False)
logger.success(f"所有属相已更新并写入文件:{save_path}")
logger.warning(f"请检查文件 {save_path} 修改日期格式。")
except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}")
except FileNotFoundError:
logger.error(f"找不到文件 {config.get('excel_file')}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())

46
utils/growt_utils.py Normal file
View File

@@ -0,0 +1,46 @@
from rich.console import Console
from loguru import logger
from config.config import load_config
from utils.pptx_utils import replace_text_in_slide, replace_picture
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
def replace_one_page(prs, name, class_name):
"""替换第一页信息"""
replace_text_in_slide(prs, 0, "name", name)
replace_text_in_slide(prs, 0, "class", class_name)
def replace_two_page(prs, comments, teacher_name):
"""替换第二页信息"""
replace_text_in_slide(prs, 1, "comments", comments)
replace_text_in_slide(prs, 1, "teacher_name", teacher_name)
def replace_three_page(prs, info_dict, me_image):
"""替换第三页信息"""
# 使用字典解包传递多个字段,减少参数数量
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]
for field in fields:
replace_text_in_slide(prs, 2, field, info_dict.get(field, ""))
if me_image:
replace_picture(prs, 2, "me_image", me_image)
else:
logger.warning(f"⚠️ 警告: {info_dict.get('name', '未知姓名')} 缺少个人照片('me_image'")
def replace_four_page(prs, class_image):
"""替换第四页信息"""
replace_picture(prs, 3, "class_image", class_image)
def replace_five_page(prs, image1, image2):
"""替换第五页信息"""
replace_picture(prs, 4, "image1", image1)
replace_picture(prs, 4, "image2", image2)

42
utils/pdf_utils.py Normal file
View File

@@ -0,0 +1,42 @@
import os
import comtypes.client
def ppt_to_pdf_single(ppt_path, pdf_path=None):
"""
单个 PPT 转 PDF
:param ppt_path: PPT 文件路径
:param pdf_path: PDF 输出路径 (可选,默认同名)
"""
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
if pdf_path is None:
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
pdf_path = os.path.abspath(pdf_path)
if not os.path.exists(ppt_path):
print(f"文件不存在: {ppt_path}")
return False
powerpoint = None
try:
# 启动 PowerPoint 应用
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
powerpoint.Visible = 1 # 设为可见,否则某些版本会报错
# 打开演示文稿
deck = powerpoint.Presentations.Open(ppt_path)
# 保存为 PDF (文件格式代码 32 代表 PDF)
deck.SaveAs(pdf_path, 32)
deck.Close()
print(f"转换成功: {pdf_path}")
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
finally:
if powerpoint:
powerpoint.Quit()

View File

@@ -1,95 +0,0 @@
import os
import comtypes.client
def ppt_to_pdf_single(ppt_path, pdf_path=None):
"""
单个 PPT 转 PDF
:param ppt_path: PPT 文件路径
:param pdf_path: PDF 输出路径 (可选,默认同名)
"""
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
if pdf_path is None:
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
pdf_path = os.path.abspath(pdf_path)
if not os.path.exists(ppt_path):
print(f"文件不存在: {ppt_path}")
return False
powerpoint = None
try:
# 启动 PowerPoint 应用
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
powerpoint.Visible = 1 # 设为可见,否则某些版本会报错
# 打开演示文稿
deck = powerpoint.Presentations.Open(ppt_path)
# 保存为 PDF (文件格式代码 32 代表 PDF)
deck.SaveAs(pdf_path, 32)
deck.Close()
print(f"转换成功: {pdf_path}")
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
finally:
if powerpoint:
powerpoint.Quit()
def batch_convert_folder(folder_path):
"""
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快)
"""
folder_path = os.path.abspath(folder_path)
if not os.path.exists(folder_path):
print("文件夹不存在")
return
# 获取所有 ppt/pptx 文件
files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.ppt', '.pptx'))]
if not files:
print("没有找到 PPT 文件")
return
print(f"发现 {len(files)} 个文件,准备开始转换...")
powerpoint = None
try:
# 1. 启动应用 (只启动一次)
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
# 某些环境下需要设为可见,否则无法运行
# powerpoint.Visible = 1
for filename in files:
ppt_path = os.path.join(folder_path, filename)
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
# 如果 PDF 已存在,可以选择跳过
if os.path.exists(pdf_path):
print(f"[跳过] 已存在: {filename}")
continue
print(f"正在转换: {filename} ...")
try:
# 打开 -> 另存为 -> 关闭
deck = powerpoint.Presentations.Open(ppt_path)
deck.SaveAs(pdf_path, 32)
deck.Close()
except Exception as e:
print(f"文件 {filename} 转换出错: {e}")
except Exception as e:
print(f"PowerPoint 进程出错: {e}")
finally:
# 2. 退出应用
if powerpoint:
powerpoint.Quit()
print("PowerPoint 已关闭,批量转换完成。")

30
utils/zodiac_utils.py Normal file
View File

@@ -0,0 +1,30 @@
import pandas as pd
def calculate_zodiac(birth_date):
"""
根据出生日期推断生肖 (基于公历年份简单算法)
逻辑1900年是鼠年以此为基准进行模运算
"""
if pd.isna(birth_date):
return "未知"
# 确保是 datetime 对象,方便提取年份
try:
birth_date = pd.to_datetime(birth_date)
except:
return "格式错误"
year = birth_date.year
# 生肖列表,顺序必须固定:鼠、牛、虎...
zodiacs = ['', '', '', '', '', '', '', '', '', '', '', '']
# 算法核心:(年份 - 1900) % 12 得到索引
# 1900年是鼠年索引为0
index = (year - 1900) % 12
return zodiacs[index]
# --- 主程序 ---
# 1. 读取 Excel 文件
file_path = 'names.xlsx' # 你的文件名