fix:实现基础功能
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/start.bat" charset="US-ASCII" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
21
.idea/growth_report.iml
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="uv (growth_report)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/templates" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
56
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<list>
|
||||||
|
<option value="loguru" />
|
||||||
|
<option value="APScheduler" />
|
||||||
|
<option value="watchdog" />
|
||||||
|
<option value="aiohttp" />
|
||||||
|
<option value="aiofiles" />
|
||||||
|
<option value="pydantic" />
|
||||||
|
<option value="SQLAlchemy" />
|
||||||
|
<option value="aiosqlite" />
|
||||||
|
<option value="fastapi" />
|
||||||
|
<option value="uvicorn" />
|
||||||
|
<option value="python-multipart" />
|
||||||
|
<option value="jinja2" />
|
||||||
|
<option value="itsdangerous" />
|
||||||
|
<option value="pillow" />
|
||||||
|
<option value="filetype" />
|
||||||
|
<option value="pydub" />
|
||||||
|
<option value="pysilk-mod" />
|
||||||
|
<option value="pymediainfo" />
|
||||||
|
<option value="py7zr" />
|
||||||
|
<option value="requests" />
|
||||||
|
<option value="httpx" />
|
||||||
|
<option value="tabulate" />
|
||||||
|
<option value="qrcode" />
|
||||||
|
<option value="psutil" />
|
||||||
|
<option value="tomli_w" />
|
||||||
|
<option value="websockets" />
|
||||||
|
<option value="redis" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
<option value="N802" />
|
||||||
|
<option value="N803" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="type.*" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="uv (data_tools)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="uv (growth_report)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/growth_report.iml" filepath="$PROJECT_DIR$/.idea/growth_report.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
149
IFLOW.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 幼儿园成长报告生成系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
这是一个基于Python的自动化幼儿园成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统还支持字体安装、图片替换、批量PDF转换等功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Python 3.x**: 主要编程语言
|
||||||
|
- **python-pptx**: PPT文件处理
|
||||||
|
- **pandas**: Excel数据读取与处理
|
||||||
|
- **langchain**: AI模型集成
|
||||||
|
- **comtypes**: PowerPoint转PDF功能
|
||||||
|
- **rich**: 美化命令行界面
|
||||||
|
- **loguru**: 日志记录
|
||||||
|
- **toml**: 配置文件解析
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 生成模板 (📁 生成模板)
|
||||||
|
|
||||||
|
- 从Excel文件中读取学生姓名
|
||||||
|
- 为每个学生创建图片文件夹结构
|
||||||
|
|
||||||
|
### 2. 生成评语 (🤖 生成评语)
|
||||||
|
|
||||||
|
- 使用AI模型根据学生姓名、年龄组和表现特征生成个性化评语
|
||||||
|
- 评语会自动写入Excel文件的对应列
|
||||||
|
- 支持跳过已有评语的记录以节省API调用
|
||||||
|
|
||||||
|
### 3. 生成报告 (📊 生成报告)
|
||||||
|
|
||||||
|
- 读取Excel数据和图片资源
|
||||||
|
- 将信息批量填充到PPT模板中
|
||||||
|
- 保持原有的文字格式和样式
|
||||||
|
- 替换文本占位符和图片占位符
|
||||||
|
|
||||||
|
### 4. 格式转换 (📑 格式转换)
|
||||||
|
|
||||||
|
- 批量将生成的PPT文件转换为PDF格式
|
||||||
|
- 使用COM接口与PowerPoint进行交互
|
||||||
|
|
||||||
|
## 主要文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
growth_report/
|
||||||
|
├── main.py # 主程序入口,包含所有核心功能
|
||||||
|
├── config.toml # 项目配置文件
|
||||||
|
├── config/
|
||||||
|
│ └── config.py # 配置加载工具
|
||||||
|
├── utils/
|
||||||
|
│ ├── agent_utils.py # AI评语生成工具
|
||||||
|
│ ├── font_utils.py # 字体安装和检测工具
|
||||||
|
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
||||||
|
│ ├── pef_utils.py # PPT转PDF工具
|
||||||
|
└── data/
|
||||||
|
├── names.xlsx # 学生数据Excel文件
|
||||||
|
└── images/ # 学生图片资源文件夹
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### config.toml
|
||||||
|
|
||||||
|
- `paths`: 定义文件路径(模板、输出、Excel、图片、字体)
|
||||||
|
- `class_info`: 班级信息(名称、教师名单)
|
||||||
|
- `defaults`: 默认设置(默认评语、年龄组)
|
||||||
|
- `excel`: Excel表配置(工作表名、列名顺序)
|
||||||
|
- `ai`: AI配置(API密钥、URL、模型、提示词)
|
||||||
|
|
||||||
|
### Excel数据格式
|
||||||
|
|
||||||
|
Excel文件应包含以下列(顺序必须与配置文件中一致):
|
||||||
|
|
||||||
|
- 姓名
|
||||||
|
- 英文名
|
||||||
|
- 性别
|
||||||
|
- 生日
|
||||||
|
- 属相
|
||||||
|
- 我的好朋友
|
||||||
|
- 我的爱好
|
||||||
|
- 喜欢的游戏
|
||||||
|
- 喜欢吃的食物
|
||||||
|
- 评价
|
||||||
|
|
||||||
|
### 图片文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
data/images/
|
||||||
|
├── 班级名称.jpg # 班级集体照片
|
||||||
|
└── 学生姓名/
|
||||||
|
├── me_image.jpg # 学生个人照片
|
||||||
|
├── 1.jpg # 活动照片1
|
||||||
|
├── 2.jpg # 活动照片2
|
||||||
|
```
|
||||||
|
|
||||||
|
## AI评语生成
|
||||||
|
|
||||||
|
系统使用langchain框架与AI模型集成,根据以下信息生成评语:
|
||||||
|
|
||||||
|
- 幼儿姓名
|
||||||
|
- 所在班级(年龄段)
|
||||||
|
- 表现特征
|
||||||
|
|
||||||
|
评语风格为"治愈系",采用三段式结构:亲切问候、具体描述优点、委婉期望与祝福。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 运行程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 初始化设置
|
||||||
|
|
||||||
|
1. 编辑`config.toml`配置文件
|
||||||
|
2. 准备Excel数据文件
|
||||||
|
3. 准备图片资源文件夹
|
||||||
|
4. 准备PPT模板文件
|
||||||
|
|
||||||
|
## 系统特点
|
||||||
|
|
||||||
|
- **自动化流程**: 从数据到成品报告的全流程自动化
|
||||||
|
- **AI集成**: 智能生成个性化评语
|
||||||
|
- **格式保持**: 替换文本时保持原有格式
|
||||||
|
- **用户友好**: 丰富的命令行界面和进度提示
|
||||||
|
- **批量处理**: 支持批量生成和转换
|
||||||
|
- **错误处理**: 完善的异常处理和日志记录
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- 使用`loguru`进行日志记录
|
||||||
|
- 使用`rich`美化命令行输出
|
||||||
|
- 配置文件使用TOML格式
|
||||||
|
- 图片和文本替换使用占位符机制
|
||||||
|
- 遵循Python代码规范
|
||||||
149
README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 幼儿园成长报告生成系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
基于Python的自动化尚城幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统还支持字体安装、图片替换、批量PDF转换等功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Python 3.x**: 主要编程语言
|
||||||
|
- **python-pptx**: PPT文件处理
|
||||||
|
- **pandas**: Excel数据读取与处理
|
||||||
|
- **langchain**: AI模型集成
|
||||||
|
- **comtypes**: PowerPoint转PDF功能
|
||||||
|
- **rich**: 美化命令行界面
|
||||||
|
- **loguru**: 日志记录
|
||||||
|
- **toml**: 配置文件解析
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 生成模板 (📁 生成模板)
|
||||||
|
|
||||||
|
- 从Excel文件中读取学生姓名
|
||||||
|
- 为每个学生创建图片文件夹结构
|
||||||
|
|
||||||
|
### 2. 生成评语 (🤖 生成评语)
|
||||||
|
|
||||||
|
- 使用AI模型根据学生姓名、年龄组和表现特征生成个性化评语
|
||||||
|
- 评语会自动写入Excel文件的对应列
|
||||||
|
- 支持跳过已有评语的记录以节省API调用
|
||||||
|
|
||||||
|
### 3. 生成报告 (📊 生成报告)
|
||||||
|
|
||||||
|
- 读取Excel数据和图片资源
|
||||||
|
- 将信息批量填充到PPT模板中
|
||||||
|
- 保持原有的文字格式和样式
|
||||||
|
- 替换文本占位符和图片占位符
|
||||||
|
|
||||||
|
### 4. 格式转换 (📑 格式转换)
|
||||||
|
|
||||||
|
- 批量将生成的PPT文件转换为PDF格式
|
||||||
|
- 使用COM接口与PowerPoint进行交互
|
||||||
|
|
||||||
|
## 主要文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
growth_report/
|
||||||
|
├── main.py # 主程序入口,包含所有核心功能
|
||||||
|
├── config.toml # 项目配置文件
|
||||||
|
├── config/
|
||||||
|
│ └── config.py # 配置加载工具
|
||||||
|
├── utils/
|
||||||
|
│ ├── agent_utils.py # AI评语生成工具
|
||||||
|
│ ├── font_utils.py # 字体安装和检测工具
|
||||||
|
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
||||||
|
│ ├── pef_utils.py # PPT转PDF工具
|
||||||
|
└── data/
|
||||||
|
├── names.xlsx # 学生数据Excel文件
|
||||||
|
└── images/ # 学生图片资源文件夹
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### config.toml
|
||||||
|
|
||||||
|
- `paths`: 定义文件路径(模板、输出、Excel、图片、字体)
|
||||||
|
- `class_info`: 班级信息(名称、教师名单)
|
||||||
|
- `defaults`: 默认设置(默认评语、年龄组)
|
||||||
|
- `excel`: Excel表配置(工作表名、列名顺序)
|
||||||
|
- `ai`: AI配置(API密钥、URL、模型、提示词)
|
||||||
|
|
||||||
|
### Excel数据格式
|
||||||
|
|
||||||
|
Excel文件应包含以下列(顺序必须与配置文件中一致):
|
||||||
|
|
||||||
|
- 姓名
|
||||||
|
- 英文名
|
||||||
|
- 性别
|
||||||
|
- 生日
|
||||||
|
- 属相
|
||||||
|
- 我的好朋友
|
||||||
|
- 我的爱好
|
||||||
|
- 喜欢的游戏
|
||||||
|
- 喜欢吃的食物
|
||||||
|
- 评价
|
||||||
|
|
||||||
|
### 图片文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
data/images/
|
||||||
|
├── 班级名称.jpg # 班级集体照片
|
||||||
|
└── 学生姓名/
|
||||||
|
├── me_image.jpg # 学生个人照片
|
||||||
|
├── 1.jpg # 活动照片1
|
||||||
|
├── 2.jpg # 活动照片2
|
||||||
|
```
|
||||||
|
|
||||||
|
## AI评语生成
|
||||||
|
|
||||||
|
系统使用langchain框架与AI模型集成,根据以下信息生成评语:
|
||||||
|
|
||||||
|
- 幼儿姓名
|
||||||
|
- 所在班级(年龄段)
|
||||||
|
- 表现特征
|
||||||
|
|
||||||
|
评语风格为"治愈系",采用三段式结构:亲切问候、具体描述优点、委婉期望与祝福。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 运行程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 初始化设置
|
||||||
|
|
||||||
|
1. 编辑`config.toml`配置文件
|
||||||
|
2. 准备Excel数据文件
|
||||||
|
3. 准备图片资源文件夹
|
||||||
|
4. 准备PPT模板文件
|
||||||
|
|
||||||
|
## 系统特点
|
||||||
|
|
||||||
|
- **自动化流程**: 从数据到成品报告的全流程自动化
|
||||||
|
- **AI集成**: 智能生成个性化评语
|
||||||
|
- **格式保持**: 替换文本时保持原有格式
|
||||||
|
- **用户友好**: 丰富的命令行界面和进度提示
|
||||||
|
- **批量处理**: 支持批量生成和转换
|
||||||
|
- **错误处理**: 完善的异常处理和日志记录
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- 使用`loguru`进行日志记录
|
||||||
|
- 使用`rich`美化命令行输出
|
||||||
|
- 配置文件使用TOML格式
|
||||||
|
- 图片和文本替换使用占位符机制
|
||||||
|
- 遵循Python代码规范
|
||||||
96
config.toml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
[paths]
|
||||||
|
# PPT模版路径
|
||||||
|
source_file = "templates/大班幼儿学期发展报告.pptx"
|
||||||
|
# 输出文件夹
|
||||||
|
output_folder = "output"
|
||||||
|
# Excel数据文件路径
|
||||||
|
excel_file = "data/names.xlsx"
|
||||||
|
# 图片资源文件夹
|
||||||
|
image_folder = "data/images"
|
||||||
|
# 字体文件夹
|
||||||
|
fonts_dir = "fonts"
|
||||||
|
|
||||||
|
[class_info]
|
||||||
|
# 班级名称
|
||||||
|
class_name = "K4D"
|
||||||
|
# 老师名单 (数组格式)
|
||||||
|
teachers = ["简蜜", "王敏千", "李玉香"]
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
# 当Excel中没有评语时的默认内容
|
||||||
|
default_comment = "暂无评语"
|
||||||
|
age_group = "大班上学期"
|
||||||
|
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Excel 配置 (Data)
|
||||||
|
# ========================
|
||||||
|
[excel]
|
||||||
|
sheet_name = "Sheet1"
|
||||||
|
# 对应Excel表头名称,顺序必须与代码中的解包顺序一致!
|
||||||
|
# 顺序:姓名, 英文名, 性别, 生日, 属相, 好朋友, 爱好, 游戏, 食物, 评语
|
||||||
|
columns = [
|
||||||
|
"姓名",
|
||||||
|
"英文名",
|
||||||
|
"性别",
|
||||||
|
"生日",
|
||||||
|
"属相",
|
||||||
|
"我的好朋友",
|
||||||
|
"我的爱好",
|
||||||
|
"喜欢的游戏",
|
||||||
|
"喜欢吃的食物",
|
||||||
|
"评价"
|
||||||
|
]
|
||||||
|
|
||||||
|
[ai]
|
||||||
|
api_key = "sk-5b4f921bd9afa44eb346783643567158"
|
||||||
|
api_url = "https://apis.iflow.cn/v1/chat/completions"
|
||||||
|
model = "deepseek-v3.2"
|
||||||
|
prompt = """
|
||||||
|
# Role
|
||||||
|
你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意,擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的,能让家长读完后感到欣慰并对未来充满希望。
|
||||||
|
|
||||||
|
# Goal
|
||||||
|
请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。
|
||||||
|
|
||||||
|
# Constraints & Rules
|
||||||
|
1. **称呼处理**:
|
||||||
|
- 自动识别用户输入的姓名。
|
||||||
|
- **必须去掉姓氏**,只使用名。
|
||||||
|
- 统一格式为:“[名]宝贝,你好!”或“[名]宝贝:”。
|
||||||
|
- 例如:“王小明” -> “小明宝贝”;“李在这个” -> “在这个宝贝”。
|
||||||
|
|
||||||
|
2. **分龄侧重 (根据 Age_Group 调整侧重点)**:
|
||||||
|
- **小班 (3-4岁)**:侧重于适应集体生活、情绪稳定性、基本生活自理能力(吃饭、午睡、如厕)、愿意与老师互动。
|
||||||
|
- **中班 (4-5岁)**:侧重于社交互动、分享与合作、动手能力、好奇心、规则意识的建立、自信心的增强。
|
||||||
|
- **大班 (5-6岁)**:侧重于学习习惯、逻辑思维、领导力/榜样作用、任务意识、为幼小衔接做的准备、抗挫折能力。
|
||||||
|
|
||||||
|
3. **写作结构 (三段式)**:
|
||||||
|
- **开头**:亲切的问候 + 总体印象(用美好的形容词,如文静、活泼、机灵等)。
|
||||||
|
- **正文**:结合提供的【表现关键词】,具体描述孩子的进步和优点(必须具体,拒绝空洞)。
|
||||||
|
- **结尾**:委婉地提出一点小小的期望(用“如果你能...老师会更为你骄傲”的句式),并送上新学期的祝福。
|
||||||
|
|
||||||
|
4. **语气风格**:
|
||||||
|
- 积极正面,多用肯定句。
|
||||||
|
- 避免生硬的批评,将缺点转化为“待提升的潜力”或“期望”。
|
||||||
|
- 字数控制在 150-250 字之间(适合PPT展示)。
|
||||||
|
|
||||||
|
# Workflow
|
||||||
|
1. 分析用户输入的年龄段,确定评价基调。
|
||||||
|
2. 处理姓名,提取昵称。
|
||||||
|
3. 将输入的关键词串联成通顺、优美的句子。
|
||||||
|
4. 按照三段式结构输出最终评语。
|
||||||
|
|
||||||
|
# Input Format
|
||||||
|
用户将提供 JSON 格式或特定格式的数据,包含:
|
||||||
|
- Name {{name}}
|
||||||
|
- Age_Group {{class_name}}
|
||||||
|
- Traits (表现关键词/特征,如:吃饭香、爱画画、有些胆小、数学好)
|
||||||
|
|
||||||
|
# Output Example
|
||||||
|
(假设输入:Name=张图图, Age_Group=小班, Traits=适应能力强, 爱笑, 挑食)
|
||||||
|
图图宝贝,你好!
|
||||||
|
你是一个爱笑的小天使,每天早上都能看到你甜甜的笑脸,老师的心都要被你融化了。这个学期你进步真大呀,从一开始的哭鼻子到现在能开心地参与游戏,你的适应能力让老师感到惊喜。在集体活动中,你总是那么投入。
|
||||||
|
不过,老师发现你在吃饭时偶尔会把不喜欢的青菜挑出来哦。如果你能和青菜宝宝做好朋友,把身体练得棒棒的,那就更完美啦!
|
||||||
|
祝可爱的图图宝贝新年快乐,健康成长!
|
||||||
|
"""
|
||||||
44
config/config.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 尝试导入 toml 解析库 (兼容 Python 3.11+ 和旧版本)
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib as toml # Python 3.11+ 内置
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import tomli as toml # 需要 pip install tomli
|
||||||
|
except ImportError:
|
||||||
|
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. 配置加载 (Config Loader)
|
||||||
|
# ==========================================
|
||||||
|
def load_config(config_path="config.toml"):
|
||||||
|
"""读取 TOML 配置文件并转换为扁平字典"""
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
print(f"错误: 找不到配置文件 {config_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path, "rb") as f:
|
||||||
|
data = toml.load(f)
|
||||||
|
|
||||||
|
# 将 TOML 的层级结构映射回原本的扁平结构,保持代码其余部分不用大改
|
||||||
|
config = {
|
||||||
|
"source_file": data["paths"]["source_file"],
|
||||||
|
"output_folder": data["paths"]["output_folder"],
|
||||||
|
"excel_file": data["paths"]["excel_file"],
|
||||||
|
"image_folder": data["paths"]["image_folder"],
|
||||||
|
"fonts_dir": data["paths"]["fonts_dir"],
|
||||||
|
"class_name": data["class_info"]["class_name"],
|
||||||
|
"teachers": data["class_info"]["teachers"],
|
||||||
|
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||||
|
"age_group": data["defaults"].get("age_group", "大班上学期"),
|
||||||
|
"ai": data["ai"]
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取配置文件出错: {e}")
|
||||||
|
sys.exit(1)
|
||||||
BIN
data/images/K4D.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/罗槿祎/1.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/罗槿祎/2.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/罗槿祎/me_image.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/黄诗雅/1.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/黄诗雅/2.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/images/黄诗雅/me_image.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/names.xlsx
Normal file
BIN
fonts/方正兰亭黑简体.ttf
Normal file
BIN
fonts/方正少儿简体.ttf
Normal file
347
main.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
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()
|
||||||
415
old/main.py
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from pptx import Presentation
|
||||||
|
from pptx.util import Pt
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_fonts():
|
||||||
|
"""获取系统中可用的字体列表"""
|
||||||
|
fonts = set()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
try:
|
||||||
|
# 读取Windows字体目录
|
||||||
|
system_fonts_dir = Path(os.environ['WINDIR']) / 'Fonts'
|
||||||
|
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||||
|
|
||||||
|
# 检查系统字体目录
|
||||||
|
if system_fonts_dir.exists():
|
||||||
|
for font_file in system_fonts_dir.glob('*.ttf'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
for font_file in system_fonts_dir.glob('*.ttc'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
for font_file in system_fonts_dir.glob('*.otf'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
|
||||||
|
# 检查用户字体目录
|
||||||
|
if user_fonts_dir.exists():
|
||||||
|
for font_file in user_fonts_dir.glob('*.ttf'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
for font_file in user_fonts_dir.glob('*.ttc'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
for font_file in user_fonts_dir.glob('*.otf'):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取系统字体时出错: {e}")
|
||||||
|
# 备选方案:返回常见字体
|
||||||
|
fonts = {"微软雅黑", "宋体", "黑体", "楷体", "仿宋", "Arial", "Times New Roman", "Courier New",
|
||||||
|
"Microsoft YaHei"}
|
||||||
|
return fonts
|
||||||
|
|
||||||
|
|
||||||
|
def is_font_available(font_name):
|
||||||
|
"""检查字体是否在系统中可用"""
|
||||||
|
system_fonts = get_system_fonts()
|
||||||
|
# 检查字体名称的多种可能形式
|
||||||
|
check_names = [font_name, font_name.replace(" ", ""), font_name.replace("-", ""), font_name.lower(),
|
||||||
|
font_name.upper()]
|
||||||
|
for name in check_names:
|
||||||
|
if name in system_fonts:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_fonts_from_directory(fonts_dir="fonts"):
|
||||||
|
"""从指定目录安装字体到系统"""
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
print("字体安装功能目前仅支持Windows系统")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取系统字体目录
|
||||||
|
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||||
|
|
||||||
|
# 优先使用用户字体目录(不需要管理员权限)
|
||||||
|
target_font_dir = user_fonts_dir
|
||||||
|
|
||||||
|
# 创建目标目录(如果不存在)
|
||||||
|
target_font_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 检查字体目录是否存在
|
||||||
|
if not os.path.exists(fonts_dir):
|
||||||
|
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 遍历字体目录中的字体文件
|
||||||
|
font_extensions = ['.ttf', '.ttc', '.otf', '.fon']
|
||||||
|
font_files = []
|
||||||
|
for ext in font_extensions:
|
||||||
|
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
|
||||||
|
|
||||||
|
if not font_files:
|
||||||
|
print(f"在 {fonts_dir} 目录中未找到字体文件 (.ttf, .ttc, .otf, .fon)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 安装字体文件
|
||||||
|
installed_fonts = []
|
||||||
|
for font_file in font_files:
|
||||||
|
try:
|
||||||
|
# 复制字体文件到系统字体目录
|
||||||
|
target_path = target_font_dir / font_file.name
|
||||||
|
if not target_path.exists(): # 避免重复安装
|
||||||
|
shutil.copy2(font_file, target_path)
|
||||||
|
print(f"已安装字体: {font_file.name}")
|
||||||
|
installed_fonts.append(font_file.name)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# print(f"字体已存在: {font_file.name}") # 减少日志输出
|
||||||
|
except Exception as e:
|
||||||
|
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if installed_fonts:
|
||||||
|
print(f"共安装了 {len(installed_fonts)} 个新字体文件")
|
||||||
|
print("注意:新安装的字体可能需要重启Python环境后才能在PowerPoint中使用")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("没有安装新字体文件,可能已存在于系统中")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||||
|
"""
|
||||||
|
在指定幻灯片中替换指定占位符的文本,并保持原有格式。
|
||||||
|
:param prs: Presentation 对象
|
||||||
|
:param slide_index: 要操作的幻灯片索引(从0开始)
|
||||||
|
:param placeholder: 占位符名称 (shape.name)
|
||||||
|
:param text: 要替换的文本
|
||||||
|
"""
|
||||||
|
slide = prs.slides[slide_index]
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if shape.name == placeholder:
|
||||||
|
if not shape.has_text_frame:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- 1. 保存原有格式信息 ---
|
||||||
|
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),
|
||||||
|
# [修复] 补充读取缩进属性,防止后面恢复时 KeyError
|
||||||
|
'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)
|
||||||
|
}
|
||||||
|
paragraph_format['font_info'].append(run_format)
|
||||||
|
|
||||||
|
original_paragraph_formats.append(paragraph_format)
|
||||||
|
|
||||||
|
# --- 2. 设置新文本内容 ---
|
||||||
|
shape.text = 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 # 防止空列表
|
||||||
|
|
||||||
|
original_para = original_paragraph_formats[orig_idx]
|
||||||
|
|
||||||
|
# 恢复段落格式
|
||||||
|
if original_para['alignment'] is not None:
|
||||||
|
paragraph.alignment = original_para['alignment']
|
||||||
|
if original_para['space_before'] is not None:
|
||||||
|
paragraph.space_before = original_para['space_before']
|
||||||
|
if original_para['space_after'] is not None:
|
||||||
|
paragraph.space_after = original_para['space_after']
|
||||||
|
if original_para['line_spacing'] is not None:
|
||||||
|
paragraph.line_spacing = original_para['line_spacing']
|
||||||
|
if original_para['left_indent'] is not None:
|
||||||
|
paragraph.left_indent = original_para['left_indent']
|
||||||
|
if original_para['right_indent'] is not None:
|
||||||
|
paragraph.right_indent = original_para['right_indent']
|
||||||
|
if original_para['first_line_indent'] is not None:
|
||||||
|
paragraph.first_line_indent = original_para['first_line_indent']
|
||||||
|
|
||||||
|
# 恢复字体格式 (尽量应用到所有 runs)
|
||||||
|
# 注意:shape.text = text 会把所有内容变成一个 run,但也可能有多个
|
||||||
|
for j, run in enumerate(paragraph.runs):
|
||||||
|
# 通常取第一个run的格式,或者按顺序取
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 字体名称
|
||||||
|
if original_font['font_name'] is not None:
|
||||||
|
font_name = original_font['font_name']
|
||||||
|
if is_font_available(font_name):
|
||||||
|
run.font.name = font_name
|
||||||
|
else:
|
||||||
|
run.font.name = "微软雅黑"
|
||||||
|
# print(f"警告: 字体 '{font_name}' 不可用,已替换")
|
||||||
|
|
||||||
|
# 其他属性
|
||||||
|
if original_font['font_size'] is not None:
|
||||||
|
run.font.size = original_font['font_size']
|
||||||
|
if original_font['bold'] is not None:
|
||||||
|
run.font.bold = original_font['bold']
|
||||||
|
if original_font['italic'] is not None:
|
||||||
|
run.font.italic = original_font['italic']
|
||||||
|
if original_font['underline'] is not None:
|
||||||
|
run.font.underline = original_font['underline']
|
||||||
|
|
||||||
|
if original_font['character_space'] is not None:
|
||||||
|
try:
|
||||||
|
run.font.space = original_font['character_space']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if original_font['all_caps'] is not None:
|
||||||
|
run.font.all_caps = original_font['all_caps']
|
||||||
|
|
||||||
|
# 颜色处理
|
||||||
|
if original_font['color'] is not None:
|
||||||
|
try:
|
||||||
|
# 仅当它是RGB类型时才复制,主题色可能导致报错
|
||||||
|
if hasattr(original_font['color'], 'rgb'):
|
||||||
|
run.font.color.rgb = original_font['color'].rgb
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||||
|
"""
|
||||||
|
在指定幻灯片中替换指定占位符的图片。
|
||||||
|
:param prs: Presentation 对象
|
||||||
|
:param slide_index: 要操作的幻灯片索引(从0开始)
|
||||||
|
:param placeholder: 占位符名称
|
||||||
|
:param img_path: 要替换的图片路径
|
||||||
|
"""
|
||||||
|
if not os.path.exists(img_path):
|
||||||
|
print(f"警告: 图片路径不存在 {img_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
slide = prs.slides[slide_index]
|
||||||
|
sp_tree = slide.shapes._spTree
|
||||||
|
|
||||||
|
# 先找到要替换的形状及其索引位置
|
||||||
|
for i, shape in enumerate(slide.shapes):
|
||||||
|
if shape.name == placeholder:
|
||||||
|
# 保存旧形状的位置信息
|
||||||
|
left = shape.left
|
||||||
|
top = shape.top
|
||||||
|
width = shape.width
|
||||||
|
height = shape.height
|
||||||
|
|
||||||
|
# 从幻灯片中移除旧图片占位符
|
||||||
|
sp_tree.remove(shape._element)
|
||||||
|
|
||||||
|
# 添加新图片
|
||||||
|
new_shape = slide.shapes.add_picture(img_path, left, top, width, height)
|
||||||
|
|
||||||
|
# 插入新图片到旧形状原来的位置
|
||||||
|
sp_tree.insert(i, new_shape._element)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# --- 1. 资源准备 ---
|
||||||
|
# 安装字体(如果存在fonts目录)
|
||||||
|
if install_fonts_from_directory("../fonts"):
|
||||||
|
print("等待系统识别新安装的字体...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
source_file = r"../templates/大班幼儿学期发展报告.pptx"
|
||||||
|
output_folder = "output"
|
||||||
|
excel_file = os.path.join("../data/names.xlsx")
|
||||||
|
image_folder = os.path.join("../data/images")
|
||||||
|
|
||||||
|
# 创建输出文件夹
|
||||||
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# 检查源文件是否存在
|
||||||
|
if not os.path.exists(source_file):
|
||||||
|
print(f"错误: 找不到模版文件 {source_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.exists(excel_file):
|
||||||
|
print(f"错误: 找不到数据文件 {excel_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2. 定义辅助函数 (显式传入 prs) ---
|
||||||
|
def replace_one_page(current_prs, name, class_name):
|
||||||
|
"""替换第一页信息"""
|
||||||
|
replace_text_in_slide(current_prs, 0, "name", name)
|
||||||
|
replace_text_in_slide(current_prs, 0, "class", class_name)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_two_page(current_prs, comments, teacher_name):
|
||||||
|
"""替换第二页信息"""
|
||||||
|
replace_text_in_slide(current_prs, 1, "comments", comments)
|
||||||
|
replace_text_in_slide(current_prs, 1, "teacher_name", teacher_name)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_three_page(current_prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image):
|
||||||
|
"""替换第三页信息
|
||||||
|
:param current_prs: Presentation对象,当前演示文稿
|
||||||
|
:param name: 学生姓名
|
||||||
|
:param english_name: 学生英文名
|
||||||
|
:param sex: 性别
|
||||||
|
:param birthday: 生日
|
||||||
|
:param zodiac: 生肖
|
||||||
|
:param friend: 好朋友
|
||||||
|
:param hobby: 爱好
|
||||||
|
:param game: 爱玩的游戏
|
||||||
|
:param food: 爱吃的食物
|
||||||
|
:param me_image: 自己的照片
|
||||||
|
"""
|
||||||
|
replace_text_in_slide(current_prs, 2, "name", name)
|
||||||
|
replace_text_in_slide(current_prs, 2, "english_name", english_name)
|
||||||
|
replace_text_in_slide(current_prs, 2, "sex", sex)
|
||||||
|
replace_text_in_slide(current_prs, 2, "birthday", birthday)
|
||||||
|
replace_text_in_slide(current_prs, 2, "zodiac", zodiac)
|
||||||
|
replace_text_in_slide(current_prs, 2, "friend", friend)
|
||||||
|
replace_text_in_slide(current_prs, 2, "hobby", hobby)
|
||||||
|
replace_text_in_slide(current_prs, 2, "game", game)
|
||||||
|
replace_text_in_slide(current_prs, 2, "food", food)
|
||||||
|
replace_picture(current_prs, 2, "me_image", me_image)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_four_page(current_prs, class_image):
|
||||||
|
"""替换第四页信息"""
|
||||||
|
replace_picture(current_prs, 3, "class_image", class_image)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_five_page(current_prs, image1, image2):
|
||||||
|
"""替换第五页信息"""
|
||||||
|
replace_picture(current_prs, 4, "image1", image1)
|
||||||
|
replace_picture(current_prs, 4, "image2", image2)
|
||||||
|
|
||||||
|
|
||||||
|
# --- 3. 读取数据并处理 ---
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(excel_file, sheet_name="Sheet1")
|
||||||
|
datas = df[["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
|
||||||
|
"评价"]].values.tolist()
|
||||||
|
|
||||||
|
class_name = "K4D"
|
||||||
|
teacher_name = ["简蜜", "王敏千", "李玉香"]
|
||||||
|
|
||||||
|
print(f"开始处理,共 {len(datas)} 位学生...")
|
||||||
|
|
||||||
|
# --- 4. 循环处理 ---
|
||||||
|
for i, (name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) in enumerate(datas):
|
||||||
|
|
||||||
|
# 班级图片
|
||||||
|
|
||||||
|
print(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
||||||
|
|
||||||
|
# [修复] 每次循环重新加载模版,保证文件干净
|
||||||
|
prs = Presentation(source_file)
|
||||||
|
|
||||||
|
# 替换第一页内容
|
||||||
|
replace_one_page(prs, name, class_name)
|
||||||
|
|
||||||
|
# 替换第二页内容
|
||||||
|
teacher_names = " ".join(teacher_name)
|
||||||
|
replace_two_page(prs, comments, teacher_names)
|
||||||
|
|
||||||
|
# 替换第三页内容
|
||||||
|
student_image_folder = os.path.join(image_folder, name)
|
||||||
|
if os.path.exists(student_image_folder):
|
||||||
|
me_image = os.path.join(student_image_folder, "me_image.jpg")
|
||||||
|
replace_three_page(prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image)
|
||||||
|
else:
|
||||||
|
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||||
|
|
||||||
|
# 替换第四页内容
|
||||||
|
class_image = os.path.join(image_folder, class_name + ".jpg")
|
||||||
|
if os.path.exists(class_image):
|
||||||
|
replace_four_page(prs, class_image)
|
||||||
|
else:
|
||||||
|
print(f"错误: 班级图片文件不存在 {class_image}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 替换第五页内容
|
||||||
|
if os.path.exists(student_image_folder):
|
||||||
|
image1 = os.path.join(student_image_folder, "1.jpg")
|
||||||
|
image2 = os.path.join(student_image_folder, "1.jpg")
|
||||||
|
replace_five_page(prs, image1, image2)
|
||||||
|
else:
|
||||||
|
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||||
|
|
||||||
|
# 获取文件拓展名
|
||||||
|
file_ext = os.path.splitext(source_file)[1]
|
||||||
|
safe_name = str(name).strip() # 去除可能存在的空格
|
||||||
|
new_filename = f"{class_name} {safe_name} 幼儿成长报告{file_ext}"
|
||||||
|
output_path = os.path.join(output_folder, new_filename)
|
||||||
|
|
||||||
|
# 保存
|
||||||
|
try:
|
||||||
|
prs.save(output_path)
|
||||||
|
except PermissionError:
|
||||||
|
print(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
|
||||||
|
|
||||||
|
print("\n所有报告生成完毕!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"程序运行出错: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
BIN
output/K4D 罗槿祎 幼儿成长报告.pptx
Normal file
BIN
output/K4D 黄诗雅 幼儿成长报告.pptx
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[project]
|
||||||
|
name = "growth-report"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"comtypes>=1.4.13",
|
||||||
|
"langchain>=1.1.3",
|
||||||
|
"langchain-openai>=1.1.1",
|
||||||
|
"loguru>=0.7.3",
|
||||||
|
"openpyxl>=3.1.5",
|
||||||
|
"pandas>=2.3.3",
|
||||||
|
"pandas-stubs==2.3.3.251201",
|
||||||
|
"python-pptx>=1.0.2",
|
||||||
|
"rich>=14.2.0",
|
||||||
|
"tomli>=2.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "aliyun"
|
||||||
|
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||||
BIN
templates/大班幼儿学期发展报告.pptx
Normal file
50
utils/agent_utils.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from langchain_core.output_parsers import StrOutputParser
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from config.config import load_config
|
||||||
|
|
||||||
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_comment(name, age_group, traits):
|
||||||
|
"""
|
||||||
|
生成评语
|
||||||
|
:param name: 学生姓名
|
||||||
|
:param age_group: 所在班级
|
||||||
|
:param traits: 表现特征
|
||||||
|
:return: 评语
|
||||||
|
"""
|
||||||
|
|
||||||
|
ai_config = config["ai"]
|
||||||
|
llm = ChatOpenAI(
|
||||||
|
base_url=ai_config["api_url"],
|
||||||
|
api_key=ai_config["api_key"],
|
||||||
|
model=ai_config["model"],
|
||||||
|
temperature=0.7,
|
||||||
|
)
|
||||||
|
# 2. 构建 Prompt Template
|
||||||
|
prompt = ChatPromptTemplate.from_messages([
|
||||||
|
("system", ai_config["prompt"]),
|
||||||
|
("human", "学生姓名:{name}\n所在班级:{age_group}\n表现特征:{traits}\n\n请开始撰写评语:")
|
||||||
|
])
|
||||||
|
|
||||||
|
# 3. 组装链 (Prompt -> Model -> OutputParser)
|
||||||
|
chain = prompt | llm | StrOutputParser()
|
||||||
|
|
||||||
|
# 4. 执行
|
||||||
|
try:
|
||||||
|
comment = chain.invoke({
|
||||||
|
"name": name,
|
||||||
|
"age_group": age_group,
|
||||||
|
"traits": traits
|
||||||
|
})
|
||||||
|
cleaned_text = re.sub(r'\s+', '', comment)
|
||||||
|
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
|
||||||
|
return cleaned_text
|
||||||
|
except Exception as e:
|
||||||
|
print(f"生成评语失败: {e}")
|
||||||
|
return "生成失败,请检查网络或Key。"
|
||||||
75
utils/font_utils.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# ==========================================
|
||||||
|
# 2. 字体工具函数 (Font Utilities)
|
||||||
|
# ==========================================
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_fonts():
|
||||||
|
"""获取系统中可用的字体列表"""
|
||||||
|
fonts = set()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
try:
|
||||||
|
system_fonts_dir = Path(os.environ['WINDIR']) / 'Fonts'
|
||||||
|
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||||
|
|
||||||
|
for folder in [system_fonts_dir, user_fonts_dir]:
|
||||||
|
if folder.exists():
|
||||||
|
for ext in ['*.ttf', '*.ttc', '*.otf']:
|
||||||
|
for font_file in folder.glob(ext):
|
||||||
|
fonts.add(font_file.stem)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取系统字体时出错: {e}")
|
||||||
|
fonts = {"微软雅黑", "宋体", "黑体", "Arial", "Microsoft YaHei"}
|
||||||
|
return fonts
|
||||||
|
|
||||||
|
|
||||||
|
def is_font_available(font_name):
|
||||||
|
"""检查字体是否在系统中可用"""
|
||||||
|
system_fonts = get_system_fonts()
|
||||||
|
check_names = [font_name, font_name.replace(" ", ""), font_name.replace("-", ""),
|
||||||
|
font_name.lower(), font_name.upper()]
|
||||||
|
for name in check_names:
|
||||||
|
if name in system_fonts:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_fonts_from_directory(fonts_dir="fonts"):
|
||||||
|
"""从指定目录安装字体到系统"""
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
print("字体安装功能目前仅支持Windows系统")
|
||||||
|
return False
|
||||||
|
|
||||||
|
target_font_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||||
|
target_font_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not os.path.exists(fonts_dir):
|
||||||
|
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
|
||||||
|
return False
|
||||||
|
|
||||||
|
font_files = []
|
||||||
|
for ext in ['.ttf', '.ttc', '.otf', '.fon']:
|
||||||
|
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
|
||||||
|
|
||||||
|
if not font_files:
|
||||||
|
print(f"在 {fonts_dir} 目录中未找到字体文件")
|
||||||
|
return False
|
||||||
|
|
||||||
|
installed_count = 0
|
||||||
|
for font_file in font_files:
|
||||||
|
try:
|
||||||
|
target_path = target_font_dir / font_file.name
|
||||||
|
if not target_path.exists():
|
||||||
|
shutil.copy2(font_file, target_path)
|
||||||
|
print(f"已安装字体: {font_file.name}")
|
||||||
|
installed_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
|
||||||
|
|
||||||
|
if installed_count > 0:
|
||||||
|
print(f"共安装了 {installed_count} 个新字体文件,建议重启Python环境")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
95
utils/pef_utils.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
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 已关闭,批量转换完成。")
|
||||||
111
utils/pptx_utils.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# ==========================================
|
||||||
|
# 3. PPT 通用工具 (PPT Utilities)
|
||||||
|
# ==========================================
|
||||||
|
import os
|
||||||
|
|
||||||
|
from utils.font_utils import is_font_available
|
||||||
|
|
||||||
|
|
||||||
|
def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||||
|
"""在指定幻灯片中替换指定占位符的文本,并保持原有格式"""
|
||||||
|
slide = prs.slides[slide_index]
|
||||||
|
for shape in slide.shapes:
|
||||||
|
if shape.name == placeholder:
|
||||||
|
if not shape.has_text_frame:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. 保存原有格式
|
||||||
|
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': []
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
paragraph_format['font_info'].append(run_format)
|
||||||
|
original_paragraph_formats.append(paragraph_format)
|
||||||
|
|
||||||
|
# 2. 设置新文本
|
||||||
|
shape.text = str(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
|
||||||
|
|
||||||
|
original_para = original_paragraph_formats[orig_idx]
|
||||||
|
|
||||||
|
# 恢复段落属性
|
||||||
|
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
|
||||||
|
|
||||||
|
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']
|
||||||
|
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['character_space']:
|
||||||
|
try:
|
||||||
|
run.font.space = original_font['character_space']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if original_font['color']:
|
||||||
|
try:
|
||||||
|
if hasattr(original_font['color'], 'rgb'):
|
||||||
|
run.font.color.rgb = original_font['color'].rgb
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||||
|
"""在指定幻灯片中替换指定占位符的图片"""
|
||||||
|
if not os.path.exists(img_path):
|
||||||
|
print(f"警告: 图片路径不存在 {img_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
slide = prs.slides[slide_index]
|
||||||
|
sp_tree = slide.shapes._spTree
|
||||||
|
|
||||||
|
for i, shape in enumerate(slide.shapes):
|
||||||
|
if shape.name == placeholder:
|
||||||
|
left, top, width, height = shape.left, shape.top, shape.width, shape.height
|
||||||
|
sp_tree.remove(shape._element)
|
||||||
|
new_shape = slide.shapes.add_picture(img_path, left, top, width, height)
|
||||||
|
sp_tree.insert(i, new_shape._element)
|
||||||
|
break
|
||||||