活动介绍

word无法创建工作文件,请检查临时环境变量

时间: 2023-05-24 10:07:19 浏览: 815
对不起,我是一个语言模型,无法确定您使用Word的上下文。不过,我建议您检查以下内容: 1. 检查您的计算机上的临时文件夹是否存在,并且您具有适当的访问权限。 2. 检查您的计算机上的磁盘空间是否充足。 3. 尝试清空您的临时文件夹并重新启动Word。 4. 如果上述解决方案无效,请尝试卸载并重新安装Microsoft Office。
相关问题

Word无法创建工作文件,请检查临时环境变量

### Word 无法创建临时文件的解决方案 在处理 Word 无法创建临时文件的问题时,需要从多个角度分析问题的可能原因,并采取相应的解决措施。以下是对该问题的专业分析和解决方案。 #### 1. 环境变量配置检查 环境变量配置错误可能导致 Word 在生成临时文件时失败。根据引用内容[^3],可以检查是否正确设置了必要的环境变量。例如,确保 `DASHSCOPE_API_KEY` 或其他相关环境变量已正确添加到系统的全局配置中。如果使用的是 Linux 系统,可以通过以下命令检查和设置环境变量: ```bash # 检查当前环境变量 echo $DASHSCOPE_API_KEY # 如果未设置,则添加到 ~/.bashrc 中 echo "export DASHSCOPE_API_KEY='YOUR_DASHSCOPE_API_KEY'" >> ~/.bashrc # 刷新配置 source ~/.bashrc ``` 确保所有与 Word 文档生成相关的环境变量都已正确配置。 #### 2. 临时文件路径权限检查 Word 在生成临时文件时,通常会将文件存储在系统指定的临时目录中。如果该目录的权限设置不正确,可能会导致无法创建临时文件。可以按照以下步骤检查和修复: - **Windows 系统**:默认临时文件路径为 `%TEMP%` 和 `%TMP%`。确保这些路径存在且具有写入权限。 - **Linux/Mac 系统**:默认临时文件路径为 `/tmp` 或 `/var/tmp`。可以通过以下命令检查权限: ```bash ls -ld /tmp ``` 确保 `/tmp` 目录具有 `drwxrwxrwt` 权限,表示所有用户都可以读取、写入和执行操作。 #### 3. WordUtil 类的异常捕获与清理机制 根据引用内容[^1],`WordUtil` 类通过静态方法封装了 FreeMarker 模板引擎的功能,并实现了临时文件的创建与清理。如果 Word 无法创建临时文件,可能是由于以下原因: - 临时文件路径未正确指定。 - 异常捕获机制未能有效处理错误。 可以尝试修改 `WordUtil` 类的实现,确保临时文件路径明确指定,并在异常发生时提供详细的错误日志。例如: ```python import tempfile import os def generate_document(template_name, data_model): try: # 创建临时文件 temp_dir = tempfile.gettempdir() temp_file_path = os.path.join(temp_dir, f"temp_{template_name}.docx") # 确保临时目录存在 if not os.path.exists(temp_dir): os.makedirs(temp_dir) # 使用 UTF-8 编码保存文档 with open(temp_file_path, 'w', encoding='utf-8') as file: file.write("Document content based on data model.") return temp_file_path except Exception as e: print(f"Error creating temporary file: {e}") return None ``` #### 4. 批量文档处理中的潜在问题 根据引用内容[^2],项目中实现了基于批量文档的问答功能。如果在处理多个文档时出现临时文件创建失败的问题,可能是由于并发操作或资源限制导致。可以尝试以下优化措施: - 增加对临时文件的唯一性检查,避免文件名冲突。 - 在多线程或多进程环境中,确保每个线程或进程使用的临时文件路径独立。 #### 5. 日志记录与调试 为了更准确地定位问题,建议在程序中增加详细的日志记录功能。例如,使用 Python 的 `logging` 模块记录临时文件的创建过程: ```python import logging logging.basicConfig(level=logging.DEBUG) def create_temp_file(): try: temp_file = tempfile.NamedTemporaryFile(delete=False) logging.debug(f"Temporary file created at: {temp_file.name}") return temp_file.name except Exception as e: logging.error(f"Failed to create temporary file: {e}") return None ``` --- ### 总结 Word 无法创建临时文件的问题可能涉及环境变量配置错误、临时文件路径权限不足、程序异常处理不当等多种原因。通过检查环境变量、确保临时文件路径权限正确、优化 `WordUtil` 类的实现以及增加日志记录功能,可以有效解决该问题。

word无法创建工作文件,请检查临时环境变量。

### Word无法创建工作文件的解决方案 当用户在运行Word或打开Word文档时,如果收到“Word无法创建工作文件,请检查临时环境变量”的错误提示,这通常是因为用户的Word配置出现问题或是系统临时文件路径不可用所致。以下是几种有效的解决方案: #### 方法一:通过`/a`参数启动Word 可以通过命令行参数绕过加载损坏的数据注册表子项来解决问题。 1. 打开“运行”对话框(按 `Win + R` 键),输入 `WinWord.exe /a` 并回车[^2]。 - `/a` 参数表示禁用自动恢复和加载自定义设置,从而允许Word以干净状态启动。 #### 方法二:清理并重建注册表项 此方法涉及修改系统的注册表,需谨慎操作。 1. **备份注册表**:在执行任何更改之前,建议先备份注册表以防万一发生意外情况。 2. 按下 `Win + R` 组合键,输入 `regedit` 启动注册表编辑器。 3. 定位到以下路径: ``` HKEY_CURRENT_USER\Software\Microsoft\Office\<版本号>\Word\Data ``` (对于 Office 2007 或更高版本,<版本号>可能是 `12.0`, 对于 Office 2003 则为 `11.0`) 4. 删除 `Data` 下的所有子项,例如 `Settings` 和 `Toolbars`。 5. 关闭注册表编辑器后重新启动计算机,并尝试再次打开Word。 #### 方法三:修复默认模板文件 有时,默认模板文件 (`Normal.dot`) 受损也会引发此类问题。可以按照以下步骤处理: 1. 查找默认模板的位置,通常是: ``` C:\Documents and Settings\<用户名>\Application Data\Microsoft\Templates\ ``` 或者在 Windows Vista/7 中为: ``` C:\Users\<用户名>\AppData\Roaming\Microsoft\Templates\ ``` 2. 将名为 `Normal.dot` 的文件重命名为其他名称,例如 `Normal_old.dot`[^2]。 3. 重启Word,它会自动生成一个新的默认模板文件。 #### 方法四:验证临时文件夹权限 确保操作系统中的临时文件夹具有正确的读写权限。 1. 压下 `Win + R` 键组合,输入 `%temp%` 来定位当前用户的临时文件夹。 2. 验证是否有足够的磁盘空间以及对该文件夹拥有完全控制权。 3. 如果怀疑是环境变量的问题,则可以在“系统属性 -> 高级 -> 环境变量”中确认是否存在有效且可写的 TEMP 和 TMP 路径[^1]。 #### 方法五:调整Internet选项中的缓存设置 尽管看起来与浏览器相关联,但某些情况下 Internet Explorer 的缓存设定也会影响 Microsoft Office 应用程序的行为。因此也可以考虑如下措施: 1. 使用快捷键 `Win + R` 输入 `inetcpl.cpl` 进入互联网属性面板。 2. 导航至“常规”标签页下的浏览历史部分点击【设置】按钮。 3. 在弹出的新窗口里选择“移动文件夹”,指定另一个可用驱动器作为新目标位置存放临时internet 文件[^3]。 以上多种途径都可以帮助解决因各种原因引起的 “Word无法创建工作文件,请检查临时环境变量” 报错现象。 ```python import os print(os.environ['TEMP']) # 输出当前系统的临时目录路径以便排查问题所在 ```
阅读全文

相关推荐

#!/bin/bash IFS=$' \t\n' word_file="words.txt" difficult_file="difficult.txt" wrong_file="wrong.txt" # 创建必要的文件 create_files() { for file in "$word_file" "$difficult_file" "$wrong_file"; do if [[ ! -f "$file" ]]; then touch "$file" fi done } # 计算单词数量 count_words() { word_count=$(wc -l < "$word_file") difficult_count=$(wc -l < "$difficult_file") wrong_count=$(wc -l < "$wrong_file") echo "当前单词数量: $word_count" echo "难词数量: $difficult_count" echo "错误单词数量: $wrong_count" } # 添加单词 add_word() { echo "请输入单词:" read word if [[ -z "$word" ]]; then echo "单词不能为空!" return fi echo "请输入该单词的汉语释义:" read meaning if [[ -z "$meaning" ]]; then echo "释义不能为空!" return fi # 检查单词是否已存在 if grep -q "^$word:" "$word_file"; then echo "该单词已存在!" return fi echo "$word $meaning" >> "$word_file" echo "单词添加成功!" } # 查找单词 find_word() { echo "请输入要查找的单词:" read word if [[ -z "$word" ]]; then echo "输入不能为空!" return fi result=$(grep -i "^$word:" "$word_file") if [[ -z "$result" ]]; then echo "没有找到该单词的释义" else echo "单词释义:${result#*:}" fi } # 随机显示单词 random_word() { local file=$1 if [[ -z "$file" ]]; then file="$word_file" fi if [[ ! -s "$file" ]]; then echo "文件为空!" return fi random_line=$(shuf -n 1 "$file") echo "随机单词: ${random_line%:*} - ${random_line#*:}" } # 复习单词 review_words() { echo "请选择复习类型:" echo "1. 全部单词" echo "2. 难词" echo "3. 错误单词" read choice local file_to_review case $choice in 1) file_to_review="$word_file" ;; 2) file_to_review="$difficult_file" ;; 3) file_to_review="$wrong_file" ;; *) echo "无效选项,返回主菜单" return ;; esac if [[ ! -s "$file_to_review" ]]; then echo "所选文件为空!" return fi echo "复习中,请按Ctrl+C退出。" while true; do random_word "$file_to_review" sleep 2 done } # 删除单词 delete_word() { echo "请输入要删除的单词:" read word if [[ -z "$word" ]]; then echo "输入不能为空!" return fi if ! grep -q "^$word:" "$word_file"; then echo "该单词不存在!" return fi sed -i "/^$word:/d" "$word_file" # 也从难词和错误词文件中删除 sed -i "/^$word:/d" "$difficult_file" sed -i "/^$word:/d" "$wrong_file" echo "单词已删除!" } # 修改单词 alter_word() { echo "请输入要修改的单词:" read word if [[ -z "$word" ]]; then echo "输入不能为空!" return fi if ! grep -q "^$word:" "$word_file"; then echo "该单词不存在!" return fi echo "请输入新的释义:" read new_meaning if [[ -z "$new_meaning" ]]; then echo "释义不能为空!" return fi sed -i "s/^$word:.*/$word:$new_meaning/" "$word_file" # 也更新难词和错误词文件中的释义 sed -i "s/^$word:.*/$word:$new_meaning/" "$difficult_file" sed -i "s/^$word:.*/$word:$new_meaning/" "$wrong_file" echo "单词释义已修改!" } # 单词默写 practice_word() { if [[ ! -s "$word_file" ]]; then echo "单词库为空,无法进行默写!" return fi echo "默写单词,输入正确的释义(中译英)。" echo "输入 'q' 退出默写。" # 打乱单词顺序 temp_file=$(mktemp) shuf "$word_file" > "$temp_file" while read -r word meaning; do echo -n "请输入 '$meaning' 的英文单词: " read -t 7 answer read_status=$? # 保存read命令的退出状态 # 测试answer的值 echo -e "\n--- 调试信息 ---" echo "变量answer的内容: '$answer'" echo "变量\$answer的内容: '${answer}'" echo "read命令退出状态: $read_status" echo "----------------" if [[ "$answer" == "q" ]]; then break fi # 处理超时情况 (read命令返回124表示超时) if [[ $read_status -eq 124 ]]; then echo "超时!正确答案是: $word" grep -q "^$word:" "$wrong_file" || echo "$word:$meaning" >> "$wrong_file" continue fi # 修正条件判断 if [[ "$answer" == "$word" ]]; then echo "正确!" else echo " 错误!正确答案是: $word" # 将错误的单词添加到错误单词文件中 grep -q "^$word:" "$wrong_file" || echo "$word:$meaning" >> "$wrong_file" fi done < "$temp_file" rm -f "$temp_file" } add_to_difficult() { echo "请输入要添加到难词列表的单词:" read word if [[ -z "$word" ]]; then echo "输入不能为空!" return fi if ! grep -q "^$word:" "$word_file"; then echo "该单词不存在于单词库中!" return fi # 获取单词的释义 meaning=$(grep "^$word:" "$word_file" | cut -d: -f2-) # 检查是否已在难词列表中 if grep -q "^$word:" "$difficult_file"; then echo "该单词已经在难词列表中!" return fi echo "$word:$meaning" >> "$difficult_file" echo "单词已添加到难词列表!" } # 主菜单 menu() { create_files while true; do echo echo " ****************************" echo " * 单词小助手 *" echo " ****************************" echo " * 操作选项: *" echo " * 1. 添加单词 *" echo " * 2. 删除单词 *" echo " * 3. 修改单词 *" echo " * 4. 查找单词释义 *" echo " * 5. 随机输出单词 *" echo " * 6. 复习单词 *" echo " * 7. 单词默写(中译英) *" echo " * 8. 添加到难词列表 *" echo " * 9. 统计单词数量 *" echo " * 0. 退出 *" echo " ****************************" echo -n "请输入你的选项:" read choice case $choice in 1) add_word ;; 2) delete_word ;; 3) alter_word ;; 4) find_word ;; 5) random_word ;; 6) review_words ;; 7) practice_word ;; 8) add_to_difficult ;; 9) count_words ;; 0) echo "感谢使用单词小助手,再见!" exit 0 ;; *) echo "无效选项,请重新选择。" ;; esac done } menu

