348 lines
14 KiB
Python
348 lines
14 KiB
Python
import os
|
||
import time
|
||
|
||
import pandas as pd
|
||
from loguru import logger
|
||
from pptx import Presentation
|
||
from pptx.util import Pt
|
||
from rich import box
|
||
from rich.align import Align
|
||
from rich.console import Console
|
||
from rich.panel import Panel
|
||
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
||
from rich.prompt import Prompt
|
||
from rich.table import Table
|
||
|
||
from config.config import load_config
|
||
from utils.agent_utils import generate_comment
|
||
from utils.font_utils import install_fonts_from_directory
|
||
from utils.pef_utils import batch_convert_folder
|
||
from utils.pptx_utils import replace_text_in_slide, replace_picture
|
||
|
||
# 如果你之前没有全局定义 console,这里定义一个
|
||
console = Console()
|
||
|
||
# ==========================================
|
||
# 1. 配置区域 (Configuration)
|
||
# ==========================================
|
||
config = load_config("config.toml")
|
||
|
||
|
||
# ==========================================
|
||
# 2. 业务逻辑函数 (Business Logic)
|
||
# ==========================================
|
||
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, ""))
|
||
replace_picture(prs, 2, "me_image", 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)
|
||
|
||
|
||
# ==========================================
|
||
# 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}")
|
||
|
||
# 每次循环重新加载模版
|
||
prs = Presentation(config["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)
|
||
if os.path.exists(student_image_folder):
|
||
me_image = os.path.join(student_image_folder, "me_image.jpg")
|
||
# 构造信息字典供 helper 使用
|
||
info_dict = {
|
||
"name": name, "english_name": english_name, "sex": sex,
|
||
"birthday": birthday, "zodiac": zodiac, "friend": friend,
|
||
"hobby": hobby, "game": game, "food": food
|
||
}
|
||
replace_three_page(prs, info_dict, me_image)
|
||
else:
|
||
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||
|
||
# --- 页面 4 ---
|
||
class_image_path = os.path.join(config["image_folder"], config["class_name"] + ".jpg")
|
||
if os.path.exists(class_image_path):
|
||
replace_four_page(prs, class_image_path)
|
||
else:
|
||
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
|
||
# 原逻辑中如果班级图片不存在会 continue 跳过保存,这里保持一致
|
||
continue
|
||
|
||
# --- 页面 5 ---
|
||
if os.path.exists(student_image_folder):
|
||
image1 = os.path.join(student_image_folder, "1.jpg")
|
||
image2 = os.path.join(student_image_folder, "2.jpg") # 注意:原代码逻辑也是两张一样的图
|
||
replace_five_page(prs, image1, image2)
|
||
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())
|
||
|
||
|
||
# ==========================================
|
||
# 4. 生成模板(根据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())
|
||
|
||
|
||
# ==========================================
|
||
# 5. 生成评语(根据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, "姓名"] # 获取当前行的姓名
|
||
|
||
# 健壮性处理
|
||
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)
|
||
|
||
# --- 将结果写入 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())
|
||
|
||
|
||
def application():
|
||
from rich.console import Console
|
||
from rich.panel import Panel
|
||
from rich.prompt import Prompt
|
||
from rich.table import Table
|
||
from rich.align import Align
|
||
from rich import box
|
||
import sys
|
||
|
||
console = Console()
|
||
|
||
while True:
|
||
console.clear()
|
||
|
||
# 1. 创建一个表格,不显示表头,使用圆角边框
|
||
table = Table(box=None, show_header=False, padding=(0, 2))
|
||
|
||
# 2. 添加两列:序号列(居右),内容列(居左)
|
||
table.add_column(justify="right", style="cyan bold")
|
||
table.add_column(justify="left")
|
||
|
||
# 3. 添加行内容
|
||
table.add_row("1.", "📁 生成模板")
|
||
table.add_row("2.", "🤖 生成评语")
|
||
table.add_row("3.", "📊 生成报告")
|
||
table.add_row("4.", "📑 格式转换") # 新增
|
||
table.add_row("5.", "🚪 退出系统")
|
||
|
||
# 4. 将表格放入面板,并居中显示
|
||
panel = Panel(
|
||
Align.center(table),
|
||
title="[bold green]🌱 幼儿园成长报告助手",
|
||
subtitle="[dim]By 寒寒",
|
||
width=60,
|
||
border_style="bright_blue",
|
||
box=box.ROUNDED # 圆角边框更柔和
|
||
)
|
||
|
||
# 使用 Align.center 让整个菜单在屏幕中间显示
|
||
console.print(Align.center(panel, vertical="middle"))
|
||
console.print("\n") # 留点空隙
|
||
|
||
choice = Prompt.ask("👉 请输入序号执行", choices=["1", "2", "3", "4", "5"], default="1")
|
||
|
||
try:
|
||
if choice == "1":
|
||
console.rule("[bold cyan]正在执行: 生成模板[/]")
|
||
with console.status("[bold green]正在创建文件夹结构...[/]", spinner="dots"):
|
||
generate_template()
|
||
elif choice == "2":
|
||
console.rule("[bold yellow]正在执行: AI 生成评语[/]")
|
||
# 这里的 generate_comment_all 最好内部有进度条,或者简单的 print
|
||
generate_comment_all()
|
||
elif choice == "3":
|
||
console.rule("[bold blue]正在执行: PPT 合成[/]")
|
||
with console.status("[bold blue]正在处理图片和文字...[/]", spinner="earth"):
|
||
generate_report()
|
||
elif choice == "4":
|
||
console.rule("[bold magenta]正在执行: PDF 批量转换[/]")
|
||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
||
batch_convert_folder(config["output_folder"])
|
||
elif choice == "5":
|
||
console.print("[bold red]👋 再见![/]")
|
||
sys.exit()
|
||
Prompt.ask("按 [bold]Enter[/] 键返回主菜单...")
|
||
except Exception as e:
|
||
console.print(Panel(f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red"))
|
||
Prompt.ask("按 Enter 键继续...")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
application()
|