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()