import os import sys import logging import time from pathlib import Path from concurrent.futures import ThreadPoolExecutor from tqdm import tqdm import tkinter as tk from tkinter import filedialog, messagebox, ttk from tkinterdnd2 import DND_FILES, TkinterDnD import json from pdf2docx import Converter import pytesseract from pdf2image import convert_from_path from docx import Document from docx.shared import Pt, Cm, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH import numexpr as ne import tempfile import traceback import io import threading import queue # 获取程序运行路径 def get_application_path(): """获取应用程序路径""" if getattr(sys, 'frozen', False): # 如果是打包后的exe return os.path.dirname(sys.executable) else: # 如果是直接运行的python脚本 return os.path.dirname(os.path.abspath(__file__)) # 设置NumExpr线程数 ne.set_num_threads(8) # 配置文件路径 CONFIG_FILE = os.path.join(get_application_path(), "config.json") # 日志文件路径 LOG_FILE = os.path.join(get_application_path(), "conversion.log") # 自定义StreamHandler来捕获所有输出 class StreamToLogger(io.StringIO): def __init__(self, logger, level): super().__init__() self.logger = logger self.level = level self.buf = '' def write(self, buf): self.buf = buf.strip('\r\n\t ') if self.buf: self.logger.log(self.level, self.buf) def flush(self): pass def setup_logging(): """配置日志系统""" try: app_path = get_application_path() log_dir = os.path.join(app_path, 'logs') os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, 'conversion.log') # 创建logger logger = logging.getLogger() logger.setLevel(logging.DEBUG) # 创建文件处理器 file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8') file_handler.setLevel(logging.DEBUG) # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 创建格式化器 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 logger.addHandler(file_handler) logger.addHandler(console_handler) logging.debug(f"日志系统初始化成功,日志文件路径:{log_file}") return logger except Exception as e: print(f"设置日志系统失败: {str(e)}") return None # 初始化日志记录器 logger = setup_logging() if not logger: print("警告:日志系统初始化失败,程序将继续运行但不会记录日志") # 创建一个基本的日志记录器 logger = logging.getLogger() logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) def load_config(): try: if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as f: config = json.load(f) # 自动修正Poppler路径 poppler_path = config.get('POPPLER_PATH', '') if poppler_path: # 检查是否存在bin子目录 bin_path = os.path.join(poppler_path, 'bin') if os.path.exists(bin_path): config['POPPLER_PATH'] = bin_path return config return {} except: return {} def save_config(config): """保存配置文件""" with open(CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=4) def setup_config(): """首次运行配置界面""" root = TkinterDnD.Tk() root.title("PDF转Word工具 - 首次配置") root.geometry("600x400") # 设置窗口样式 root.configure(bg='#f0f0f0') style = {'bg': '#f0f0f0', 'font': ('微软雅黑', 10)} button_style = {'bg': '#4CAF50', 'fg': 'white', 'font': ('微软雅黑', 10), 'padx': 10, 'pady': 5} # 创建说明标签 tk.Label(root, text="首次使用需要配置以下路径:", **style).pack(pady=10) # Poppler路径配置 poppler_frame = tk.Frame(root, bg='#f0f0f0') poppler_frame.pack(fill='x', padx=20, pady=5) tk.Label(poppler_frame, text="Poppler路径:", **style).pack(side='left') poppler_path = tk.StringVar() poppler_entry = tk.Entry(poppler_frame, textvariable=poppler_path, width=50) poppler_entry.pack(side='left', padx=5) def select_poppler(): path = filedialog.askdirectory(title="选择Poppler安装目录") if path: poppler_path.set(path) tk.Button(poppler_frame, text="浏览", command=select_poppler, **button_style).pack(side='left') # Tesseract路径配置 tesseract_frame = tk.Frame(root, bg='#f0f0f0') tesseract_frame.pack(fill='x', padx=20, pady=5) tk.Label(tesseract_frame, text="Tesseract路径:", **style).pack(side='left') tesseract_path = tk.StringVar() tesseract_entry = tk.Entry(tesseract_frame, textvariable=tesseract_path, width=50) tesseract_entry.pack(side='left', padx=5) def select_tesseract(): path = filedialog.askdirectory(title="选择Tesseract安装目录") if path: tesseract_path.set(path) tk.Button(tesseract_frame, text="浏览", command=select_tesseract, **button_style).pack(side='left') # 说明文本 help_text = """ 使用说明: 1. Poppler路径:选择poppler的安装目录(包含bin文件夹的目录) 2. Tesseract路径:选择Tesseract-OCR的安装目录 3. 配置完成后点击"保存配置"即可开始使用 4. Poppler和Tesseract需放在在系统C盘下(C:\Program Files\) """ tk.Label(root, text=help_text, justify='left', **style).pack(pady=10) def save_and_exit(): if not poppler_path.get() or not tesseract_path.get(): messagebox.showerror("错误", "请填写所有配置项!") return tesseract_dir = tesseract_path.get() tesseract_exe = os.path.join(tesseract_dir, 'tesseract.exe') # 新增:验证Tesseract路径有效性 if not os.path.isfile(tesseract_exe): messagebox.showerror("错误", "选择的Tesseract路径无效,未找到tesseract.exe!") return config = { 'POPPLER_PATH': poppler_path.get(), 'TESSERACT_CMD': tesseract_exe # 直接使用正确路径 } save_config(config) root.destroy() tk.Button(root, text="保存配置", command=save_and_exit, **button_style).pack(pady=20) root.mainloop() class PDFConverterGUI: def __init__(self): self.root = TkinterDnD.Tk() self.root.title("PDF转Word工具") self.root.geometry("800x600") # 设置样式 self.style = {'bg': '#f0f0f0', 'font': ('微软雅黑', 10)} self.button_style = {'bg': '#4CAF50', 'fg': 'white', 'font': ('微软雅黑', 10), 'padx': 10, 'pady': 5} # 绑定窗口关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.setup_ui() def on_closing(self): """处理窗口关闭事件""" try: if messagebox.askokcancel("退出", "确定要退出程序吗?"): self.root.destroy() sys.exit(0) except Exception as e: logger.error(f"关闭窗口时出错: {str(e)}") self.root.destroy() sys.exit(1) def setup_ui(self): # 创建主框架 main_frame = tk.Frame(self.root, bg='#f0f0f0') main_frame.pack(fill='both', expand=True, padx=20, pady=20) # 路径选择区域 path_frame = tk.Frame(main_frame, bg='#f0f0f0') path_frame.pack(fill='x', pady=10) tk.Label(path_frame, text="选择PDF文件或目录:", **self.style).pack(side='left') self.path_var = tk.StringVar() path_entry = tk.Entry(path_frame, textvariable=self.path_var, width=50) path_entry.pack(side='left', padx=5) tk.Button(path_frame, text="选择文件", command=self.select_file, **self.button_style).pack(side='left', padx=5) tk.Button(path_frame, text="选择目录", command=self.select_directory, **self.button_style).pack(side='left') # 文件列表区域 list_frame = tk.Frame(main_frame, bg='#f0f0f0') list_frame.pack(fill='both', expand=True, pady=10) tk.Label(list_frame, text="待处理文件列表(支持拖放文件):", **self.style).pack(anchor='w') # 创建带滚动条的列表框 self.listbox_frame = tk.Frame(list_frame, bg='#f0f0f0') self.listbox_frame.pack(fill='both', expand=True) self.file_listbox = tk.Listbox(self.listbox_frame, width=80, height=15) self.file_listbox.pack(side='left', fill='both', expand=True) scrollbar = tk.Scrollbar(self.listbox_frame) scrollbar.pack(side='right', fill='y') self.file_listbox.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.file_listbox.yview) # 绑定拖放事件 self.file_listbox.drop_target_register(DND_FILES) self.file_listbox.dnd_bind('<<Drop>>', self.handle_drop) # 进度条 self.progress_var = tk.DoubleVar() self.progress = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100) self.progress.pack(fill='x', pady=10) # 状态标签 self.status_var = tk.StringVar(value="就绪") tk.Label(main_frame, textvariable=self.status_var, **self.style).pack(pady=5) # 按钮区域 button_frame = tk.Frame(main_frame, bg='#f0f0f0') button_frame.pack(pady=10) tk.Button(button_frame, text="开始转换", command=self.start_conversion, **self.button_style).pack(side='left', padx=5) tk.Button(button_frame, text="清空列表", command=self.clear_list, **self.button_style).pack(side='left', padx=5) tk.Button(button_frame, text="退出", command=self.root.quit, **self.button_style).pack(side='left', padx=5) # 添加日志显示区域 log_frame = tk.Frame(main_frame, bg='#f0f0f0') log_frame.pack(fill='both', expand=True, pady=10) tk.Label(log_frame, text="转换日志:", **self.style).pack(anchor='w') # 创建带滚动条的日志文本框 self.log_text = tk.Text(log_frame, wrap=tk.WORD, width=80, height=10) scrollbar = tk.Scrollbar(log_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.log_text.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.log_text.yview) # 配置日志队列和更新机制 self.log_queue = queue.Queue() self.setup_log_handler() self.root.after(100, self.update_log_display) def update_log_display(self): """定期更新日志显示""" while not self.log_queue.empty(): msg = self.log_queue.get() self.log_text.configure(state='normal') self.log_text.insert(tk.END, msg + '\n') self.log_text.configure(state='disabled') # 自动滚动到底部 self.log_text.see(tk.END) self.root.after(100, self.update_log_display) def setup_log_handler(self): """配置GUI日志处理器""" class QueueHandler(logging.Handler): def __init__(self, queue): super().__init__() self.queue = queue def emit(self, record): msg = self.format(record) self.queue.put(msg) # 创建并添加自定义处理器 handler = QueueHandler(self.log_queue) handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) def handle_drop(self, event): """处理文件拖放事件""" files = event.data.split() for file in files: # 移除文件路径中的花括号(如果有) file = file.strip('{}') if file.lower().endswith('.pdf'): if file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, file) self.path_var.set(os.path.dirname(file)) elif os.path.isdir(file): # 如果是目录,则添加目录下的所有PDF文件 pdf_files = find_pdf_files(file) for pdf_file in pdf_files: if pdf_file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, pdf_file) self.path_var.set(file) def select_file(self): files = filedialog.askopenfilenames( title="选择PDF文件", filetypes=[("PDF文件", "*.pdf")] ) if files: for file in files: if file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, file) self.path_var.set(os.path.dirname(files[0])) def select_directory(self): directory = filedialog.askdirectory(title="选择包含PDF文件的目录") if directory: self.path_var.set(directory) self.clear_list() pdf_files = find_pdf_files(directory) for file in pdf_files: self.file_listbox.insert(tk.END, file) def clear_list(self): self.file_listbox.delete(0, tk.END) self.progress_var.set(0) self.status_var.set("就绪") def start_conversion(self): files = list(self.file_listbox.get(0, tk.END)) if not files: messagebox.showwarning("警告", "请先选择要转换的PDF文件!") return total_files = len(files) self.progress_var.set(0) def update_progress(current, total): try: progress = (current / total) * 100 self.progress_var.set(progress) self.status_var.set(f"正在处理: {current}/{total}") self.root.update() except Exception as e: logger.error(f"更新进度时出错: {str(e)}") for i, file in enumerate(files, 1): try: self.status_var.set(f"正在处理: {os.path.basename(file)}") self.root.update() if process_single_pdf(file): self.file_listbox.itemconfig(i - 1, {'bg': '#90EE90'}) # 浅绿色表示成功 else: self.file_listbox.itemconfig(i - 1, {'bg': '#FFB6C1'}) # 浅红色表示失败 update_progress(i, total_files) except Exception as e: logger.error(f"处理文件失败: {str(e)}", exc_info=True) self.file_listbox.itemconfig(i - 1, {'bg': '#FFB6C1'}) self.status_var.set("转换完成!") messagebox.showinfo("完成", f"处理完成!共处理 {total_files} 个文件。") def validate_environment(): missing_deps = [] # 检查Poppler路径 if not os.path.isdir(POPPLER_PATH): missing_deps.append(f"Poppler路径不存在: {POPPLER_PATH}") else: # 检查是否存在关键可执行文件 required_files = ['pdfinfo.exe', 'pdftoppm.exe'] for file in required_files: if not os.path.isfile(os.path.join(POPPLER_PATH, file)): missing_deps.append(f"Poppler路径中缺失文件: {file}") # 检查Tesseract是否存在且是可执行文件 if not os.path.isfile(TESSERACT_CMD) or not os.access(TESSERACT_CMD, os.X_OK): missing_deps.append(f"Tesseract路径不正确或不可执行: {TESSERACT_CMD}") if missing_deps: for dep in missing_deps: logger.error(dep) messagebox.showerror("配置错误", "环境验证失败:\n" + "\n".join(missing_deps) + "\n请检查Poppler是否安装正确") return False return True def extract_text_from_image(image): """从图片中提取文字""" try: # 配置OCR参数 custom_config = r'--oem 3 --psm 6 -l chi_sim' text = pytesseract.image_to_string(image, config=custom_config) return text except Exception as e: logger.error(f"OCR识别出错: {str(e)}", exc_info=True) return "" def process_single_pdf(pdf_path): """处理单个PDF文件""" temp_file = None temp_path = None try: start_time = time.time() pdf_name = os.path.basename(pdf_path) word_path = os.path.splitext(pdf_path)[0] + '.docx' logger.info(f"▶ 开始处理: {pdf_name}") # 基础转换 cv = Converter(pdf_path) cv.convert(word_path) cv.close() logger.info(" 基础转换完成") # OCR处理 images = convert_from_path(pdf_path, poppler_path=POPPLER_PATH) total_pages = len(images) logger.info(f" 共检测到 {total_pages} 页需要OCR识别") # 创建临时文件来存储OCR识别的文本 temp_path = os.path.join(tempfile.gettempdir(), f'ocr_text_{int(time.time())}.txt') temp_file = open(temp_path, 'w', encoding='utf-8') # 对每一页进行OCR识别 for idx, image in enumerate(images): logger.info(f" 正在处理第 {idx + 1}/{total_pages} 页") text = extract_text_from_image(image) if text.strip(): # 如果识别到文字 temp_file.write(f"第{idx + 1}页识别到的文字:\n{text}\n\n") # 确保所有内容都写入文件 temp_file.flush() temp_file.close() temp_file = None # 将OCR识别的文字添加到Word文档中 if os.path.exists(temp_path): with open(temp_path, 'r', encoding='utf-8') as f: ocr_text = f.read() if ocr_text.strip(): logger.info(" 正在将OCR识别的文字添加到Word文档...") # 打开已转换的Word文档 doc = Document(word_path) # 添加OCR识别的文字 doc.add_paragraph("\nOCR识别结果:") doc.add_paragraph(ocr_text) # 保存文档 doc.save(word_path) logger.info(f" 已添加OCR识别结果") cost_time = time.time() - start_time logger.info(f"✓ 处理完成,耗时 {cost_time:.2f} 秒\n") return True except Exception as e: logger.error(f"转换失败: {str(e)}", exc_info=True) return False finally: # 确保临时文件被关闭和删除 if temp_file and not temp_file.closed: try: temp_file.close() except: pass if temp_path and os.path.exists(temp_path): try: os.unlink(temp_path) except: pass def find_pdf_files(directory): """递归查找所有PDF文件""" pdf_files = [] try: for root, _, files in os.walk(directory): for file in files: if file.lower().endswith('.pdf'): full_path = os.path.join(root, file) pdf_files.append(full_path) return pdf_files except Exception as e: logger.error(f"文件搜索失败: {str(e)}") return [] def main(): try: print("程序启动...") logger.info("程序启动") # 检查配置文件 config = load_config() if not config: print("首次运行,请进行配置...") logger.info("首次运行,启动配置界面") setup_config() config = load_config() if not config: print("配置失败,程序退出") logger.error("配置失败,程序退出") return # 设置全局变量 global POPPLER_PATH, TESSERACT_CMD, TESSDATA_PREFIX POPPLER_PATH = config.get('POPPLER_PATH') TESSERACT_CMD = config.get('TESSERACT_CMD') TESSERACT_DIR = os.path.dirname(TESSERACT_CMD) TESSDATA_PREFIX = os.path.join(TESSERACT_DIR, 'tessdata') os.environ['TESSDATA_PREFIX'] = TESSDATA_PREFIX # 新增:设置pytesseract的tesseract路径 pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD # 添加这一行 print(f"Poppler路径: {POPPLER_PATH}") print(f"Tesseract路径: {TESSERACT_CMD}") # 验证环境 if not validate_environment(): print("环境验证失败") logger.error("环境验证失败") messagebox.showerror("错误", "环境配置不正确,请检查配置!") return # 启动GUI print("启动GUI界面...") logger.info("启动GUI界面") app = PDFConverterGUI() app.root.mainloop() except Exception as e: error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}" print(error_msg) logger.error(error_msg) messagebox.showerror("错误", f"程序运行出错:{str(e)}") finally: # 确保所有日志都被写入 for handler in logger.handlers: handler.flush() handler.close() if __name__ == "__main__": main()

