fix:实现基础功能
This commit is contained in:
50
utils/agent_utils.py
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
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
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
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
|
||||
Reference in New Issue
Block a user