diff --git a/config.toml b/config.toml index 94d2fd2..2a136a8 100644 --- a/config.toml +++ b/config.toml @@ -1,26 +1,26 @@ [paths] -source_file = "大二班 幼儿学期发展报告.pptx" +source_file = "大四班 幼儿学期发展报告.pptx" output_folder = "output" excel_file = "names.xlsx" image_folder = "images" fonts_dir = "fonts" -signature_image = "signature.png" +signature_image = "C:\\Users\\Administrator\\Desktop\\文档资料\\code\\growth_report\\data\\" [class_info] -class_name = "K4B" +class_name = "K4D" teachers = [ - "李垚", - "廖琴琴", - "余鸿仙", + "康璐璐", + "冯宇阳", + "孙继艳", ] -class_type = 1 +class_type = 2 [defaults] default_comment = "" age_group = "大班上学期" [ai] -api_key = "sk-8b0c9522df8843b4d0e7e91ecb628957" +api_key = "sk-ccb6a1445126f2e56ba50d7622ff350f" api_url = "https://apis.iflow.cn/v1/chat/completions" model = "deepseek-v3.2" prompt = "# Role \n你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意,擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的,能让家长读完后感到欣慰并对未来充满希望。\n\n# Goal\n请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。\n# Constraints & Rules\n1. **严格的格式排版 (Strict Formatting)**:\n- **换行**:正文中间不要随意换行,保持为一段完整的段落。\n2. **称呼处理**:\n- 自动识别用户输入的姓名,去掉姓氏。\n- 例如:“王小明” -> 第一行输出“小明宝贝:”。\n3. **分龄侧重 (根据 Age_Group 调整侧重点)**:\n- **小班 (3-4岁)**:侧重于适应集体生活、情绪稳定性、基本生活自理能力、愿意与老师互动。\n- **中班 (4-5岁)**:侧重于社交互动、分享与合作、动手能力、好奇心、规则意识。\n- **大班 (5-6岁)**:侧重于学习习惯、逻辑思维、领导力、任务意识、幼小衔接准备。\n4. **写作结构 (固定内容)**:\n- **开头**:固定文本必须包含:“{class_type}”\n- **正文**:结合【表现关键词】和【性别】,具体描述进步和优点。\n- **结尾**:委婉地提出期望(“如果你能...老师会更为你骄傲”),并送上祝福。\n5. **语气风格**:\n- 积极正面,多用肯定句。\n- 字数控制在 150-250 字之间。\n# Input Format\n- Name {{name}}\n- Age_Group {{class_name}}\n- Traits {{traits}}\n- Sex {{sex}}\n# Output Example\n(假设输入:Name=张图图, Age_Group=小班, Traits=适应能力强, 爱笑, 挑食,Sex=女)\n亲爱的图图宝贝:{class_type}\n你是一个爱笑的小天使,每天早上都能看到你甜甜的笑脸。从一开始的哭鼻子到现在能开心地参与游戏,你的适应能力让老师感到惊喜。不过,老师发现你在吃饭时偶尔会把不喜欢的青菜挑出来哦。如果你能和青菜宝宝做好朋友,把身体练得棒棒的,那就更完美啦!祝可爱的图图宝贝新年快乐,健康成长!\n" diff --git a/config/config.py b/config/config.py index c2cd97f..9058cd9 100644 --- a/config/config.py +++ b/config/config.py @@ -18,24 +18,27 @@ except ImportError: # 如果没安装,提供一个 fallback 提示 toml_write = None + def get_base_dir(): - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) else: # 假设当前文件在项目根目录或根目录下的某个文件夹中 return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + def get_resource_path(relative_path): base_path = get_base_dir() external_path = os.path.join(base_path, relative_path) if os.path.exists(external_path): return external_path - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): internal_path = os.path.join(sys._MEIPASS, relative_path) if os.path.exists(internal_path): return internal_path return external_path + # ========================================== # 1. 配置加载 (Config Loader) # ========================================== @@ -44,7 +47,7 @@ def load_config(config_filename="config.toml"): if not os.path.exists(config_path): # 如果彻底找不到,返回一个最小化的默认值,防止程序奔溃 - return { "source_file": "", "ai": {"api_key": ""}, "teachers": [] } + return {"source_file": "", "ai": {"api_key": ""}, "teachers": []} try: with open(config_path, "rb") as f: @@ -59,20 +62,32 @@ def load_config(config_filename="config.toml"): config = { "root_path": base_dir, + "data_folder": os.path.join(os.path.join("data")), # 扁平化映射 - "source_file": get_resource_path(os.path.join("templates", paths.get("source_file", ""))), - "excel_file": get_resource_path(os.path.join("data", paths.get("excel_file", ""))), - "image_folder": get_resource_path(os.path.join("data", paths.get("image_folder", ""))), + "source_file": get_resource_path( + os.path.join("templates", paths.get("source_file", "")) + ), + "excel_file": get_resource_path( + os.path.join("data", paths.get("excel_file", "")) + ), + "image_folder": get_resource_path( + os.path.join("data", paths.get("image_folder", "")) + ), "fonts_dir": get_resource_path(paths.get("fonts_dir", "fonts")), - "output_folder": os.path.join(base_dir, paths.get("output_folder", "output")), - "signature_image": get_resource_path(os.path.join("data", paths.get("signature_image", ""))), - + "output_folder": os.path.join( + base_dir, paths.get("output_folder", "output") + ), + "signature_image": get_resource_path( + os.path.join("data", paths.get("signature_image", "")) + ), "class_name": class_info.get("class_name", "未命名班级"), "teachers": class_info.get("teachers", []), "class_type": class_info.get("class_type", 0), "default_comment": defaults.get("default_comment", "暂无评语"), "age_group": defaults.get("age_group", "大班上学期"), - "ai": data.get("ai", {"api_key": "", "api_url": "", "model":"","prompt":""}), + "ai": data.get( + "ai", {"api_key": "", "api_url": "", "model": "", "prompt": ""} + ), } return config @@ -80,6 +95,7 @@ def load_config(config_filename="config.toml"): print(f"解析配置文件失败: {e}") return {} + # ========================================== # 2. 配置保存 (Config Saver) # ========================================== @@ -95,11 +111,15 @@ def save_config(config_data, config_filename="config.toml"): new_data = { "paths": { "source_file": os.path.basename(config_data.get("source_file", "")), - "output_folder": os.path.basename(config_data.get("output_folder", "output")), + "output_folder": os.path.basename( + config_data.get("output_folder", "output") + ), "excel_file": os.path.basename(config_data.get("excel_file", "")), "image_folder": os.path.basename(config_data.get("image_folder", "")), "fonts_dir": os.path.basename(config_data.get("fonts_dir", "fonts")), - "signature_image": get_resource_path(os.path.join("data", config_data.get("signature_image", ""))), + "signature_image": get_resource_path( + os.path.join("data", config_data.get("signature_image", "")) + ), }, "class_info": { "class_name": config_data.get("class_name", ""), @@ -110,14 +130,14 @@ def save_config(config_data, config_filename="config.toml"): "default_comment": config_data.get("default_comment", ""), "age_group": config_data.get("age_group", ""), }, - "ai": config_data.get("ai", {}) + "ai": config_data.get("ai", {}), } # 写入文件 with open(save_path, "wb") as f: f.write(toml_write.dumps(new_data).encode("utf-8")) - + return True, f"成功保存到: {save_path}" except Exception as e: - return False, f"写入失败: {str(e)}" \ No newline at end of file + return False, f"写入失败: {str(e)}" diff --git a/data/names.xlsx b/data/names.xlsx index 27239c5..2c900bc 100644 Binary files a/data/names.xlsx and b/data/names.xlsx differ diff --git a/templates/中一班 幼儿学期发展报告.pptx b/templates/中一班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..a9c5dda Binary files /dev/null and b/templates/中一班 幼儿学期发展报告.pptx differ diff --git a/templates/大四班 幼儿学期发展报告.pptx b/templates/大四班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..5112195 Binary files /dev/null and b/templates/大四班 幼儿学期发展报告.pptx differ diff --git a/templates/小三班 幼儿学期发展报告.pptx b/templates/小三班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..22c50c1 Binary files /dev/null and b/templates/小三班 幼儿学期发展报告.pptx differ diff --git a/templates/(竖版)大班 幼儿学期发展报告.pptx b/templates/(竖版)大班 幼儿学期发展报告.pptx index 444264f..91f6541 100644 Binary files a/templates/(竖版)大班 幼儿学期发展报告.pptx and b/templates/(竖版)大班 幼儿学期发展报告.pptx differ diff --git a/ui/views/config_page.py b/ui/views/config_page.py index 2636378..ec5905b 100644 --- a/ui/views/config_page.py +++ b/ui/views/config_page.py @@ -1,14 +1,16 @@ from nicegui import ui import os from utils.template_utils import get_template_files + # 修改点 1:统一导入,避免与变量名 config 冲突 -from config.config import load_config, save_config +from config.config import load_config, save_config + def create_config_page(): # 修改点 2:将加载逻辑放入页面生成函数内,确保每次刷新页面获取最新值 conf_data = load_config("config.toml") template_options = get_template_files() - current_filename = os.path.basename(conf_data.get('source_file', '')) + current_filename = os.path.basename(conf_data.get("source_file", "")) if current_filename and current_filename not in template_options: template_options.append(current_filename) @@ -16,63 +18,148 @@ def create_config_page(): ui.add_head_html('') # 样式修正:添加全屏且不滚动条的 CSS - ui.add_head_html(''' + ui.add_head_html( + """ - ''') + """ + ) - with ui.header().classes('app-header items-center justify-between shadow-md'): + with ui.header().classes("app-header items-center justify-between shadow-md"): # 左侧:图标和标题 - with ui.row().classes('items-center gap-2'): - ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain') - ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold') + with ui.row().classes("items-center gap-2"): + ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain") + ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold") # 右侧:署名 + 配置按钮 - with ui.row().classes('items-center gap-4'): - ui.label('By 寒寒 | 这里的每一份评语都充满爱意').classes('text-xs opacity-90') - ui.button(icon='home', on_click=lambda: ui.navigate.to('/')).props('flat round color=white') + with ui.row().classes("items-center gap-4"): + ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes( + "text-xs opacity-90" + ) + ui.button(icon="home", on_click=lambda: ui.navigate.to("/")).props( + "flat round color=white" + ) # 修改点 3:使用 flex 布局撑满 - with ui.card().classes('w-full max-w-5xl mx-auto shadow-lg main-card p-0'): - with ui.tabs().classes('w-full') as tabs: - tab_path = ui.tab('路径设置', icon='folder') - tab_class = ui.tab('班级与教师', icon='school') - tab_ai = ui.tab('AI 接口配置', icon='psychology') + with ui.card().classes("w-full max-w-5xl mx-auto shadow-lg main-card p-0"): + with ui.tabs().classes("w-full") as tabs: + tab_path = ui.tab("路径设置", icon="folder") + tab_class = ui.tab("班级与教师", icon="school") + tab_ai = ui.tab("AI 接口配置", icon="psychology") - with ui.tab_panels(tabs, value=tab_path).classes('w-full flex-grow bg-transparent'): + with ui.tab_panels(tabs, value=tab_path).classes( + "w-full flex-grow bg-transparent" + ): # --- 路径设置 --- - with ui.tab_panel(tab_path).classes('w-full p-0'): - with ui.column().classes('w-full p-4 gap-4'): - source_file = ui.select(options=template_options, label='PPT 模板', value=current_filename).props('outlined fill-input').classes('w-full') - excel_file = ui.input('Excel 文件', value=os.path.basename(conf_data.get('excel_file', ''))).props('outlined').classes('w-full') - image_folder = ui.input('图片目录', value=os.path.basename(conf_data.get('image_folder', ''))).props('outlined').classes('w-full') - output_folder = ui.input('输出目录', value=os.path.basename(conf_data.get('output_folder', 'output'))).props('outlined').classes('w-full') + with ui.tab_panel(tab_path).classes("w-full p-0"): + with ui.column().classes("w-full p-4 gap-4"): + source_file = ( + ui.select( + options=template_options, + label="PPT 模板", + value=current_filename, + ) + .props("outlined fill-input") + .classes("w-full") + ) + excel_file = ( + ui.input( + "Excel 文件", + value=os.path.basename(conf_data.get("excel_file", "")), + ) + .props("outlined") + .classes("w-full") + ) + image_folder = ( + ui.input( + "图片目录", + value=os.path.basename(conf_data.get("image_folder", "")), + ) + .props("outlined") + .classes("w-full") + ) + output_folder = ( + ui.input( + "输出目录", + value=os.path.basename( + conf_data.get("output_folder", "output") + ), + ) + .props("outlined") + .classes("w-full") + ) # --- 班级信息 --- - with ui.tab_panel(tab_class).classes('w-full p-0'): - with ui.column().classes('w-full p-4 gap-4'): - class_name = ui.input('班级名称', value=conf_data.get('class_name', '')).props('outlined').classes('w-full') - age_group = ui.select( - options=['小班上学期', '小班下学期', '中班上学期', '中班下学期', '大班上学期', '大班下学期'], - label='年龄段', value=conf_data.get('age_group', '中班上学期') - ).props('outlined').classes('w-full') - teachers_text = ui.textarea('教师名单', value='\n'.join(conf_data.get('teachers', []))).props('outlined').classes('w-full h-40') - class_type = ui.select( - options={0: '便宜班', 1: '昂贵班'}, - label='班级类型', value=conf_data.get('class_type', 0) - ).props('outlined').classes('w-full') + with ui.tab_panel(tab_class).classes("w-full p-0"): + with ui.column().classes("w-full p-4 gap-4"): + class_name = ( + ui.input("班级名称", value=conf_data.get("class_name", "")) + .props("outlined") + .classes("w-full") + ) + age_group = ( + ui.select( + options=[ + "小班上学期", + "小班下学期", + "中班上学期", + "中班下学期", + "大班上学期", + "大班下学期", + ], + label="年龄段", + value=conf_data.get("age_group", "中班上学期"), + ) + .props("outlined") + .classes("w-full") + ) + teachers_text = ( + ui.textarea( + "教师名单", value="\n".join(conf_data.get("teachers", [])) + ) + .props("outlined") + .classes("w-full h-40") + ) + class_type = ( + ui.select( + options={0: "便宜班", 1: "昂贵班", 2: "昂贵的双木桥班"}, + label="班级类型", + value=conf_data.get("class_type", 0), + ) + .props("outlined") + .classes("w-full") + ) # --- AI 配置 --- - with ui.tab_panel(tab_ai).classes('w-full p-0'): - with ui.column().classes('w-full p-4 gap-4'): - ai_key = ui.input('API Key', value=conf_data['ai'].get('api_key', '')).props('outlined password').classes('w-full') - ai_url = ui.input('API URL', value=conf_data['ai'].get('api_url', '')).props('outlined').classes('w-full') - ai_model = ui.input('Model Name', value=conf_data['ai'].get('model', '')).props('outlined').classes('w-full') - ai_prompt = ui.textarea('System Prompt', value=conf_data['ai'].get('prompt', '')).props('outlined').classes('w-full h-full') + with ui.tab_panel(tab_ai).classes("w-full p-0"): + with ui.column().classes("w-full p-4 gap-4"): + ai_key = ( + ui.input("API Key", value=conf_data["ai"].get("api_key", "")) + .props("outlined password") + .classes("w-full") + ) + ai_url = ( + ui.input("API URL", value=conf_data["ai"].get("api_url", "")) + .props("outlined") + .classes("w-full") + ) + ai_model = ( + ui.input("Model Name", value=conf_data["ai"].get("model", "")) + .props("outlined") + .classes("w-full") + ) + ai_prompt = ( + ui.textarea( + "System Prompt", value=conf_data["ai"].get("prompt", "") + ) + .props("outlined") + .classes("w-full h-full") + ) # 底部固定按钮 - with ui.row().classes('w-full p-4'): + with ui.row().classes("w-full p-4"): + async def handle_save(): new_data = { "source_file": source_file.value, @@ -81,17 +168,21 @@ def create_config_page(): "output_folder": output_folder.value, "class_name": class_name.value, "age_group": age_group.value, - "teachers": [t.strip() for t in teachers_text.value.split('\n') if t.strip()], + "teachers": [ + t.strip() for t in teachers_text.value.split("\n") if t.strip() + ], "class_type": class_type.value, "ai": { "api_key": ai_key.value, "api_url": ai_url.value, "model": ai_model.value, - "prompt": ai_prompt.value - } + "prompt": ai_prompt.value, + }, } # 修改点 4:直接调用导入的 save_config 函数名 success, message = save_config(new_data) - ui.notify("配置已保存重启生效", type='positive') + ui.notify("配置已保存重启生效", type="positive") - ui.button('保存配置', on_click=handle_save).classes('w-full py-4').props('outline color=primary') + ui.button("保存配置", on_click=handle_save).classes("w-full py-4").props( + "outline color=primary" + ) diff --git a/ui/views/home_page.py b/ui/views/home_page.py index a7f4b25..f5bca20 100644 --- a/ui/views/home_page.py +++ b/ui/views/home_page.py @@ -5,25 +5,35 @@ from ui.core.task_runner import run_task, select_folder # 导入业务函数 from utils.generate_utils import ( - generate_template, generate_comment_all, - batch_convert_folder, generate_report, generate_zodiac, generate_signature + generate_template, + generate_comment_all, + batch_convert_folder, + generate_report, + generate_zodiac, + generate_signature, ) -from utils.file_utils import export_templates_folder, initialize_project, export_data +from utils.file_utils import initialize_project, open_folder config = load_config("config.toml") + def create_header(): - with ui.header().classes('app-header items-center justify-between shadow-md'): + with ui.header().classes("app-header items-center justify-between shadow-md"): # 左侧:图标和标题 - with ui.row().classes('items-center gap-2'): - ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain') - ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold') - + with ui.row().classes("items-center gap-2"): + ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain") + ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold") + # 右侧:署名 + 配置按钮 - with ui.row().classes('items-center gap-4'): - ui.label('By 寒寒 | 这里的每一份评语都充满爱意').classes('text-xs opacity-90') + with ui.row().classes("items-center gap-4"): + ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes( + "text-xs opacity-90" + ) # 添加配置按钮 - ui.button(icon='settings', on_click=lambda: ui.navigate.to('/config')).props('flat round color=white').tooltip('系统配置') + ui.button( + icon="settings", on_click=lambda: ui.navigate.to("/config") + ).props("flat round color=white").tooltip("系统配置") + def create_home_page(): # 1. 引入外部 CSS @@ -32,62 +42,70 @@ def create_home_page(): create_header() # 主容器 - with ui.column().classes('w-full max-w-4xl mx-auto p-4 gap-4 thin-scrollbar'): + with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-4 thin-scrollbar"): # === 进度条区域 === - with ui.card().classes('func-card'): - app_state.progress_label = ui.label('⛳ 任务进度: 待命').classes('font-bold text-gray-700 mb-1') + with ui.card().classes("func-card"): + app_state.progress_label = ui.label("⛳ 任务进度: 待命").classes( + "font-bold text-gray-700 mb-1" + ) # 使用 NiceGUI 原生属性配合 CSS 类 - app_state.progress_bar = ui.linear_progress(value=0, show_value=False).classes('h-4 rounded') - app_state.progress_bar.props('color=positive') # 使用 Quasar 颜色变量 + app_state.progress_bar = ui.linear_progress( + value=0, show_value=False + ).classes("h-4 rounded") + app_state.progress_bar.props("color=positive") # 使用 Quasar 颜色变量 # === 核心功能区 === - with ui.card().classes('func-card card-core'): - ui.label('🛠️ 核心功能').classes('section-title text-green') + with ui.card().classes("func-card card-core"): + ui.label("🛠️ 核心功能").classes("section-title text-green") - with ui.grid(columns=3).classes('w-full gap-3'): + with ui.grid(columns=3).classes("w-full gap-3"): # 辅助函数:快速创建按钮 def func_btn(text, icon, func): - ui.button(text, on_click=lambda: run_task(func)).props(f'outline').classes('w-full') + ui.button(text, on_click=lambda: run_task(func)).props( + f"outline" + ).classes("w-full") + + func_btn("📁 生成图片路径", "image", generate_template) + func_btn("🤖 生成评语 (AI)", "smart_toy", generate_comment_all) + func_btn("📊 生成报告 (PPT)", "analytics", generate_report) - func_btn('📁 生成图片路径', 'image', generate_template) - func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all) - func_btn('📊 生成报告 (PPT)', 'analytics', generate_report) # 特殊处理带参数的 async def run_convert(): await run_task(batch_convert_folder, config.get("output_folder")) - ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline') - func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac) - func_btn('💴 园长一键签名', 'refresh', generate_signature) + + ui.button("📑 格式转换 (PDF)", on_click=run_convert).props("outline") + func_btn("🐂 生肖转化 (生日)", "pets", generate_zodiac) + func_btn("💴 园长一键签名", "refresh", generate_signature) # === 下方双栏布局 === - with ui.grid(columns=2).classes('w-full gap-4'): - # 数据管理 - with ui.card().classes('func-card card-data'): - ui.label('📦 数据管理').classes('section-title text-blue') - with ui.row().classes('w-full'): - async def do_export(func): - path = await select_folder() - if path: await run_task(func, path) + # 数据管理 + with ui.card().classes("func-card card-data"): + ui.label("⚙️ 系统操作").classes("section-title text-blue") - ui.button('📦 导出模板', on_click=lambda: do_export(export_templates_folder)).props(f'outline') - ui.button('📤 导出备份', on_click=lambda: do_export(export_data)).props(f'outline') - # 系统操作 - with ui.card().classes('func-card card-system'): - ui.label('⚙️ 系统操作').classes('section-title text-red') - with ui.row().classes('w-full'): - def stop_now(): - if app_state.is_running: - app_state.stop_event.set() - ui.notify("发送停止信号...", type="warning") + def stop_now(): + if app_state.is_running: + app_state.stop_event.set() + ui.notify("发送停止信号...", type="warning") - ui.button('⛔ 停止', on_click=stop_now).props('color=negative').classes('flex-1') + with ui.row().classes("w-full"): + ui.button( + "📦 打开输出文件夹", + on_click=lambda: open_folder(config.get("output_folder")), + ).props(f"outline") + ui.button( + "📤 打开数据文件夹", + on_click=lambda: open_folder(config.get("data_folder")), + ).props(f"outline") + ui.button("⛔ 停止", on_click=stop_now).props("color=negative").classes( + "flex-1" + ) - async def reset_sys(): - await run_task(initialize_project) - - ui.button('⚠️ 初始化', on_click=reset_sys).props('outline color=warning').classes('flex-1') # === 日志区 === - with ui.card().classes('func-card card-logging'): - with ui.expansion('📝 系统实时日志',value=True).classes('w-full bg-white shadow-sm rounded'): - app_state.log_element = ui.log(max_lines=200).classes('w-full h-40 font-mono text-xs bg-gray-100 p-2') \ No newline at end of file + with ui.card().classes("func-card card-logging"): + with ui.expansion("📝 系统实时日志", value=True).classes( + "w-full bg-white shadow-sm rounded" + ): + app_state.log_element = ui.log(max_lines=200).classes( + "w-full h-40 font-mono text-xs bg-gray-100 p-2" + ) diff --git a/utils/agent_utils.py b/utils/agent_utils.py index 2bb4bb1..b2d28d9 100644 --- a/utils/agent_utils.py +++ b/utils/agent_utils.py @@ -8,8 +8,9 @@ import traceback from config.config import load_config class_type_config =[ - "本期开展了小袋鼠整合主题课程:(语言、社会、科学、健康、艺术)、生活数学;特色课程(英语、体能、美工、篮球)", - "本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)" + "本期开展了小袋鼠整合主题课程:(语言、社会、科学、健康、艺术)、生活数学;特色课程(英语、体能、美工、篮球)。", + "本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。", + "本学期开展了双木桥主题课程(图说汉字、妙趣汉音、情智阅读、麦斯思维、专注力训练);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。" ] def generate_comment(name, age_group, traits,sex): diff --git a/utils/file_utils.py b/utils/file_utils.py index 50a308a..2d4e3b7 100644 --- a/utils/file_utils.py +++ b/utils/file_utils.py @@ -5,6 +5,7 @@ from loguru import logger import zipfile import traceback + def export_templates_folder(output_folder, stop_event, progress_callback=None): """ 将指定文件夹压缩为 zip 包 @@ -121,7 +122,9 @@ def export_data(save_dir, root_dir=".", progress_callback=None): has_files = True # 更新进度条 if progress_callback: - progress_callback(processed_count + 1, total_files, "导出数据中...") + progress_callback( + processed_count + 1, total_files, "导出数据中..." + ) if has_files: # 确保进度条最后能走到 100% @@ -139,6 +142,7 @@ def export_data(save_dir, root_dir=".", progress_callback=None): except Exception as e: logger.error(f"导出过程出错: {str(e)}") import traceback + logger.error(traceback.format_exc()) return None @@ -218,6 +222,7 @@ def check_file_exists(file_path): """ return file_path and isinstance(file_path, str) and os.path.exists(file_path) + def get_output_pptx_files(output_dir="output"): """ 获取 output 文件夹下所有的 pptx 文件 @@ -241,3 +246,17 @@ def get_output_pptx_files(output_dir="output"): except Exception as e: logger.error(f"发生未知错误: {e}") logger.error(traceback.format_exc()) + + +def open_folder(folder_path): + """ + 打开指定文件夹 + :param folder_path: 文件夹路径 + """ + try: + if os.path.exists(folder_path): + os.startfile(folder_path) + else: + logger.error(f"文件夹不存在: {folder_path}") + except Exception as e: + logger.error(f"打开文件夹失败: {e}") diff --git a/utils/pptx_utils.py b/utils/pptx_utils.py index 82449af..4a4f57f 100644 --- a/utils/pptx_utils.py +++ b/utils/pptx_utils.py @@ -21,28 +21,28 @@ def replace_text_in_slide(prs, slide_index, placeholder, text): original_paragraph_formats = [] for paragraph in shape.text_frame.paragraphs: paragraph_format = { - 'alignment': paragraph.alignment, - 'space_before': getattr(paragraph, 'space_before', None), - 'space_after': getattr(paragraph, 'space_after', None), - 'line_spacing': getattr(paragraph, 'line_spacing', None), - 'left_indent': getattr(paragraph, 'left_indent', None), - 'right_indent': getattr(paragraph, 'right_indent', None), - 'first_line_indent': getattr(paragraph, 'first_line_indent', None), - 'font_info': [] + "alignment": paragraph.alignment, + "space_before": getattr(paragraph, "space_before", None), + "space_after": getattr(paragraph, "space_after", None), + "line_spacing": getattr(paragraph, "line_spacing", None), + "left_indent": getattr(paragraph, "left_indent", None), + "right_indent": getattr(paragraph, "right_indent", None), + "first_line_indent": getattr(paragraph, "first_line_indent", None), + "font_info": [], } for run in paragraph.runs: run_format = { - 'font_name': run.font.name, - 'font_size': run.font.size, - 'bold': run.font.bold, - 'italic': run.font.italic, - 'underline': run.font.underline, - 'color': run.font.color, - 'character_space': getattr(run.font, 'space', None), - 'all_caps': getattr(run.font, 'all_caps', None), - 'small_caps': getattr(run.font, 'small_caps', None) + "font_name": run.font.name, + "font_size": run.font.size, + "bold": run.font.bold, + "italic": run.font.italic, + "underline": run.font.underline, + "color": run.font.color, + "character_space": getattr(run.font, "space", None), + "all_caps": getattr(run.font, "all_caps", None), + "small_caps": getattr(run.font, "small_caps", None), } - paragraph_format['font_info'].append(run_format) + paragraph_format["font_info"].append(run_format) original_paragraph_formats.append(paragraph_format) # 2. 设置新文本 @@ -51,55 +51,69 @@ def replace_text_in_slide(prs, slide_index, placeholder, text): # 3. 恢复格式 for i, paragraph in enumerate(shape.text_frame.paragraphs): orig_idx = i if i < len(original_paragraph_formats) else -1 - if not original_paragraph_formats: break + if not original_paragraph_formats: + break original_para = original_paragraph_formats[orig_idx] # 恢复段落属性 - for attr in ['alignment', 'space_before', 'space_after', 'line_spacing', - 'left_indent', 'right_indent', 'first_line_indent']: + for attr in [ + "alignment", + "space_before", + "space_after", + "line_spacing", + "left_indent", + "right_indent", + "first_line_indent", + ]: if original_para[attr] is not None: setattr(paragraph, attr, original_para[attr]) # 恢复字体属性 for j, run in enumerate(paragraph.runs): - font_idx = j if j < len(original_para['font_info']) else 0 - if not original_para['font_info']: break + font_idx = j if j < len(original_para["font_info"]) else 0 + if not original_para["font_info"]: + break - original_font = original_para['font_info'][font_idx] + original_font = original_para["font_info"][font_idx] # 字体名称检查与回退 - if original_font['font_name']: - if is_font_available(original_font['font_name']): - run.font.name = original_font['font_name'] + if original_font["font_name"]: + if is_font_available(original_font["font_name"]): + run.font.name = original_font["font_name"] else: run.font.name = "微软雅黑" # 恢复其他字体属性 - if original_font['font_size']: run.font.size = original_font['font_size'] - if original_font['bold']: run.font.bold = original_font['bold'] - if original_font['italic']: run.font.italic = original_font['italic'] - if original_font['underline']: run.font.underline = original_font['underline'] - if original_font['all_caps']: run.font.all_caps = original_font['all_caps'] + if original_font["font_size"]: + run.font.size = original_font["font_size"] + if original_font["bold"]: + run.font.bold = original_font["bold"] + if original_font["italic"]: + run.font.italic = original_font["italic"] + if original_font["underline"]: + run.font.underline = original_font["underline"] + if original_font["all_caps"]: + run.font.all_caps = original_font["all_caps"] - if original_font['character_space']: + if original_font["character_space"]: try: - run.font.space = original_font['character_space'] + run.font.space = original_font["character_space"] except: - pass + logger.error(f"错误: 无法设置字体间距 {original_font['character_space']}") - if original_font['color']: + if original_font["color"]: try: - if hasattr(original_font['color'], 'rgb'): - run.font.color.rgb = original_font['color'].rgb + if hasattr(original_font["color"], "rgb"): + run.font.color.rgb = original_font["color"].rgb except: - pass + logger.error(f"错误: 无法设置字体颜色 {original_font['color']}") def replace_picture(prs, slide_index, placeholder, img_path): """ 在指定幻灯片中替换指定占位符的图片(包含自动旋转修复) - + 参数: prs: Presentation 对象 slide_index: 幻灯片索引 (从0开始) @@ -122,19 +136,26 @@ def replace_picture(prs, slide_index, placeholder, img_path): target_shape = shape target_index = i break - + if target_shape: # 获取原位置信息 - left, top, width, height = target_shape.left, target_shape.top, target_shape.width, target_shape.height - + left, top, width, height = ( + target_shape.left, + target_shape.top, + target_shape.width, + target_shape.height, + ) + # 2. 获取修正后的图片流 img_stream = get_corrected_image_stream(img_path) - + # 3. 移除旧形状 sp_tree.remove(target_shape._element) - + # 4. 插入新图片 (使用流而不是路径) new_shape = slide.shapes.add_picture(img_stream, left, top, width, height) - + # 5. 恢复层级位置 (z-order) - sp_tree.insert(target_index, new_shape._element) \ No newline at end of file + sp_tree.insert(target_index, new_shape._element) + else: + logger.warning(f"警告: 幻灯片 {slide_index} 中未找到占位符 {placeholder}")