我是一名移动运营商的工作人员,负责以电话外呼的方式开展客户满意度修复工作。现在需要利用python技术写一个关于外呼电话录音包批量质检分析的系统,需要封装成可执行的.exe程序。请给出详细的方案及切实可执行的详细完整代码(附注解),不要假设可行的伪代码场景,不要简化。以下为分析维度及要求: 一、要求: 1、输入框需包含:音频选择框(支持多选或选择文件夹)、关键词选择框(选择本地关键词.xlsx文件地址)。 2、程序界面需包含开始分析、停止分析、清空等按钮命令。 3、输出框需包含已完成分析、分析错误、分析报告储存路径等提示(分析报告生成一个汇总的结构化excel清单,一行代表一个文件,包含多个分析维度结果。同时生成一个汇总的可视化分析word版报告)。 4、音频文件可能有多种格式,如MP3、WAV、AMR等,需要兼容多种格式。 5、模型使用组合:语音识别:campp_enterprise_v2;说话人分离:campp_enterprise_v2;中文文本情感分析:campp_enterprise_v2。 6、所有模型均下载至本地离线加载使用。 7、录音包主要为客服与客户的对话,进行说话人分离时客服人员说话根据全部片段的前三个片段是否存在开场白关键字来确定身份。 8、模型优化内存资源,确保最后封装为程序时可在GPU专享内存为512M,共享内存为3.7G的个人笔记本中运行。 二、分析维度: 1、通话基本信息:音频时长、文件名称。 2、服务规范检查:开场白检查(根据关键字)、结束语检查(根据关键字)、服务禁语检查(根据关键字)。 3、客服服务态度情感分析:结合客服的文本情感分析,分析是否有不耐烦、生气等负面情绪。 4、客户情感分析:结合客户的文本情感分析客户整体情感极性(积极/消极/中性),特定情绪识别(如愤怒、不耐烦)。 5、沟通技巧检查:客服语速与清晰度分析、平均音量水平及稳定性。 6、问题解决率分析:客户问题是否被有效回答。

检查代码是否可运行,是否高效,是否可CPUimport sys import os import json import time import wave import numpy as np import pandas as pd import matplotlib.pyplot as plt import soundfile as sf from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, QFileDialog, QProgressBar, QGroupBox, QComboBox, QCheckBox, QMessageBox) from PyQt5.QtCore import QThread, pyqtSignal from pydub import AudioSegment from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification import whisper from pyannote.audio import Pipeline from docx import Document from docx.shared import Inches import librosa import tempfile from collections import defaultdict import re from concurrent.futures import ThreadPoolExecutor, as_completed import torch from torch.cuda import is_available as cuda_available import logging import gc # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 全局模型缓存 MODEL_CACHE = {} class AnalysisThread(QThread): progress = pyqtSignal(int) message = pyqtSignal(str) analysis_complete = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, audio_files, keyword_file, whisper_model_path, pyannote_model_path, emotion_model_path): super().__init__() self.audio_files = audio_files self.keyword_file = keyword_file self.whisper_model_path = whisper_model_path self.pyannote_model_path = pyannote_model_path self.emotion_model_path = emotion_model_path self.running = True self.cached_models = {} self.temp_files = [] # 用于管理临时文件 self.lock = torch.multiprocessing.Lock() # 用于模型加载的锁 def run(self): try: # 加载关键词 self.message.emit("正在加载关键词...") keywords = self.load_keywords() # 预加载模型 self.message.emit("正在预加载模型...") self.preload_models() results = [] total_files = len(self.audio_files) for idx, audio_file in enumerate(self.audio_files): if not self.running: self.message.emit("分析已停止") return self.message.emit(f"正在处理文件: {os.path.basename(audio_file)} ({idx + 1}/{total_files})") file_result = self.analyze_file(audio_file, keywords) if file_result: results.append(file_result) # 定期清理内存 if idx % 5 == 0: gc.collect() torch.cuda.empty_cache() if cuda_available() else None self.progress.emit(int((idx + 1) / total_files * 100)) self.analysis_complete.emit({"results": results, "keywords": keywords}) self.message.emit("分析完成!") except Exception as e: import traceback error_msg = f"分析过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.error.emit(error_msg) logger.error(error_msg) finally: # 清理临时文件 self.cleanup_temp_files() def cleanup_temp_files(self): """清理所有临时文件""" for temp_file in self.temp_files: if os.path.exists(temp_file): try: os.unlink(temp_file) except Exception as e: logger.warning(f"删除临时文件失败: {temp_file}, 原因: {str(e)}") def preload_models(self): """预加载所有模型到缓存(添加线程安全)""" global MODEL_CACHE # 使用锁确保线程安全 with self.lock: # 检查全局缓存是否已加载模型 if 'whisper' in MODEL_CACHE and 'pyannote' in MODEL_CACHE and 'emotion_classifier' in MODEL_CACHE: self.cached_models = MODEL_CACHE self.message.emit("使用缓存的模型") return self.cached_models = {} try: # 加载语音识别模型 if 'whisper' not in MODEL_CACHE: self.message.emit("正在加载语音识别模型...") MODEL_CACHE['whisper'] = whisper.load_model( self.whisper_model_path, device="cuda" if cuda_available() else "cpu" ) self.cached_models['whisper'] = MODEL_CACHE['whisper'] # 加载说话人分离模型 if 'pyannote' not in MODEL_CACHE: self.message.emit("正在加载说话人分离模型...") MODEL_CACHE['pyannote'] = Pipeline.from_pretrained( self.pyannote_model_path, use_auth_token=True ) self.cached_models['pyannote'] = MODEL_CACHE['pyannote'] # 加载情感分析模型 if 'emotion_classifier' not in MODEL_CACHE: self.message.emit("正在加载情感分析模型...") device = 0 if cuda_available() else -1 tokenizer = AutoTokenizer.from_pretrained(self.emotion_model_path) model = AutoModelForSequenceClassification.from_pretrained(self.emotion_model_path) # 尝试使用半精度浮点数减少内存占用 try: if device != -1: model = model.half() except Exception: pass # 如果失败则继续使用全精度 MODEL_CACHE['emotion_classifier'] = pipeline( "text-classification", model=model, tokenizer=tokenizer, device=device ) self.cached_models['emotion_classifier'] = MODEL_CACHE['emotion_classifier'] except Exception as e: raise Exception(f"模型加载失败: {str(e)}") def analyze_file(self, audio_file, keywords): """分析单个音频文件(优化内存使用)""" try: # 确保音频为WAV格式 wav_file, is_temp = self.convert_to_wav(audio_file) if is_temp: self.temp_files.append(wav_file) # 获取音频信息 duration, sample_rate, channels = self.get_audio_info(wav_file) # 说话人分离 - 使用较小的音频片段处理大文件 diarization = self.process_diarization(wav_file, duration) # 识别客服和客户 agent_segments, customer_segments = self.identify_speakers(wav_file, diarization, keywords['opening']) # 并行处理客服和客户音频 agent_result, customer_result = {}, {} with ThreadPoolExecutor(max_workers=2) as executor: agent_future = executor.submit( self.process_speaker_audio, wav_file, agent_segments, "客服" ) customer_future = executor.submit( self.process_speaker_audio, wav_file, customer_segments, "客户" ) agent_result = agent_future.result() customer_result = customer_future.result() # 情感分析 - 批处理提高效率 agent_emotion, customer_emotion = self.analyze_emotions( [agent_result.get('text', ''), customer_result.get('text', '')] ) # 服务规范检查 opening_check = self.check_opening(agent_result.get('text', ''), keywords['opening']) closing_check = self.check_closing(agent_result.get('text', ''), keywords['closing']) forbidden_check = self.check_forbidden(agent_result.get('text', ''), keywords['forbidden']) # 沟通技巧分析 speech_rate = self.analyze_speech_rate(agent_result.get('segments', [])) volume_analysis = self.analyze_volume(wav_file, agent_segments, sample_rate) # 问题解决率分析 resolution_rate = self.analyze_resolution( agent_result.get('text', ''), customer_result.get('text', ''), keywords['resolution'] ) return { "file_name": os.path.basename(audio_file), "duration": duration, "agent_text": agent_result.get('text', ''), "customer_text": customer_result.get('text', ''), "opening_check": opening_check, "closing_check": closing_check, "forbidden_check": forbidden_check, "agent_emotion": agent_emotion, "customer_emotion": customer_emotion, "speech_rate": speech_rate, "volume_mean": volume_analysis.get('mean', -60), "volume_std": volume_analysis.get('std', 0), "resolution_rate": resolution_rate } except Exception as e: error_msg = f"处理文件 {os.path.basename(audio_file)} 时出错: {str(e)}" self.error.emit(error_msg) logger.error(error_msg, exc_info=True) return None finally: # 清理临时文件 if is_temp and os.path.exists(wav_file): try: os.unlink(wav_file) except Exception: pass def process_diarization(self, wav_file, duration): """分块处理说话人分离,避免大文件内存溢出""" # 对于短音频直接处理 if duration <= 600: # 10分钟以下 return self.cached_models['pyannote'](wav_file) # 对于长音频分块处理 self.message.emit(f"音频较长({duration:.1f}秒),将分块处理...") diarization_result = [] chunk_size = 300 # 5分钟块 for start in range(0, int(duration), chunk_size): if not self.running: return [] end = min(start + chunk_size, duration) self.message.emit(f"处理片段: {start}-{end}秒") # 提取音频片段 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: self.extract_audio_segment(wav_file, start, end, tmpfile.name) segment_diarization = self.cached_models['pyannote'](tmpfile.name) # 调整时间偏移 for segment, _, speaker in segment_diarization.itertracks(yield_label=True): diarization_result.append(( segment.start + start, segment.end + start, speaker )) return diarization_result def extract_audio_segment(self, input_file, start_sec, end_sec, output_file): """提取音频片段""" audio = AudioSegment.from_wav(input_file) start_ms = int(start_sec * 1000) end_ms = int(end_sec * 1000) segment = audio[start_ms:end_ms] segment.export(output_file, format="wav") def process_speaker_audio(self, wav_file, segments, speaker_type): """处理说话人音频(优化内存使用)""" if not segments: return {'text': "", 'segments': []} text = "" segment_details = [] whisper_model = self.cached_models['whisper'] # 处理每个片段 for idx, (start, end) in enumerate(segments): if not self.running: break # 每处理5个片段报告一次进度 if idx % 5 == 0: self.message.emit(f"{speaker_type}: 处理片段 {idx+1}/{len(segments)}") duration = end - start segment_text = self.transcribe_audio_segment(wav_file, start, end, whisper_model) segment_details.append({ 'start': start, 'end': end, 'duration': duration, 'text': segment_text }) text += segment_text + " " return { 'text': text.strip(), 'segments': segment_details } def identify_speakers(self, wav_file, diarization, opening_keywords): """ 改进的客服识别方法 1. 检查前三个片段是否有开场白关键词 2. 如果片段不足三个,则检查所有存在的片段 3. 如果无法确定客服,则默认第二个说话人是客服 """ if not diarization: return [], [] speaker_segments = defaultdict(list) speaker_first_occurrence = {} # 记录每个说话人的首次出现时间 # 收集所有说话人片段并记录首次出现时间 for item in diarization: if len(item) == 3: # 来自分块处理的结果 start, end, speaker = item else: # 来自pyannote的直接结果 segment, _, speaker = item start, end = segment.start, segment.end speaker_segments[speaker].append((start, end)) if speaker not in speaker_first_occurrence or start < speaker_first_occurrence[speaker]: speaker_first_occurrence[speaker] = start # 如果没有说话人 if not speaker_segments: return [], [] # 如果只有一个说话人 if len(speaker_segments) == 1: speaker = list(speaker_segments.keys())[0] return speaker_segments[speaker], [] # 计算每个说话人的开场白得分 speaker_scores = {} whisper_model = self.cached_models['whisper'] for speaker, segments in speaker_segments.items(): score = 0 # 检查前三个片段(如果存在) check_segments = segments[:3] # 最多取前三个片段 for start, end in check_segments: # 转录片段 text = self.transcribe_audio_segment(wav_file, start, end, whisper_model) # 检查开场白关键词 for keyword in opening_keywords: if keyword and keyword in text: score += 1 break # 找到一个关键词就加分并跳出循环 speaker_scores[speaker] = score # 尝试找出得分最高的说话人 max_score = max(speaker_scores.values()) max_speakers = [spk for spk, score in speaker_scores.items() if score == max_score] # 如果有唯一最高分说话人,作为客服 if len(max_speakers) == 1: agent_speaker = max_speakers[0] else: # 无法通过开场白确定客服时,默认第二个说话人是客服 # 按首次出现时间排序 sorted_speakers = sorted(speaker_first_occurrence.items(), key=lambda x: x[1]) # 确保至少有两个说话人 if len(sorted_speakers) >= 2: # 取时间上第二个出现的说话人 agent_speaker = sorted_speakers[1][0] else: # 如果只有一个说话人(理论上不会进入此分支,但安全处理) agent_speaker = sorted_speakers[0][0] # 分离客服和客户片段 agent_segments = speaker_segments[agent_speaker] customer_segments = [] for speaker, segments in speaker_segments.items(): if speaker != agent_speaker: customer_segments.extend(segments) return agent_segments, customer_segments def load_keywords(self): """从Excel文件加载关键词(增强健壮性)""" try: df = pd.read_excel(self.keyword_file) # 确保列存在 columns = ['opening', 'closing', 'forbidden', 'resolution'] for col in columns: if col not in df.columns: raise ValueError(f"关键词文件缺少必要列: {col}") keywords = { "opening": [str(k).strip() for k in df['opening'].dropna().tolist() if str(k).strip()], "closing": [str(k).strip() for k in df['closing'].dropna().tolist() if str(k).strip()], "forbidden": [str(k).strip() for k in df['forbidden'].dropna().tolist() if str(k).strip()], "resolution": [str(k).strip() for k in df['resolution'].dropna().tolist() if str(k).strip()] } # 检查是否有足够的关键词 if not any(keywords.values()): raise ValueError("关键词文件中没有找到有效关键词") return keywords except Exception as e: raise Exception(f"加载关键词文件失败: {str(e)}") def convert_to_wav(self, audio_file): """将音频文件转换为WAV格式(增强健壮性)""" try: if not os.path.exists(audio_file): raise FileNotFoundError(f"音频文件不存在: {audio_file}") if audio_file.lower().endswith('.wav'): return audio_file, False # 使用临时文件避免磁盘IO with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmpfile: output_file = tmpfile.name audio = AudioSegment.from_file(audio_file) audio.export(output_file, format='wav') return output_file, True except Exception as e: raise Exception(f"音频转换失败: {str(e)}") def get_audio_info(self, wav_file): """获取音频文件信息(增强健壮性)""" try: if not os.path.exists(wav_file): raise FileNotFoundError(f"音频文件不存在: {wav_file}") # 使用soundfile获取更可靠的信息 with sf.SoundFile(wav_file) as f: duration = len(f) / f.samplerate sample_rate = f.samplerate channels = f.channels return duration, sample_rate, channels except Exception as e: raise Exception(f"获取音频信息失败: {str(e)}") def transcribe_audio_segment(self, wav_file, start, end, model): """转录单个音频片段 - 优化内存使用""" # 使用pydub加载音频 audio = AudioSegment.from_wav(wav_file) # 转换为毫秒 start_ms = int(start * 1000) end_ms = int(end * 1000) segment_audio = audio[start_ms:end_ms] # 使用临时文件 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: segment_audio.export(tmpfile.name, format="wav") try: result = model.transcribe( tmpfile.name, fp16=cuda_available() # 使用FP16加速(如果可用) ) return result['text'] except RuntimeError as e: if "out of memory" in str(e).lower(): # 尝试释放内存后重试 torch.cuda.empty_cache() gc.collect() result = model.transcribe( tmpfile.name, fp16=cuda_available() ) return result['text'] raise def analyze_emotions(self, texts): """批量分析文本情感(提高效率)""" if not any(t.strip() for t in texts): return [{"label": "中性", "score": 0.0} for _ in texts] # 截断长文本以提高性能 processed_texts = [t[:500] if len(t) > 500 else t for t in texts] # 批量处理 classifier = self.cached_models['emotion_classifier'] results = classifier(processed_texts, truncation=True, max_length=512, batch_size=4) # 确保返回格式一致 emotions = [] for result in results: if isinstance(result, list) and result: emotions.append({ "label": result[0]['label'], "score": result[0]['score'] }) else: emotions.append({ "label": "中性", "score": 0.0 }) return emotions def check_opening(self, text, opening_keywords): """检查开场白(使用正则表达式提高准确性)""" if not text or not opening_keywords: return False pattern = "|".join(re.escape(k) for k in opening_keywords) return bool(re.search(pattern, text)) def check_closing(self, text, closing_keywords): """检查结束语(使用正则表达式提高准确性)""" if not text or not closing_keywords: return False pattern = "|".join(re.escape(k) for k in closing_keywords) return bool(re.search(pattern, text)) def check_forbidden(self, text, forbidden_keywords): """检查服务禁语(使用正则表达式提高准确性)""" if not text or not forbidden_keywords: return False pattern = "|".join(re.escape(k) for k in forbidden_keywords) return bool(re.search(pattern, text)) def analyze_speech_rate(self, segments): """改进的语速分析 - 基于实际识别文本""" if not segments: return 0 total_chars = 0 total_duration = 0 for segment in segments: # 计算片段时长(秒) duration = segment['duration'] total_duration += duration # 计算中文字符数(去除标点和空格) chinese_chars = sum(1 for char in segment['text'] if '\u4e00' <= char <= '\u9fff') total_chars += chinese_chars if total_duration == 0: return 0 # 语速 = 总字数 / 总时长(分钟) return total_chars / (total_duration / 60) def analyze_volume(self, wav_file, segments, sample_rate): """改进的音量分析 - 使用librosa计算RMS分贝值""" if not segments: return {"mean": -60, "std": 0} # 使用soundfile加载音频(更高效) try: y, sr = sf.read(wav_file, dtype='float32') if sr != sample_rate: y = librosa.resample(y, orig_sr=sr, target_sr=sample_rate) sr = sample_rate except Exception: # 回退到librosa y, sr = librosa.load(wav_file, sr=sample_rate, mono=True) all_dB = [] for start, end in segments: start_sample = int(start * sr) end_sample = int(end * sr) # 确保片段在有效范围内 if start_sample < len(y) and end_sample <= len(y): segment_audio = y[start_sample:end_sample] # 计算RMS并转换为dB rms = librosa.feature.rms(y=segment_audio)[0] dB = librosa.amplitude_to_db(rms, ref=1.0) # 使用标准参考值 all_dB.extend(dB) if not all_dB: return {"mean": -60, "std": 0} return { "mean": float(np.mean(all_dB)), "std": float(np.std(all_dB)) } def analyze_resolution(self, agent_text, customer_text, resolution_keywords): """分析问题解决率(使用更智能的匹配)""" # 检查客户是否提到问题 problem_patterns = [ "问题", "故障", "解决", "怎么办", "如何", "为什么", "不行", "不能", "无法", "错误", "bug", "issue", "疑问", "咨询" ] problem_regex = re.compile("|".join(problem_patterns)) has_problem = bool(problem_regex.search(customer_text)) # 检查客服是否提供解决方案 solution_regex = re.compile("|".join(re.escape(k) for k in resolution_keywords)) solution_found = bool(solution_regex.search(agent_text)) # 如果没有检测到问题,则认为已解决 if not has_problem: return True return solution_found def stop(self): """停止分析""" self.running = False self.message.emit("正在停止分析...") class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("外呼电话录音包质检分析系统") self.setGeometry(100, 100, 1000, 700) self.setStyleSheet(""" QMainWindow { background-color: #f0f0f0; } QGroupBox { font-weight: bold; border: 1px solid gray; border-radius: 5px; margin-top: 1ex; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } QPushButton { background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 3px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #cccccc; } QProgressBar { border: 1px solid grey; border-radius: 3px; text-align: center; } QProgressBar::chunk { background-color: #4CAF50; width: 10px; } QTextEdit { font-family: Consolas, Monaco, monospace; } """) # 初始化变量 self.audio_files = [] self.keyword_file = "" self.whisper_model_path = "./models/whisper-small" self.pyannote_model_path = "./models/pyannote-speaker-diarization" self.emotion_model_path = "./models/Erlangshen-Roberta-110M-Sentiment" self.output_dir = os.path.expanduser("~/质检报告") # 创建主控件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 文件选择区域 file_group = QGroupBox("文件选择") file_layout = QVBoxLayout(file_group) file_layout.setSpacing(8) # 音频文件选择 audio_layout = QHBoxLayout() self.audio_label = QLabel("音频文件/文件夹:") audio_layout.addWidget(self.audio_label) self.audio_path_edit = QLineEdit() self.audio_path_edit.setPlaceholderText("请选择音频文件或文件夹") audio_layout.addWidget(self.audio_path_edit, 3) self.audio_browse_btn = QPushButton("浏览...") self.audio_browse_btn.clicked.connect(self.browse_audio) audio_layout.addWidget(self.audio_browse_btn) file_layout.addLayout(audio_layout) # 关键词文件选择 keyword_layout = QHBoxLayout() self.keyword_label = QLabel("关键词文件:") keyword_layout.addWidget(self.keyword_label) self.keyword_path_edit = QLineEdit() self.keyword_path_edit.setPlaceholderText("请选择Excel格式的关键词文件") keyword_layout.addWidget(self.keyword_path_edit, 3) self.keyword_browse_btn = QPushButton("浏览...") self.keyword_browse_btn.clicked.connect(self.browse_keyword) keyword_layout.addWidget(self.keyword_browse_btn) file_layout.addLayout(keyword_layout) main_layout.addWidget(file_group) # 模型设置区域 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) model_layout.setSpacing(8) # Whisper模型路径 whisper_layout = QHBoxLayout() whisper_layout.addWidget(QLabel("Whisper模型路径:")) self.whisper_edit = QLineEdit(self.whisper_model_path) whisper_layout.addWidget(self.whisper_edit, 3) model_layout.addLayout(whisper_layout) # Pyannote模型路径 pyannote_layout = QHBoxLayout() pyannote_layout.addWidget(QLabel("Pyannote模型路径:")) self.pyannote_edit = QLineEdit(self.pyannote_model_path) pyannote_layout.addWidget(self.pyannote_edit, 3) model_layout.addLayout(pyannote_layout) # 情感分析模型路径 emotion_layout = QHBoxLayout() emotion_layout.addWidget(QLabel("情感分析模型路径:")) self.emotion_edit = QLineEdit(self.emotion_model_path) emotion_layout.addWidget(self.emotion_edit, 3) model_layout.addLayout(emotion_layout) # 输出目录 output_layout = QHBoxLayout() output_layout.addWidget(QLabel("输出目录:")) self.output_edit = QLineEdit(self.output_dir) self.output_edit.setPlaceholderText("请选择报告输出目录") output_layout.addWidget(self.output_edit, 3) self.output_browse_btn = QPushButton("浏览...") self.output_browse_btn.clicked.connect(self.browse_output) output_layout.addWidget(self.output_browse_btn) model_layout.addLayout(output_layout) main_layout.addWidget(model_group) # 控制按钮区域 control_layout = QHBoxLayout() control_layout.setSpacing(10) self.start_btn = QPushButton("开始分析") self.start_btn.setStyleSheet("background-color: #2196F3;") self.start_btn.clicked.connect(self.start_analysis) control_layout.addWidget(self.start_btn) self.stop_btn = QPushButton("停止分析") self.stop_btn.setStyleSheet("background-color: #f44336;") self.stop_btn.clicked.connect(self.stop_analysis) self.stop_btn.setEnabled(False) control_layout.addWidget(self.stop_btn) self.clear_btn = QPushButton("清空") self.clear_btn.clicked.connect(self.clear_all) control_layout.addWidget(self.clear_btn) main_layout.addLayout(control_layout) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setValue(0) self.progress_bar.setFormat("就绪") self.progress_bar.setMinimumHeight(25) main_layout.addWidget(self.progress_bar) # 日志输出区域 log_group = QGroupBox("分析日志") log_layout = QVBoxLayout(log_group) self.log_text = QTextEdit() self.log_text.setReadOnly(True) log_layout.addWidget(self.log_text) main_layout.addWidget(log_group, 1) # 给日志区域更多空间 # 状态区域 status_layout = QHBoxLayout() self.status_label = QLabel("状态: 就绪") status_layout.addWidget(self.status_label, 1) self.file_count_label = QLabel("已选择0个音频文件") status_layout.addWidget(self.file_count_label) main_layout.addLayout(status_layout) # 初始化分析线程 self.analysis_thread = None def browse_audio(self): """浏览音频文件或文件夹""" options = QFileDialog.Options() files, _ = QFileDialog.getOpenFileNames( self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.amr *.ogg *.flac *.m4a);;所有文件 (*)", options=options ) if files: self.audio_files = files self.audio_path_edit.setText("; ".join(files)) self.file_count_label.setText(f"已选择{len(files)}个音频文件") self.log_text.append(f"已选择{len(files)}个音频文件") def browse_keyword(self): """浏览关键词文件""" options = QFileDialog.Options() file, _ = QFileDialog.getOpenFileName( self, "选择关键词文件", "", "Excel文件 (*.xlsx *.xls);;所有文件 (*)", options=options ) if file: self.keyword_file = file self.keyword_path_edit.setText(file) self.log_text.append(f"已选择关键词文件: {file}") def browse_output(self): """浏览输出目录""" options = QFileDialog.Options() directory = QFileDialog.getExistingDirectory( self, "选择输出目录", self.output_dir, options=options ) if directory: self.output_dir = directory self.output_edit.setText(directory) self.log_text.append(f"输出目录设置为: {directory}") def start_analysis(self): """开始分析""" if not self.audio_files: self.show_warning("请先选择音频文件") return if not self.keyword_file: self.show_warning("请先选择关键词文件") return if not os.path.exists(self.keyword_file): self.show_warning("关键词文件不存在,请重新选择") return # 检查模型路径 model_paths = [ self.whisper_edit.text(), self.pyannote_edit.text(), self.emotion_edit.text() ] for path in model_paths: if not os.path.exists(path): self.show_warning(f"模型路径不存在: {path}") return # 更新模型路径 self.whisper_model_path = self.whisper_edit.text() self.pyannote_model_path = self.pyannote_edit.text() self.emotion_model_path = self.emotion_edit.text() self.output_dir = self.output_edit.text() # 创建输出目录 os.makedirs(self.output_dir, exist_ok=True) self.log_text.append("开始分析...") self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText("状态: 分析中...") self.progress_bar.setFormat("分析中... 0%") self.progress_bar.setValue(0) # 创建并启动分析线程 self.analysis_thread = AnalysisThread( self.audio_files, self.keyword_file, self.whisper_model_path, self.pyannote_model_path, self.emotion_model_path ) self.analysis_thread.progress.connect(self.update_progress) self.analysis_thread.message.connect(self.log_text.append) self.analysis_thread.analysis_complete.connect(self.on_analysis_complete) self.analysis_thread.error.connect(self.on_analysis_error) self.analysis_thread.finished.connect(self.on_analysis_finished) self.analysis_thread.start() def update_progress(self, value): """更新进度条""" self.progress_bar.setValue(value) self.progress_bar.setFormat(f"分析中... {value}%") def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.log_text.append("正在停止分析...") self.stop_btn.setEnabled(False) def clear_all(self): """清空所有内容""" self.audio_files = [] self.keyword_file = "" self.audio_path_edit.clear() self.keyword_path_edit.clear() self.log_text.clear() self.progress_bar.setValue(0) self.progress_bar.setFormat("就绪") self.status_label.setText("状态: 就绪") self.file_count_label.setText("已选择0个音频文件") self.log_text.append("已清空所有内容") def show_warning(self, message): """显示警告消息""" QMessageBox.warning(self, "警告", message) self.log_text.append(f"警告: {message}") def on_analysis_complete(self, result): """分析完成处理""" try: self.log_text.append("正在生成报告...") if not result.get("results"): self.log_text.append("警告: 没有生成任何分析结果") return # 生成Excel报告 excel_path = os.path.join(self.output_dir, "质检分析报告.xlsx") self.generate_excel_report(result, excel_path) # 生成Word报告 word_path = os.path.join(self.output_dir, "质检分析报告.docx") self.generate_word_report(result, word_path) self.log_text.append(f"分析报告已保存至: {excel_path}") self.log_text.append(f"可视化报告已保存至: {word_path}") self.log_text.append("分析完成!") self.status_label.setText(f"状态: 分析完成!报告保存至: {self.output_dir}") self.progress_bar.setFormat("分析完成!") # 显示完成消息 QMessageBox.information( self, "分析完成", f"分析完成!报告已保存至:\n{excel_path}\n{word_path}" ) except Exception as e: import traceback error_msg = f"生成报告时出错: {str(e)}\n{traceback.format_exc()}" self.log_text.append(error_msg) logger.error(error_msg) def on_analysis_error(self, message): """分析错误处理""" self.log_text.append(f"错误: {message}") self.status_label.setText("状态: 发生错误") self.progress_bar.setFormat("发生错误") QMessageBox.critical(self, "分析错误", message) def on_analysis_finished(self): """分析线程结束处理""" self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) def generate_excel_report(self, result, output_path): """生成Excel报告(增强健壮性)""" try: # 从结果中提取数据 data = [] for res in result['results']: data.append({ "文件名": res['file_name'], "音频时长(秒)": res['duration'], "开场白检查": "通过" if res['opening_check'] else "未通过", "结束语检查": "通过" if res['closing_check'] else "未通过", "服务禁语检查": "通过" if not res['forbidden_check'] else "未通过", "客服情感": res['agent_emotion']['label'], "客服情感得分": res['agent_emotion']['score'], "客户情感": res['customer_emotion']['label'], "客户情感得分": res['customer_emotion']['score'], "语速(字/分)": res['speech_rate'], "平均音量(dB)": res['volume_mean'], "音量标准差": res['volume_std'], "问题解决率": "是" if res['resolution_rate'] else "否" }) # 创建DataFrame并保存 df = pd.DataFrame(data) # 尝试使用openpyxl引擎(更稳定) try: df.to_excel(output_path, index=False, engine='openpyxl') except ImportError: df.to_excel(output_path, index=False) # 添加汇总统计 try: with pd.ExcelWriter(output_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer: summary_data = { "统计项": ["总文件数", "开场白通过率", "结束语通过率", "服务禁语通过率", "问题解决率"], "数值": [ len(result['results']), df['开场白检查'].value_counts().get('通过', 0) / len(df), df['结束语检查'].value_counts().get('通过', 0) / len(df), df['服务禁语检查'].value_counts().get('通过', 0) / len(df), df['问题解决率'].value_counts().get('是', 0) / len(df) ] } summary_df = pd.DataFrame(summary_data) summary_df.to_excel(writer, sheet_name='汇总统计', index=False) except Exception as e: self.log_text.append(f"添加汇总统计时出错: {str(e)}") except Exception as e: raise Exception(f"生成Excel报告失败: {str(e)}") def generate_word_report(self, result, output_path): """生成Word报告(增强健壮性)""" try: doc = Document() # 添加标题 doc.add_heading('外呼电话录音质检分析报告', 0) # 添加基本信息 doc.add_heading('分析概况', level=1) doc.add_paragraph(f"分析时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") doc.add_paragraph(f"分析文件数量: {len(result['results'])}") doc.add_paragraph(f"关键词文件: {os.path.basename(self.keyword_file)}") # 添加汇总统计 doc.add_heading('汇总统计', level=1) # 创建汇总表格 table = doc.add_table(rows=5, cols=2) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells hdr_cells[0].text = '统计项' hdr_cells[1].text = '数值' # 计算统计数据 df = pd.DataFrame(result['results']) pass_rates = { "开场白通过率": df['opening_check'].mean() if not df.empty else 0, "结束语通过率": df['closing_check'].mean() if not df.empty else 0, "服务禁语通过率": (1 - df['forbidden_check']).mean() if not df.empty else 0, "问题解决率": df['resolution_rate'].mean() if not df.empty else 0 } # 填充表格 rows = [ ("总文件数", len(result['results'])), ("开场白通过率", f"{pass_rates['开场白通过率']:.2%}"), ("结束语通过率", f"{pass_rates['结束语通过率']:.2%}"), ("服务禁语通过率", f"{pass_rates['服务禁语通过率']:.2%}"), ("问题解决率", f"{pass_rates['问题解决率']:.2%}") ] for i, row_data in enumerate(rows): if i < len(table.rows): row_cells = table.rows[i].cells row_cells[0].text = row_data[0] row_cells[1].text = str(row_data[1]) # 添加情感分析图表 if result['results']: doc.add_heading('情感分析', level=1) # 客服情感分布 agent_emotions = [res['agent_emotion']['label'] for res in result['results']] agent_emotion_counts = pd.Series(agent_emotions).value_counts() if not agent_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) agent_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客服情感分布') ax.set_ylabel('') # 移除默认的ylabel plt.tight_layout() # 保存图表到临时文件 chart_path = os.path.join(self.output_dir, "agent_emotion_chart.png") plt.savefig(chart_path, dpi=100, bbox_inches='tight') plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图1: 客服情感分布') # 客户情感分布 customer_emotions = [res['customer_emotion']['label'] for res in result['results']] customer_emotion_counts = pd.Series(customer_emotions).value_counts() if not customer_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) customer_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客户情感分布') ax.set_ylabel('') # 移除默认的ylabel plt.tight_layout() chart_path = os.path.join(self.output_dir, "customer_emotion_chart.png") plt.savefig(chart_path, dpi=100, bbox_inches='tight') plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图2: 客户情感分布') # 添加详细分析结果 doc.add_heading('详细分析结果', level=1) # 创建详细表格 table = doc.add_table(rows=1, cols=6) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells headers = ['文件名', '开场白', '结束语', '禁语', '客服情感', '问题解决'] for i, header in enumerate(headers): hdr_cells[i].text = header # 填充数据 for res in result['results']: row_cells = table.add_row().cells row_cells[0].text = res['file_name'] row_cells[1].text = "✓" if res['opening_check'] else "✗" row_cells[2].text = "✓" if res['closing_check'] else "✗" row_cells[3].text = "✗" if res['forbidden_check'] else "✓" row_cells[4].text = res['agent_emotion']['label'] row_cells[5].text = "✓" if res['resolution_rate'] else "✗" # 保存文档 doc.save(output_path) except Exception as e: raise Exception(f"生成Word报告失败: {str(e)}") if __name__ == "__main__": # 检查是否安装了torch try: import torch except ImportError: print("警告: PyTorch 未安装,情感分析可能无法使用GPU加速") app = QApplication(sys.argv) # 设置应用样式 app.setStyle("Fusion") window = MainWindow() window.show() sys.exit(app.exec_())

检查代码并优化: import sys import os import json import time import wave import numpy as np import pandas as pd import matplotlib.pyplot as plt import soundfile as sf # 新增依赖,用于音频片段保存 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, QFileDialog, QProgressBar, QGroupBox, QComboBox, QCheckBox) from PyQt5.QtCore import QThread, pyqtSignal from pydub import AudioSegment from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification import whisper from pyannote.audio import Pipeline from docx import Document from docx.shared import Inches import librosa import tempfile from collections import defaultdict import re # 新增用于中文字符计数 class AnalysisThread(QThread): progress = pyqtSignal(int) message = pyqtSignal(str) analysis_complete = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, audio_files, keyword_file, whisper_model_path, pyannote_model_path, emotion_model_path): super().__init__() self.audio_files = audio_files self.keyword_file = keyword_file self.whisper_model_path = whisper_model_path self.pyannote_model_path = pyannote_model_path self.emotion_model_path = emotion_model_path self.running = True self.cached_models = {} def run(self): try: # 加载关键词 self.message.emit("正在加载关键词...") keywords = self.load_keywords() # 预加载模型 self.message.emit("正在预加载模型...") self.preload_models() results = [] total_files = len(self.audio_files) for idx, audio_file in enumerate(self.audio_files): if not self.running: self.message.emit("分析已停止") return self.message.emit(f"正在处理文件: {os.path.basename(audio_file)} ({idx+1}/{total_files})") file_result = self.analyze_file(audio_file, keywords) if file_result: results.append(file_result) self.progress.emit(int((idx + 1) / total_files * 100)) self.analysis_complete.emit({"results": results, "keywords": keywords}) self.message.emit("分析完成!") except Exception as e: import traceback self.error.emit(f"分析过程中发生错误: {str(e)}\n{traceback.format_exc()}") def preload_models(self): """预加载所有模型到缓存""" # 检查是否已加载模型 if hasattr(self, 'cached_models') and self.cached_models: return self.cached_models = {} # 加载语音识别模型 if 'whisper' not in self.cached_models: self.message.emit("正在加载语音识别模型...") self.cached_models['whisper'] = whisper.load_model(self.whisper_model_path) # 加载说话人分离模型 if 'pyannote' not in self.cached_models: self.message.emit("正在加载说话人分离模型...") self.cached_models['pyannote'] = Pipeline.from_pretrained(self.pyannote_model_path) # 加载情感分析模型 if 'emotion_classifier' not in self.cached_models: self.message.emit("正在加载情感分析模型...") tokenizer = AutoTokenizer.from_pretrained(self.emotion_model_path) model = AutoModelForSequenceClassification.from_pretrained(self.emotion_model_path) self.cached_models['emotion_classifier'] = pipeline( "text-classification", model=model, tokenizer=tokenizer, device=0 if torch.cuda.is_available() else -1 # 使用GPU如果可用 ) def analyze_file(self, audio_file, keywords): """分析单个音频文件""" try: # 确保音频为WAV格式 wav_file = self.convert_to_wav(audio_file) # 获取音频信息 duration, sample_rate, channels = self.get_audio_info(wav_file) # 说话人分离 diarization = self.cached_models['pyannote'](wav_file) # 识别客服和客户(使用改进的方法) agent_segments, customer_segments = self.identify_speakers(wav_file, diarization, keywords['opening']) # 语音识别(使用优化后的方法) whisper_model = self.cached_models['whisper'] agent_text = self.transcribe_audio(wav_file, agent_segments, whisper_model) customer_text = self.transcribe_audio(wav_file, customer_segments, whisper_model) # 情感分析 emotion_classifier = self.cached_models['emotion_classifier'] agent_emotion = self.analyze_emotion(agent_text, emotion_classifier) customer_emotion = self.analyze_emotion(customer_text, emotion_classifier) # 服务规范检查 opening_check = self.check_opening(agent_text, keywords['opening']) closing_check = self.check_closing(agent_text, keywords['closing']) forbidden_check = self.check_forbidden(agent_text, keywords['forbidden']) # 沟通技巧分析(使用改进的方法) speech_rate = self.analyze_speech_rate(wav_file, agent_segments) volume_analysis = self.analyze_volume(wav_file, agent_segments) # 问题解决率分析 resolution_rate = self.analyze_resolution(agent_text, customer_text, keywords['resolution']) # 构建结果 return { "file_name": os.path.basename(audio_file), "duration": duration, "agent_text": agent_text, "customer_text": customer_text, "opening_check": opening_check, "closing_check": closing_check, "forbidden_check": forbidden_check, "agent_emotion": agent_emotion, "customer_emotion": customer_emotion, "speech_rate": speech_rate, "volume_mean": volume_analysis['mean'], "volume_std": volume_analysis['std'], "resolution_rate": resolution_rate } except Exception as e: self.error.emit(f"处理文件 {os.path.basename(audio_file)} 时出错: {str(e)}") return None def load_keywords(self): """从Excel文件加载关键词""" try: df = pd.read_excel(self.keyword_file) keywords = { "opening": [str(k).strip() for k in df['opening'].dropna().tolist()], "closing": [str(k).strip() for k in df['closing'].dropna().tolist()], "forbidden": [str(k).strip() for k in df['forbidden'].dropna().tolist()], "resolution": [str(k).strip() for k in df['resolution'].dropna().tolist()] } return keywords except Exception as e: raise Exception(f"加载关键词文件失败: {str(e)}") def convert_to_wav(self, audio_file): """将音频文件转换为WAV格式(如果需要)""" try: if not audio_file.lower().endswith('.wav'): # 使用临时文件避免磁盘IO with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmpfile: output_file = tmpfile.name audio = AudioSegment.from_file(audio_file) audio.export(output_file, format='wav') return output_file return audio_file except Exception as e: raise Exception(f"音频转换失败: {str(e)}") def get_audio_info(self, wav_file): """获取音频文件信息""" try: with wave.open(wav_file, 'rb') as wf: frames = wf.getnframes() rate = wf.getframerate() channels = wf.getnchannels() duration = frames / float(rate) return duration, rate, channels except Exception as e: raise Exception(f"获取音频信息失败: {str(e)}") def identify_speakers(self, wav_file, diarization, opening_keywords): """改进的客服识别方法 - 检查前三个片段是否有开场白关键词""" speaker_segments = defaultdict(list) for segment, _, speaker in diarization.itertracks(yield_label=True): speaker_segments[speaker].append((segment.start, segment.end)) # 如果没有说话人 if not speaker_segments: return [], [] # 如果只有一个说话人 if len(speaker_segments) == 1: speaker = list(speaker_segments.keys())[0] return speaker_segments[speaker], [] # 检查每个说话人的前三个片段是否有开场白 speaker_scores = {} whisper_model = self.cached_models['whisper'] for speaker, segments in speaker_segments.items(): score = 0 # 取前三个片段(或所有片段如果少于3个) check_segments = segments[:3] for start, end in check_segments: # 转录片段 text = self.transcribe_audio_segment(wav_file, [(start, end)], whisper_model) # 检查开场白关键词 for keyword in opening_keywords: if keyword and keyword in text: score += 1 break speaker_scores[speaker] = score # 找到得分最高的说话人作为客服 agent_speaker = max(speaker_scores, key=speaker_scores.get) agent_segments = [] customer_segments = [] for speaker, segments in speaker_segments.items(): if speaker == agent_speaker: agent_segments = segments else: customer_segments.extend(segments) return agent_segments, customer_segments def transcribe_audio_segment(self, wav_file, segments, model): """转录单个音频片段 - 用于客服识别""" if not segments: return "" # 使用pydub加载音频 audio = AudioSegment.from_wav(wav_file) start, end = segments[0] # 转换为毫秒 start_ms = int(start * 1000) end_ms = int(end * 1000) segment_audio = audio[start_ms:end_ms] # 使用临时文件 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: segment_audio.export(tmpfile.name, format="wav") result = model.transcribe(tmpfile.name) return result['text'] def transcribe_audio(self, wav_file, segments, model): """优化后的转录方法 - 按片段转录""" if not segments: return "" # 使用pydub加载音频 audio = AudioSegment.from_wav(wav_file) full_text = "" # 只处理指定片段 for start, end in segments: # 转换为毫秒 start_ms = int(start * 1000) end_ms = int(end * 1000) segment_audio = audio[start_ms:end_ms] # 使用临时文件避免内存占用 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: segment_audio.export(tmpfile.name, format="wav") result = model.transcribe(tmpfile.name) full_text += result['text'] + " " return full_text.strip() def analyze_emotion(self, text, classifier): """分析文本情感""" if not text.strip(): return {"label": "中性", "score": 0.0} # 截断长文本以提高性能 if len(text) > 500: text = text[:500] result = classifier(text, truncation=True, max_length=512) return { "label": result[0]['label'], "score": result[0]['score'] } def check_opening(self, text, opening_keywords): """检查开场白""" return any(keyword in text for keyword in opening_keywords if keyword) def check_closing(self, text, closing_keywords): """检查结束语""" return any(keyword in text for keyword in closing_keywords if keyword) def check_forbidden(self, text, forbidden_keywords): """检查服务禁语""" return any(keyword in text for keyword in forbidden_keywords if keyword) def analyze_speech_rate(self, wav_file, segments): """改进的语速分析 - 基于实际识别文本""" if not segments: return 0 # 加载音频 y, sr = librosa.load(wav_file, sr=None) total_chars = 0 total_duration = 0 whisper_model = self.cached_models['whisper'] for start, end in segments: # 计算片段时长(秒) duration = end - start total_duration += duration # 转录片段 text = self.transcribe_audio_segment(wav_file, [(start, end)], whisper_model) # 计算中文字符数(去除标点和空格) chinese_chars = sum(1 for char in text if '\u4e00' <= char <= '\u9fff') total_chars += chinese_chars if total_duration == 0: return 0 # 语速 = 总字数 / 总时长(分钟) return total_chars / (total_duration / 60) def analyze_volume(self, wav_file, segments): """改进的音量分析 - 使用librosa计算RMS分贝值""" if not segments: return {"mean": -60, "std": 0} # 加载音频 y, sr = librosa.load(wav_file, sr=None) all_dB = [] for start, end in segments: start_sample = int(start * sr) end_sample = int(end * sr) segment_audio = y[start_sample:end_sample] # 计算RMS并转换为dB rms = librosa.feature.rms(y=segment_audio)[0] dB = librosa.amplitude_to_db(rms, ref=np.max) all_dB.extend(dB) if not all_dB: return {"mean": -60, "std": 0} return { "mean": float(np.mean(all_dB)), "std": float(np.std(all_dB)) } def analyze_resolution(self, agent_text, customer_text, resolution_keywords): """分析问题解决率""" return any(keyword in agent_text for keyword in resolution_keywords if keyword) def stop(self): """停止分析""" self.running = False class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("外呼电话录音包质检分析系统") self.setGeometry(100, 100, 1000, 700) # 初始化变量 self.audio_files = [] self.keyword_file = "" self.whisper_model_path = "./models/whisper-small" self.pyannote_model_path = "./models/pyannote-speaker-diarization" self.emotion_model_path = "./models/Erlangshen-Roberta-110M-Sentiment" self.output_dir = "./reports" # 创建主控件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 文件选择区域 file_group = QGroupBox("文件选择") file_layout = QVBoxLayout(file_group) # 音频文件选择 audio_layout = QHBoxLayout() self.audio_label = QLabel("音频文件/文件夹:") audio_layout.addWidget(self.audio_label) self.audio_path_edit = QLineEdit() audio_layout.addWidget(self.audio_path_edit) self.audio_browse_btn = QPushButton("浏览...") self.audio_browse_btn.clicked.connect(self.browse_audio) audio_layout.addWidget(self.audio_browse_btn) file_layout.addLayout(audio_layout) # 关键词文件选择 keyword_layout = QHBoxLayout() self.keyword_label = QLabel("关键词文件:") keyword_layout.addWidget(self.keyword_label) self.keyword_path_edit = QLineEdit() keyword_layout.addWidget(self.keyword_path_edit) self.keyword_browse_btn = QPushButton("浏览...") self.keyword_browse_btn.clicked.connect(self.browse_keyword) keyword_layout.addWidget(self.keyword_browse_btn) file_layout.addLayout(keyword_layout) main_layout.addWidget(file_group) # 模型设置区域 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) # Whisper模型路径 whisper_layout = QHBoxLayout() whisper_layout.addWidget(QLabel("Whisper模型路径:")) self.whisper_edit = QLineEdit(self.whisper_model_path) whisper_layout.addWidget(self.whisper_edit) model_layout.addLayout(whisper_layout) # Pyannote模型路径 pyannote_layout = QHBoxLayout() pyannote_layout.addWidget(QLabel("Pyannote模型路径:")) self.pyannote_edit = QLineEdit(self.pyannote_model_path) pyannote_layout.addWidget(self.pyannote_edit) model_layout.addLayout(pyannote_layout) # 情感分析模型路径 emotion_layout = QHBoxLayout() emotion_layout.addWidget(QLabel("情感分析模型路径:")) self.emotion_edit = QLineEdit(self.emotion_model_path) emotion_layout.addWidget(self.emotion_edit) model_layout.addLayout(emotion_layout) # 输出目录 output_layout = QHBoxLayout() output_layout.addWidget(QLabel("输出目录:")) self.output_edit = QLineEdit(self.output_dir) output_layout.addWidget(self.output_edit) self.output_browse_btn = QPushButton("浏览...") self.output_browse_btn.clicked.connect(self.browse_output) output_layout.addWidget(self.output_browse_btn) model_layout.addLayout(output_layout) main_layout.addWidget(model_group) # 控制按钮区域 control_layout = QHBoxLayout() self.start_btn = QPushButton("开始分析") self.start_btn.clicked.connect(self.start_analysis) control_layout.addWidget(self.start_btn) self.stop_btn = QPushButton("停止分析") self.stop_btn.clicked.connect(self.stop_analysis) self.stop_btn.setEnabled(False) control_layout.addWidget(self.stop_btn) self.clear_btn = QPushButton("清空") self.clear_btn.clicked.connect(self.clear_all) control_layout.addWidget(self.clear_btn) main_layout.addLayout(control_layout) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setValue(0) main_layout.addWidget(self.progress_bar) # 日志输出区域 log_group = QGroupBox("分析日志") log_layout = QVBoxLayout(log_group) self.log_text = QTextEdit() self.log_text.setReadOnly(True) log_layout.addWidget(self.log_text) main_layout.addWidget(log_group) # 状态区域 status_layout = QHBoxLayout() self.status_label = QLabel("就绪") status_layout.addWidget(self.status_label) self.file_count_label = QLabel("已选择0个音频文件") status_layout.addWidget(self.file_count_label) main_layout.addLayout(status_layout) # 初始化分析线程 self.analysis_thread = None def browse_audio(self): """浏览音频文件或文件夹""" options = QFileDialog.Options() files, _ = QFileDialog.getOpenFileNames( self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.amr *.ogg *.flac);;所有文件 (*)", options=options ) if files: self.audio_files = files self.audio_path_edit.setText("; ".join(files)) self.file_count_label.setText(f"已选择{len(files)}个音频文件") self.log_text.append(f"已选择{len(files)}个音频文件") def browse_keyword(self): """浏览关键词文件""" options = QFileDialog.Options() file, _ = QFileDialog.getOpenFileName( self, "选择关键词文件", "", "Excel文件 (*.xlsx *.xls);;所有文件 (*)", options=options ) if file: self.keyword_file = file self.keyword_path_edit.setText(file) self.log_text.append(f"已选择关键词文件: {file}") def browse_output(self): """浏览输出目录""" options = QFileDialog.Options() directory = QFileDialog.getExistingDirectory( self, "选择输出目录", "", options=options ) if directory: self.output_dir = directory self.output_edit.setText(directory) self.log_text.append(f"输出目录设置为: {directory}") def start_analysis(self): """开始分析""" if not self.audio_files: self.log_text.append("错误: 请先选择音频文件") return if not self.keyword_file: self.log_text.append("错误: 请先选择关键词文件") return # 更新模型路径 self.whisper_model_path = self.whisper_edit.text() self.pyannote_model_path = self.pyannote_edit.text() self.emotion_model_path = self.emotion_edit.text() self.output_dir = self.output_edit.text() # 创建输出目录 os.makedirs(self.output_dir, exist_ok=True) self.log_text.append("开始分析...") self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText("分析中...") self.progress_bar.setValue(0) # 创建并启动分析线程 self.analysis_thread = AnalysisThread( self.audio_files, self.keyword_file, self.whisper_model_path, self.pyannote_model_path, self.emotion_model_path ) self.analysis_thread.progress.connect(self.progress_bar.setValue) self.analysis_thread.message.connect(self.log_text.append) self.analysis_thread.analysis_complete.connect(self.on_analysis_complete) self.analysis_thread.error.connect(self.on_analysis_error) self.analysis_thread.finished.connect(self.on_analysis_finished) self.analysis_thread.start() def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.log_text.append("正在停止分析...") self.stop_btn.setEnabled(False) def clear_all(self): """清空所有内容""" self.audio_files = [] self.keyword_file = "" self.audio_path_edit.clear() self.keyword_path_edit.clear() self.log_text.clear() self.progress_bar.setValue(0) self.status_label.setText("就绪") self.file_count_label.setText("已选择0个音频文件") self.log_text.append("已清空所有内容") def on_analysis_complete(self, result): """分析完成处理""" try: self.log_text.append("正在生成报告...") if not result.get("results"): self.log_text.append("警告: 没有生成任何分析结果") return # 生成Excel报告 excel_path = os.path.join(self.output_dir, "质检分析报告.xlsx") self.generate_excel_report(result, excel_path) # 生成Word报告 word_path = os.path.join(self.output_dir, "质检分析报告.docx") self.generate_word_report(result, word_path) self.log_text.append(f"分析报告已保存至: {excel_path}") self.log_text.append(f"可视化报告已保存至: {word_path}") self.log_text.append("分析完成!") self.status_label.setText(f"分析完成!报告保存至: {self.output_dir}") except Exception as e: import traceback self.log_text.append(f"生成报告时出错: {str(e)}\n{traceback.format_exc()}") def on_analysis_error(self, message): """分析错误处理""" self.log_text.append(f"错误: {message}") self.status_label.setText("发生错误") def on_analysis_finished(self): """分析线程结束处理""" self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) def generate_excel_report(self, result, output_path): """生成Excel报告""" # 从结果中提取数据 data = [] for res in result['results']: data.append({ "文件名": res['file_name'], "音频时长(秒)": res['duration'], "开场白检查": "通过" if res['opening_check'] else "未通过", "结束语检查": "通过" if res['closing_check'] else "未通过", "服务禁语检查": "通过" if not res['forbidden_check'] else "未通过", "客服情感": res['agent_emotion']['label'], "客服情感得分": res['agent_emotion']['score'], "客户情感": res['customer_emotion']['label'], "客户情感得分": res['customer_emotion']['score'], "语速(字/分)": res['speech_rate'], "平均音量(dB)": res['volume_mean'], "音量标准差": res['volume_std'], "问题解决率": "是" if res['resolution_rate'] else "否" }) # 创建DataFrame并保存 df = pd.DataFrame(data) df.to_excel(output_path, index=False) # 添加汇总统计 try: with pd.ExcelWriter(output_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer: summary_data = { "统计项": ["总文件数", "开场白通过率", "结束语通过率", "服务禁语通过率", "问题解决率"], "数值": [ len(result['results']), df['开场白检查'].value_counts().get('通过', 0) / len(df), df['结束语检查'].value_counts().get('通过', 0) / len(df), df['服务禁语检查'].value_counts().get('通过', 0) / len(df), df['问题解决率'].value_counts().get('是', 0) / len(df) ] } summary_df = pd.DataFrame(summary_data) summary_df.to_excel(writer, sheet_name='汇总统计', index=False) except Exception as e: self.log_text.append(f"添加汇总统计时出错: {str(e)}") def generate_word_report(self, result, output_path): """生成Word报告""" doc = Document() # 添加标题 doc.add_heading('外呼电话录音质检分析报告', 0) # 添加基本信息 doc.add_heading('分析概况', level=1) doc.add_paragraph(f"分析时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") doc.add_paragraph(f"分析文件数量: {len(result['results'])}") doc.add_paragraph(f"关键词文件: {os.path.basename(self.keyword_file)}") # 添加汇总统计 doc.add_heading('汇总统计', level=1) # 创建汇总表格 table = doc.add_table(rows=5, cols=2) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells hdr_cells[0].text = '统计项' hdr_cells[1].text = '数值' # 计算统计数据 df = pd.DataFrame(result['results']) pass_rates = { "开场白通过率": df['opening_check'].mean() if not df.empty else 0, "结束语通过率": df['closing_check'].mean() if not df.empty else 0, "服务禁语通过率": (1 - df['forbidden_check']).mean() if not df.empty else 0, "问题解决率": df['resolution_rate'].mean() if not df.empty else 0 } # 填充表格 rows = [ ("总文件数", len(result['results'])), ("开场白通过率", f"{pass_rates['开场白通过率']:.2%}"), ("结束语通过率", f"{pass_rates['结束语通过率']:.2%}"), ("服务禁语通过率", f"{pass_rates['服务禁语通过率']:.2%}"), ("问题解决率", f"{pass_rates['问题解决率']:.2%}") ] for i, row_data in enumerate(rows): if i < len(table.rows): row_cells = table.rows[i].cells row_cells[0].text = row_data[0] row_cells[1].text = str(row_data[1]) # 添加情感分析图表 if result['results']: doc.add_heading('情感分析', level=1) # 客服情感分布 agent_emotions = [res['agent_emotion']['label'] for res in result['results']] agent_emotion_counts = pd.Series(agent_emotions).value_counts() if not agent_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) agent_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客服情感分布') plt.tight_layout() # 保存图表到临时文件 chart_path = os.path.join(self.output_dir, "agent_emotion_chart.png") plt.savefig(chart_path, dpi=100) plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图1: 客服情感分布') # 客户情感分布 customer_emotions = [res['customer_emotion']['label'] for res in result['results']] customer_emotion_counts = pd.Series(customer_emotions).value_counts() if not customer_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) customer_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客户情感分布') plt.tight_layout() chart_path = os.path.join(self.output_dir, "customer_emotion_chart.png") plt.savefig(chart_path, dpi=100) plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图2: 客户情感分布') # 添加详细分析结果 doc.add_heading('详细分析结果', level=1) # 创建详细表格 table = doc.add_table(rows=1, cols=6) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells headers = ['文件名', '开场白', '结束语', '禁语', '客服情感', '问题解决'] for i, header in enumerate(headers): hdr_cells[i].text = header # 填充数据 for res in result['results']: row_cells = table.add_row().cells row_cells[0].text = res['file_name'] row_cells[1].text = "✓" if res['opening_check'] else "✗" row_cells[2].text = "✓" if res['closing_check'] else "✗" row_cells[3].text = "✗" if res['forbidden_check'] else "✓" row_cells[4].text = res['agent_emotion']['label'] row_cells[5].text = "✓" if res['resolution_rate'] else "✗" # 保存文档 doc.save(output_path) if __name__ == "__main__": # 检查是否安装了torch try: import torch except ImportError: print("警告: PyTorch 未安装,情感分析可能无法使用GPU加速") app = QApplication(sys.argv) window = MainWindow()

非常好!我们将在前文的基础上,**重新整理一套完整、清晰、适合地质行业电脑小白用户的 AI 工作流部署方案**。该方案将基于: - **Windows 11 + WSL2** - **Docker Desktop(Linux 容器模式)** - **Ollama(本地大模型)** - **RAGFlow(文档知识库 + 向量检索)** - **Dify(可视化 AI 工作流)** - **Python 脚本(自动生成 Word 报告)** --- ## 📁 部署目标结构 D:\AI_Studio\ ├── apps\ # 所有应用程序安装目录 │ ├── docker\ # Docker Desktop 安装路径 │ ├── python\ # Python 安装路径 │ └── ollama\ # Ollama 原生可执行文件(备选) │ ├── data\ # 数据存储目录 │ ├── models\ # 模型缓存(如 Ollama 模型) │ ├── ragflow_data\ # RAGFlow 数据目录 │ ├── dify_data\ # Dify 数据目录 │ ├── docker\ # Docker 数据卷 │ └── reports\ # Word 报告模板与输出 │ ├── templates\ # Word 模板 │ └── output\ # 自动生成的报告 │ ├── installer\ # 安装包统一存放目录 │ ├── Docker Desktop Installer.exe │ ├── ollama-windows-amd64.exe │ └── python-3.11.9-amd64.exe │ └── scripts\ # 自定义脚本目录 └── generate_report.py # 自动生成 Word 报告的 Python 脚本 --- ## 🔧 第一步:准备系统环境 ### ✅ 1. 启用 WSL2(无需手动安装 Ubuntu) 以管理员身份打开 PowerShell: powershell # 启用 WSL 功能并安装默认发行版(Ubuntu) wsl --install # 设置默认版本为 WSL2 wsl --set-default-version 2 # 查看已安装的发行版 wsl --list --verbose > ⚠️ 如果未自动安装 Ubuntu,运行: powershell wsl --install -d Ubuntu 安装完成后设置用户名和密码。 --- ## 🐳 第二步:安装 Docker Desktop(集成 WSL2) ### ✅ 1. 下载安装包 放入: D:\AI_Studio\installer\Docker%20Desktop%20Installer.exe ### ✅ 2. 安装 Docker Desktop 双击运行安装程序,并选择安装路径为: D:\AI_Studio\apps\docker > 💡 如果安装程序不支持更改路径,可以先安装到默认位置,再移动文件夹并创建软链接。 ### ✅ 3. 设置 Docker 数据卷路径 编辑或新建配置文件: C:\ProgramData\Docker\config\daemon.json 写入内容: json { "data-root": "D:/AI_Studio/data/docker" } 重启 Docker 服务生效。 ### ✅ 4. 切换容器类型为 Linux 模式 右键任务栏 Docker Desktop 图标 → Switch to Linux containers --- ## 🧠 第三步:安装 Ollama(推荐使用 Docker 模式) ### ✅ 方法一:使用 Docker 运行 Ollama(推荐) powershell docker run -d -p 11434:11434 -v D:/AI_Studio/data/models:/root/.ollama infiniflow/ollama:latest 拉取模型: powershell ollama pull llama3 ### ✅ 方法二:使用原生 Windows 版本(备选) 将下载好的 ollama-windows-amd64.exe 放入: D:\AI_Studio\apps\ollama 重命名为 ollama.exe,并在 PowerShell 中运行: powershell cd D:\AI_Studio\apps\ollama .\ollama.exe serve --- ## 📚 第四步:部署 RAGFlow(基于 Docker) powershell mkdir D:\AI_Studio\data\ragflow_data docker run -d --name ragflow -p 8080:8080 -v D:/AI_Studio/data/ragflow_data:/data -e LOCAL_MODE=1 -e MODEL_PROVIDER=ollama -e OLLAMA_BASE_URL=http://host.docker.internal:11434 -e DEFAULT_VICUNA_MODEL=llama3 infiniflow/ragflow:latest 访问: http://localhost:8080 --- ## 🔧 第五步:部署 Dify(基于 Docker) powershell mkdir D:\AI_Studio\data\dify_data docker run -d --name dify -p 3000:3000 -v D:/AI_Studio/data/dify_data:/app/data -e DATABASE_URL=file:///app/data/dify.db -e OLLAMA_API_BASE=http://host.docker.internal:11434 -e MODEL_PROVIDER=ollama chatchat/oneclick:latest 访问: http://localhost:3000 --- ## 🐍 第六步:安装 Python(用于自动化脚本) ### ✅ 安装建议版本:Python 3.11.x 下载地址: 🔗 [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/) 安装时请勾选: - ✅ Add Python to PATH(添加到系统环境变量) 修改安装路径为: D:\AI_Studio\apps\python ### ✅ 安装常用库 powershell pip install docxtpl requests python-docx pandas flask langchain --- ## 📝 第七步:编写自动化生成 Word 报告脚本 保存路径:D:\AI_Studio\scripts\generate_report.py python from docxtpl import DocxTemplate import os def generate_report(template_name, context): template_path = os.path.join("D:\\AI_Studio\\data\\reports\\templates", template_name) doc = DocxTemplate(template_path) doc.render(context) output_path = os.path.join("D:\\AI_Studio\\data\\reports\\output", f"report_{context['project']}.docx") doc.save(output_path) print(f"Report saved to {output_path}") # 示例调用 generate_report( "geology_template.docx", { "project": "某矿区勘探项目", "location": "新疆某地", "conclusion": "发现高品位铜矿带" } ) --- ## 🔁 第八步:整合工作流(RAGFlow + Dify + Python 报告生成) 你可以通过 Dify 构建如下 AI 工作流: 1. 用户上传 PDF 地质资料 2. Dify 调用 RAGFlow 提取关键信息(如构造、岩性、矿体等) 3. 输出结构化 JSON 数据 4. 传入 Python 脚本填充 Word 模板 5. 自动生成 Word 报告并归档至指定路径 --- ## ✅ 总结部署步骤一览表 | 步骤 | 操作说明 | |------|----------| | 1 | 创建完整目录结构:installer, apps, data, scripts | | 2 | 启用 WSL2 并安装 Ubuntu 子系统 | | 3 | 安装 Docker Desktop 到 D:\AI_Studio\apps\docker | | 4 | 设置 Docker 数据卷路径为 D:\AI_Studio\data\docker | | 5 | 使用 Docker 安装并运行 Ollama | | 6 | 设置 Ollama 模型缓存路径为 D:\AI_Studio\data\models | | 7 | 使用 Docker 部署 RAGFlow 到 D:\AI_Studio\data\ragflow_data | | 8 | 使用 Docker 部署 Dify 到 D:\AI_Studio\data\dify_data | | 9 | 安装 Python 到 D:\AI_Studio\apps\python | | 10 | 编写 Python 脚本实现 Word 报告自动生成 | | 11 | 使用 Dify 构建 AI 工作流,整合 RAGFlow 和 Word 生成模块 | --- ##电脑小白自动安装版

zip
资源下载链接为: https://pan.quark.cn/s/9648a1f24758 这个HTML文件是一个专门设计的网页,适合在告白或纪念日这样的特殊时刻送给女朋友,给她带来惊喜。它通过HTML技术,将普通文字转化为富有情感和创意的表达方式,让数字媒体也能传递深情。HTML(HyperText Markup Language)是构建网页的基础语言,通过标签描述网页结构和内容,让浏览器正确展示页面。在这个特效网页中,开发者可能使用了HTML5的新特性,比如音频、视频、Canvas画布或WebGL图形,来提升视觉效果和交互体验。 原本这个文件可能是基于ASP.NET技术构建的,其扩展名是“.aspx”。ASP.NET是微软开发的一个服务器端Web应用程序框架,支持多种编程语言(如C#或VB.NET)来编写动态网页。但为了在本地直接运行,不依赖服务器,开发者将其转换为纯静态的HTML格式,只需浏览器即可打开查看。 在使用这个HTML特效页时,建议使用Internet Explorer(IE)浏览器,因为一些老的或特定的网页特效可能只在IE上表现正常,尤其是那些依赖ActiveX控件或IE特有功能的页面。不过,由于IE逐渐被淘汰,现代网页可能不再对其进行优化,因此在其他现代浏览器上运行可能会出现问题。 压缩包内的文件“yangyisen0713-7561403-biaobai(html版本)_1598430618”是经过压缩的HTML文件,可能包含图片、CSS样式表和JavaScript脚本等资源。用户需要先解压,然后在浏览器中打开HTML文件,就能看到预设的告白或纪念日特效。 这个项目展示了HTML作为动态和互动内容载体的强大能力,也提醒我们,尽管技术在进步,但有时复古的方式(如使用IE浏览器)仍能唤起怀旧之情。在准备类似的个性化礼物时,掌握基本的HTML和网页制作技巧非常

最新推荐

recommend-type

Mockingbird v2:PocketMine-MP新防作弊机制详解

标题和描述中所涉及的知识点如下: 1. Mockingbird反作弊系统: Mockingbird是一个正在开发中的反作弊系统,专门针对PocketMine-MP服务器。PocketMine-MP是Minecraft Pocket Edition(Minecraft PE)的一个服务器软件,允许玩家在移动平台上共同游戏。随着游戏的普及,作弊问题也随之而来,因此Mockingbird的出现正是为了应对这种情况。 2. Mockingbird的版本迭代: 从描述中提到的“Mockingbird的v1变体”和“v2版本”的变化来看,Mockingbird正在经历持续的开发和改进过程。软件版本迭代是常见的开发实践,有助于修复已知问题,改善性能和用户体验,添加新功能等。 3. 服务器性能要求: 描述中强调了运行Mockingbird的服务器需要具备一定的性能,例如提及“WitherHosting的$ 1.25计划”,这暗示了反作弊系统对服务器资源的需求较高。这可能是因为反作弊机制需要频繁处理大量的数据和事件,以便及时检测和阻止作弊行为。 4. Waterdog问题: Waterdog是另一种Minecraft服务器软件,特别适合 PocketMine-MP。描述中提到如果将Mockingbird和Waterdog结合使用可能会遇到问题,这可能是因为两者在某些机制上的不兼容或Mockingbird对Waterdog的特定实现尚未完全优化。 5. GitHub使用及问题反馈: 作者鼓励用户通过GitHub问题跟踪系统来报告问题、旁路和功能建议。这是一个公共代码托管平台,广泛用于开源项目协作,便于开发者和用户进行沟通和问题管理。作者还提到请用户在GitHub上发布问题而不是在评论区留下不好的评论,这体现了良好的社区维护和用户交流的实践。 6. 软件标签: “pocketmine”和“anticheat”(反作弊)作为标签,说明Mockingbird是一个特别为PocketMine-MP平台开发的反作弊软件。而“PHP”则可能指的是Mockingbird的开发语言,虽然这个信息与常见的Java或C++等开发Minecraft相关软件的语言不同,但并不排除使用PHP进行服务器端开发的可能性,尤其是对于处理动态网页、服务器端脚本等场景。 7. 压缩包文件: “Mockingbird-stable”是一个文件名称,很可能表示这是一个包含最新稳定版Mockingbird反作弊系统的压缩包。通常,这样的文件名中包含“stable”意味着这是一个经过充分测试且推荐用于生产环境的版本。 8. 社区协作和用户参与: 特别感谢部分提到了shur,这可能是对某位贡献者或社区成员的感激之情。这种感谢表明了软件开发不仅是开发者个人的劳动成果,同时也依赖于社区的支持和参与,包括提供反馈、报告问题、贡献代码和文档等。 总结以上内容,我们可以看到Mockingbird作为一款反作弊系统,其开发和维护需要依赖于社区的广泛参与和支持,同时还需要强大的服务器后端作为支撑。通过不断迭代更新版本,开发者希望解决现有问题,提高反作弊效率,并希望与社区保持良好的沟通,以持续优化产品。
recommend-type

“历史人物独白解说”视频:数据处理的6种革命性技术

# 1. 数据处理的历史回顾与技术演进 数据处理的历史,从最初的简单机械记录,到如今复杂的数据处理体系,不仅反映了技术的演进,也映射了人类社会进步的轨迹。在计算机技术发展初期,数据处理主要依靠打孔卡片和磁带,通过批处理的方式进行,效率低下且出错率高。随着技术的发展,上世纪70年代,关系型数据库的出现大大提高了数据处理的效率和准确性,为数据处理技术的发展奠定了基础。 随后,随着互
recommend-type

怎么判断多级运放电路的稳定性?

<think>首先,用户的问题是关于判断多级运算放大器电路的稳定性。我需要根据系统级指令来构建回答。 系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考引用: - 引用[1]:关于集成运算放大电路的设计、组成和性能评估。 - 引用[2]:高频电路中运放的带宽限制,一级放大电路的增益通常为100倍,过高会引起振
recommend-type

利用AHP和节点集中度解决影响力最大化问题的Flask应用教程

从给定的文件信息中,我们可以提取以下相关知识点进行详细说明: ### 标题知识点 **IM问题与AHP结合** IM问题(Influence Maximization)是网络分析中的一个核心问题,旨在识别影响网络中信息传播的关键节点。为了求解IM问题,研究者们常常结合使用不同的算法和策略,其中AHP(Analytic Hierarchy Process,分析层次结构过程)作为一种决策分析方法,被用于评估网络节点的重要性。AHP通过建立层次模型,对各个因素进行比较排序,从而量化影响度,并通过一致性检验保证决策结果的有效性。将AHP应用于IM问题,意味着将分析网络节点影响的多个维度,比如节点的中心性(centrality)和影响力。 **集中度措施** 集中度(Centralization)是衡量网络节点分布状况的指标,它反映了网络中节点之间的连接关系。在网络分析中,集中度常用于识别网络中的“枢纽”或“中心”节点。例如,通过计算网络的度中心度(degree centrality)可以了解节点与其他节点的直接连接数量;接近中心度(closeness centrality)衡量节点到网络中其他所有节点的平均距离;中介中心度(betweenness centrality)衡量节点在连接网络中其他节点对的最短路径上的出现频率。集中度高意味着节点在网络中处于重要位置,对信息的流动和控制具有较大影响力。 ### 描述知识点 **Flask框架** Flask是一个轻量级的Web应用框架,它使用Python编程语言开发。它非常适合快速开发小型Web应用,以及作为微服务架构的一部分。Flask的一个核心特点是“微”,意味着它提供了基本的Web开发功能,同时保持了框架的小巧和灵活。Flask内置了开发服务器,支持Werkzeug WSGI工具包和Jinja2模板引擎,提供了RESTful请求分发和请求钩子等功能。 **应用布局** 一个典型的Flask应用会包含以下几个关键部分: - `app/`:这是应用的核心目录,包含了路由设置、视图函数、模型和控制器等代码文件。 - `static/`:存放静态文件,比如CSS样式表、JavaScript文件和图片等,这些文件的内容不会改变。 - `templates/`:存放HTML模板文件,Flask将使用这些模板渲染最终的HTML页面。模板语言通常是Jinja2。 - `wsgi.py`:WSGI(Web Server Gateway Interface)是Python应用程序和Web服务器之间的一种标准接口。这个文件通常用于部署到生产服务器时,作为应用的入口点。 **部署到Heroku** Heroku是一个支持多种编程语言的云平台即服务(PaaS),它允许开发者轻松部署、运行和管理应用。部署Flask应用到Heroku,需要几个步骤:首先,创建一个Procfile文件,告知Heroku如何启动应用;其次,确保应用的依赖关系被正确管理,通常通过一个requirements.txt文件列出所有依赖;最后,使用Git将应用推送到Heroku提供的仓库,Heroku会自动识别Procfile并开始部署过程。 ### 标签知识点 **HTML** HTML(HyperText Markup Language,超文本标记语言)是用于创建网页和Web应用的标准标记语言。它定义了网页的结构和内容。HTML文件由一系列元素组成,这些元素通过标签(tags)来表示,如`<p>`代表段落,`<a>`代表链接,`<img>`代表图像等。HTML5是当前使用的最新版本,支持更多的特性,如离线存储、多媒体和图形等。 ### 压缩包子文件的文件名称列表知识点 **IMproblem-using-AHP-and-centralisation-of-nodes-master** 这里的文件名称“IMproblem-using-AHP-and-centralisation-of-nodes-master”表明了一个GitHub仓库的名称,其中包含了源代码以及与项目相关的所有文件。从名称中可以看出,该仓库是关于如何结合AHP和节点集中度分析来解决IM问题的Flask应用程序。文件名中的“master”表明这是仓库的主分支(现在叫做main分支),它是项目最新的、可部署的代码版本。 综合来看,本段信息为我们提供了构建和部署一个使用Flask框架、针对IM问题使用AHP和节点集中度分析的Web应用的方法和步骤。同时,介绍了应用在不同环节中所需技术和组件的详细知识点。
recommend-type

视频内容自动生成算法:突破性的8大最新进展

# 1. 视频内容自动生成算法概述 ## 算法发展背景 随着人工智能技术的迅速发展,视频内容自动生成算法已经成为媒体和娱乐行业的重要工具。这些算法能够自动编辑和合成视频内容,使内容创作者能够以较低的成本和时间生成高质量的视频。从社交媒体动态到在线教育内容,视频内容自动生成的应用场景正在不断扩大。 ## 核心技术简述 视
recommend-type

Softmax函数实际应用举例

<think>嗯,用户现在想了解Softmax函数的实际应用场景,这和我之前详细解释过的Softmax原理是直接相关的。用户特意提到要参考站内引用,看来是想看到具体案例支撑。 从用户提问方式看,ta应该已经理解了Softmax的基本数学原理(毕竟我上次用向量[2,1,0.1]的例子详细演示过计算过程),现在需要落地到实践层面。这种从理论到应用的递进很合理,可能是个正在学习机器学习的学生,或者需要解决实际问题的工程师。 我注意到用户提供的引用里有些关键线索:引用[1]提到图像分类和概率解释,引用[4]强调指数放大的特性,引用[5]则对比了Sigmoid在多标签分类的应用差异。这些正好能支撑我
recommend-type

WDI项目1:PriceIsRight游戏开发实践

### 标题解析 标题“price-is-right:WDI项目1-PriceIsRight游戏”表明这是一个名为“Price Is Right”的游戏项目,这是WDI(Web Development Immersive,全栈网页开发沉浸式课程)的第一个项目。WDI是一种常用于IT培训机构的课程名称,旨在通过实战项目来培养学员的全栈网页开发能力。 ### 描述解析 描述中提到,该游戏的目的是为了练习基本的JavaScript技能。这表明游戏被设计成一个编程练习,让开发者通过实现游戏逻辑来加深对JavaScript的理解。描述中也提到了游戏是一个支持两个玩家的版本,包含了分配得分、跟踪得分以及宣布获胜者等逻辑,这是游戏开发中常见的功能实现。 开发者还提到使用了Bootstrap框架来增加网站的可伸缩性。Bootstrap是一个流行的前端框架,它让网页设计和开发工作更加高效,通过提供预设的CSS样式和JavaScript组件,让开发者能够快速创建出响应式的网站布局。此外,开发者还使用了HTML5和CSS进行网站设计,这表明项目也涉及到了前端开发的基础技能。 ### 标签解析 标签“JavaScript”指出了该游戏中核心编程语言的使用。JavaScript是一种高级编程语言,常用于网页开发中,负责实现网页上的动态效果和交互功能。通过使用JavaScript,开发者可以在不离开浏览器的情况下实现复杂的游戏逻辑和用户界面交互。 ### 文件名称解析 压缩包子文件的文件名称列表中仅提供了一个条目:“price-is-right-master”。这里的“master”可能指明了这是项目的主分支或者主版本,通常在版本控制系统(如Git)中使用。文件名中的“price-is-right”与标题相呼应,表明该文件夹内包含的代码和资源是与“Price Is Right”游戏相关的。 ### 知识点总结 #### 1. JavaScript基础 - **变量和数据类型**:用于存储得分等信息。 - **函数和方法**:用于实现游戏逻辑,如分配得分、更新分数。 - **控制结构**:如if-else语句和循环,用于实现游戏流程控制。 - **事件处理**:监听玩家的输入(如点击按钮)和游戏状态的变化。 #### 2. Bootstrap框架 - **网格系统**:实现响应式布局,让游戏界面在不同设备上都能良好展示。 - **预设组件**:可能包括按钮、表单、警告框等,用于快速开发用户界面。 - **定制样式**:根据需要自定义组件样式来符合游戏主题。 #### 3. HTML5与CSS - **语义化标签**:使用HTML5提供的新标签来构建页面结构,如`<header>`, `<section>`, `<footer>`等。 - **CSS布局**:使用Flexbox或Grid等布局技术对页面元素进行定位和排版。 - **样式设计**:通过CSS为游戏界面增添美观的视觉效果。 #### 4. 项目结构和版本控制 - **主分支管理**:`master`分支通常保存着项目的稳定版本,用于部署生产环境。 - **代码组织**:合理的文件结构有助于维护和扩展项目。 #### 5. 前端开发最佳实践 - **分离关注点**:将样式、脚本和内容分离,确保代码清晰易维护。 - **响应式设计**:确保游戏在多种设备和屏幕尺寸上均有良好的用户体验。 - **可访问性**:考虑键盘导航、屏幕阅读器等无障碍功能,让游戏更加友好。 #### 6. 交互式游戏开发 - **游戏逻辑实现**:创建一个简单的游戏循环,管理玩家输入和得分更新。 - **状态管理**:游戏中的得分和其他游戏状态需要妥善保存和更新。 - **用户界面反馈**:提供即时的视觉和听觉反馈,增强玩家体验。 通过上述知识点的解析,可以看出“Price Is Right”游戏项目不仅仅是一个简单的编程练习,它还融合了多种前端技术,包括JavaScript、Bootstrap、HTML5和CSS,以实现一个完整的、可交互的游戏体验。此项目也反映了开发者在掌握前端开发技能的同时,了解了如何组织代码、维护项目结构和实践开发最佳实践。
recommend-type

人工智能视频编辑:如何利用技术进步提升内容创作质量

# 1. 人工智能视频编辑概述 随着人工智能技术的飞速发展,视频编辑领域正在经历一场前所未有的革命。AI的介入,不仅极大地提升了视频编辑的效率,还赋予了内容创作者全新的表达方式。本章旨在概述人工智能视频编辑的概念、发展历程和当前的应用现状,为读者提供一个全面而深入的了解。 ## 1.1 人工智能视频编辑的兴起 人工智能视频编辑是将先进的机器学习算法与传统视频处理技术相
recommend-type

反激变换器

<think>首先,用户意图是了解反激变换器的工作原理或设计方法。用户的问题是:“反激变换器工作原理及设计指南”。所以我需要基于这个意图和问题,通过调用网络搜索工具进行检索。 我的任务是: 1. 调用网络搜索工具来检索相关内容。 2. 整合检索到的内容,生成详细、清晰、实用、可操作的回答。 3. 禁止将所有引用内容集中在回答末尾;引用应该融入回答中。 4. 避免使用第一人称如“我”。 5. 避免步骤词汇如“首先”、“然后”。 6. 将答案组织为多个方法或步骤,并在每个步骤间换行。 7. 对于代码和公式,遵循特定格式: - 行内公式:$公式内容$ - 独立公式:$$公式内容$$
recommend-type

Node.js 应用模块加载示例教程

Node.js 应用示例项目分析 标题 “nodejs-app-sample” 指明这个文件或文件夹是一个Node.js应用程序的示例。Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它使得JavaScript可以在服务器端运行。Node.js采用事件驱动、非阻塞I/O模型,使其轻量又高效,并且非常适合处理高并发的网络请求,广泛应用于构建网络服务器。 描述部分提到 “Dioscouri可加载模块的示例”。Dioscouri可能是一个拼写错误,实际上可能是指“Discord”。这里我们可以假设描述信息中的“Dioscouri”意在指向“Discord.js”,这是一个流行的Node.js模块,用于构建和使用Discord机器人。Discord是一个主要用于视频游戏社区的即时通讯平台,而Discord.js模块提供了与Discord API交互的能力,使得开发者可以通过编写Node.js应用来创建机器人或其他自定义功能。 标签 “JavaScript” 明确指出了该示例项目的编程语言。JavaScript是一种广泛使用的高级、解释型编程语言,它遵循ECMAScript规范,并且是Web开发中最核心的技术之一。在Node.js环境中,JavaScript被用来实现后端逻辑,与浏览器端JavaScript共享相似的语法和特性,但是能够访问文件系统、网络接口等服务器端资源。 关于 “压缩包子文件的文件名称列表: nodejs-app-sample-master”,这部分信息表明文件或文件夹是某个版本控制系统(如Git)的master分支,并且被压缩打包。通常情况下,开发人员会使用版本控制系统来跟踪和管理代码的变更,并且在需要分享或备份项目时,会将代码库打包成压缩文件,以便于传输和部署。这里的“nodejs-app-sample-master”很可能是一个包含完整Node.js应用示例代码的压缩包。 从这些信息中,我们可以提炼出以下知识点: 1. Node.js基础:Node.js是一个运行在服务器端的JavaScript环境,它允许开发者使用JavaScript编写服务器端应用。Node.js的非阻塞I/O模型和事件循环机制,使其适合于构建大型的、分布式的实时应用。 2. JavaScript编程语言:JavaScript是Web开发的核心技术之一,用于编写在浏览器端运行的脚本。Node.js的出现使得JavaScript不再局限于浏览器,还可以用于服务器端的编程。 3. Discord.js模块:这是一个用于创建和管理Discord机器人的Node.js模块。通过Discord.js,开发者可以方便地实现消息监听、响应、频道管理、用户权限控制等多种功能。 4. 版本控制系统:这类系统用于管理代码变更历史,帮助团队协作开发,避免冲突,并提供代码的版本回溯。Git是最流行的版本控制系统之一。 5. 代码打包:在版本控制系统中,代码库可以被打包压缩,方便进行传输、分发或备份。这种做法在开源社区尤为常见,也广泛应用于企业环境。 6. 项目结构:一般来说,一个Node.js项目会包含一个`package.json`文件,该文件描述了项目的依赖关系和其它元数据。项目中还可能包含`node_modules`文件夹,用于存放项目依赖的模块。其他常见的项目文件包括脚本文件(例如`server.js`)、配置文件、测试文件等。 通过上述分析,我们可以了解到,这个“nodejs-app-sample”很可能是一个基于Node.js和JavaScript构建的示例项目,专门用于展示如何利用Discord.js模块来实现一个Discord机器人,同时该示例项目可能存在于一个版本控制仓库的master分支上,并被打包提供给开发者学习和参考。