检查以下代码,提供优化建议:
import os
import sys
import json
import time
import wave
import librosa
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
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,
QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox
)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from pydub import AudioSegment
from transformers import pipeline
from pyannote.audio import Pipeline
import torch
import whisper
import speech_recognition as sr
# 设置环境变量(用户需要替换为自己的Hugging Face Token)
os.environ['HUGGINGFACE_TOKEN'] = 'your_hugging_face_token_here'
# 模型路径配置
MODEL_DIR = "./models"
WHISPER_MODEL_PATH = os.path.join(MODEL_DIR, "whisper-small")
SENTIMENT_MODEL_PATH = os.path.join(MODEL_DIR, "Erlangshen-Roberta-110M-Sentiment")
PYANNOTE_MODEL_PATH = os.path.join(MODEL_DIR, "pyannote-speaker-diarization-3.0")
# 确保模型目录存在
os.makedirs(MODEL_DIR, exist_ok=True)
class AnalysisWorker(QThread):
"""后台分析线程"""
progress_updated = pyqtSignal(int, str)
analysis_completed = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
def __init__(self, audio_files, keyword_file):
super().__init__()
self.audio_files = audio_files
self.keyword_file = keyword_file
self.keywords = {}
self.running = True
def run(self):
try:
# 加载关键词
self.load_keywords()
# 加载模型
self.progress_updated.emit(5, "加载模型中...")
self.load_models()
results = []
total_files = len(self.audio_files)
for idx, audio_file in enumerate(self.audio_files):
if not self.running:
return
file_name = os.path.basename(audio_file)
self.progress_updated.emit(int((idx/total_files)*90 + 5), f"分析文件: {file_name}")
# 分析单个文件
result = self.analyze_audio(audio_file)
results.append(result)
# 生成报告
self.progress_updated.emit(95, "生成分析报告...")
report = self.generate_report(results)
# 完成
self.progress_updated.emit(100, "分析完成!")
self.analysis_completed.emit(report)
except Exception as e:
self.error_occurred.emit(str(e))
def load_keywords(self):
"""从Excel文件加载关键词"""
try:
df = pd.read_excel(self.keyword_file)
self.keywords = {
"opening": df["开场白关键词"].dropna().tolist(),
"closing": df["结束语关键词"].dropna().tolist(),
"forbidden": df["服务禁语"].dropna().tolist()
}
self.progress_updated.emit(2, f"加载关键词: {len(self.keywords['opening'])}个开场白, "
f"{len(self.keywords['closing'])}个结束语, "
f"{len(self.keywords['forbidden'])}个禁语")
except Exception as e:
raise Exception(f"加载关键词文件失败: {str(e)}")
def load_models(self):
"""加载所有需要的模型"""
# 加载语音识别模型
self.whisper_model = whisper.load_model("small", download_root=MODEL_DIR)
# 加载情感分析模型
self.sentiment_model = pipeline(
"text-classification",
model=SENTIMENT_MODEL_PATH,
tokenizer=SENTIMENT_MODEL_PATH
)
# 加载说话人分离模型
self.pyannote_pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.0",
use_auth_token=os.environ['HUGGINGFACE_TOKEN']
)
def convert_audio_format(self, audio_file):
"""将音频转换为WAV格式(如果需要)"""
if not audio_file.lower().endswith('.wav'):
output_file = os.path.splitext(audio_file)[0] + '.wav'
audio = AudioSegment.from_file(audio_file)
audio.export(output_file, format="wav")
return output_file
return audio_file
def get_audio_duration(self, audio_file):
"""获取音频时长"""
with wave.open(audio_file, 'r') as wav_file:
frames = wav_file.getnframes()
rate = wav_file.getframerate()
duration = frames / float(rate)
return duration
def analyze_audio(self, audio_file):
"""分析单个音频文件"""
result = {
"file_name": os.path.basename(audio_file),
"duration": 0,
"opening_found": False,
"closing_found": False,
"forbidden_found": False,
"agent_sentiment": [],
"customer_sentiment": [],
"agent_speech_rate": 0,
"agent_clarity": 0,
"agent_volume": 0,
"problem_solved": False
}
try:
# 转换音频格式
wav_file = self.convert_audio_format(audio_file)
# 获取音频时长
result["duration"] = self.get_audio_duration(wav_file)
# 说话人分离
diarization = self.pyannote_pipeline(wav_file)
# 识别第一个说话人(默认客服)
agent_segments = []
customer_segments = []
for turn, _, speaker in diarization.itertracks(yield_label=True):
# 假设第一个说话人是客服
if speaker == "SPEAKER_00":
agent_segments.append((turn.start, turn.end))
else:
customer_segments.append((turn.start, turn.end))
# 如果没有识别到说话人,使用默认分割
if not agent_segments and not customer_segments:
mid = result["duration"] / 2
agent_segments = [(0, mid)]
customer_segments = [(mid, result["duration"])]
# 识别语音转文本
recognizer = sr.Recognizer()
with sr.AudioFile(wav_file) as source:
audio_data = recognizer.record(source)
# 识别客服语音
agent_text = ""
for start, end in agent_segments:
segment = audio_data.get_segment(start*1000, end*1000)
try:
text = recognizer.recognize_google(segment, language="zh-CN")
agent_text += text + " "
except:
pass
# 识别客户语音
customer_text = ""
for start, end in customer_segments:
segment = audio_data.get_segment(start*1000, end*1000)
try:
text = recognizer.recognize_google(segment, language="zh-CN")
customer_text += text + " "
except:
pass
# 关键词检查
result["opening_found"] = any(keyword in agent_text for keyword in self.keywords["opening"])
result["closing_found"] = any(keyword in agent_text for keyword in self.keywords["closing"])
result["forbidden_found"] = any(keyword in agent_text for keyword in self.keywords["forbidden"])
# 情感分析
if agent_text:
agent_sentiments = self.sentiment_model(agent_text, top_k=None)
result["agent_sentiment"] = [{"label": s["label"], "score": s["score"]} for s in agent_sentiments]
if customer_text:
customer_sentiments = self.sentiment_model(customer_text, top_k=None)
result["customer_sentiment"] = [{"label": s["label"], "score": s["score"]} for s in customer_sentiments]
# 客服语音分析
y, sr = librosa.load(wav_file)
agent_audio = []
for start, end in agent_segments:
start_sample = int(start * sr)
end_sample = int(end * sr)
agent_audio.extend(y[start_sample:end_sample])
if agent_audio:
agent_audio = np.array(agent_audio)
# 计算语速(音节/秒)
syllables = len(agent_text) / 2 # 简单估算
speaking_time = sum(end - start for start, end in agent_segments)
result["agent_speech_rate"] = syllables / speaking_time if speaking_time > 0 else 0
# 计算清晰度(频谱质心)
spectral_centroids = librosa.feature.spectral_centroid(y=agent_audio, sr=sr)[0]
result["agent_clarity"] = np.mean(spectral_centroids)
# 计算平均音量
result["agent_volume"] = np.mean(librosa.amplitude_to_db(np.abs(agent_audio)))
# 问题解决率(简化逻辑)
solution_keywords = ["解决", "完成", "处理", "安排", "办理"]
result["problem_solved"] = any(keyword in agent_text for keyword in solution_keywords)
# 清理临时文件
if wav_file != audio_file:
os.remove(wav_file)
return result
except Exception as e:
self.error_occurred.emit(f"分析文件 {audio_file} 时出错: {str(e)}")
return None
def generate_report(self, results):
"""生成分析报告"""
# 过滤掉失败的分析
valid_results = [r for r in results if r is not None]
if not valid_results:
return {"excel_path": "", "chart_path": ""}
# 创建报告目录
report_dir = os.path.join(os.getcwd(), "质检报告")
os.makedirs(report_dir, exist_ok=True)
timestamp = time.strftime("%Y%m%d%H%M%S")
# 生成Excel报告
excel_path = os.path.join(report_dir, f"质检报告_{timestamp}.xlsx")
self.generate_excel_report(valid_results, excel_path)
# 生成图表报告
chart_path = os.path.join(report_dir, f"图表报告_{timestamp}.png")
self.generate_chart_report(valid_results, chart_path)
return {
"excel_path": excel_path,
"chart_path": chart_path,
"results": valid_results
}
def generate_excel_report(self, results, output_path):
"""生成Excel格式的报告"""
df = pd.DataFrame(results)
# 处理情感分析数据
df['客服情感'] = df['agent_sentiment'].apply(
lambda x: max(x, key=lambda s: s['score'])['label'] if x else '未知'
)
df['客户情感'] = df['customer_sentiment'].apply(
lambda x: max(x, key=lambda s: s['score'])['label'] if x else '未知'
)
# 选择需要的列
report_df = df[[
"file_name", "duration", "opening_found", "closing_found",
"forbidden_found", "客服情感", "客户情感", "agent_speech_rate",
"agent_clarity", "agent_volume", "problem_solved"
]]
# 重命名列
report_df.columns = [
"文件名", "通话时长(秒)", "开场白合规", "结束语合规", "使用禁语",
"客服情感", "客户情感", "客服语速(音节/秒)", "客服清晰度", "客服平均音量(dB)", "问题解决"
]
# 保存Excel
report_df.to_excel(output_path, index=False)
def generate_chart_report(self, results, output_path):
"""生成图表报告"""
# 创建图表
fig, axes = plt.subplots(3, 2, figsize=(15, 15))
fig.suptitle('外呼录音质检分析报告', fontsize=16)
# 1. 开场白合规率
opening_compliance = sum(1 for r in results if r['opening_found']) / len(results) * 100
axes[0, 0].bar(['合规', '不合规'], [opening_compliance, 100-opening_compliance], color=['green', 'red'])
axes[0, 0].set_title('开场白合规率')
axes[0, 0].set_ylabel('百分比(%)')
# 2. 客服情感分布
agent_sentiments = [max(r['agent_sentiment'], key=lambda s: s['score'])['label'] for r in results if r['agent_sentiment']]
sentiment_counts = pd.Series(agent_sentiments).value_counts()
axes[0, 1].pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%')
axes[0, 1].set_title('客服情感分布')
# 3. 客户情感分布
customer_sentiments = [max(r['customer_sentiment'], key=lambda s: s['score'])['label'] for r in results if r['customer_sentiment']]
sentiment_counts = pd.Series(customer_sentiments).value_counts()
axes[1, 0].pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%')
axes[1, 0].set_title('客户情感分布')
# 4. 客服语速分布
speech_rates = [r['agent_speech_rate'] for r in results if r['agent_speech_rate'] > 0]
axes[1, 1].hist(speech_rates, bins=10, color='skyblue', edgecolor='black')
axes[1, 1].set_title('客服语速分布')
axes[1, 1].set_xlabel('语速(音节/秒)')
axes[1, 1].set_ylabel('频次')
# 5. 问题解决率
problem_solved = sum(1 for r in results if r['problem_solved']) / len(results) * 100
axes[2, 0].bar(['已解决', '未解决'], [problem_solved, 100-problem_solved], color=['blue', 'orange'])
axes[2, 0].set_title('问题解决率')
axes[2, 0].set_ylabel('百分比(%)')
# 6. 使用禁语情况
forbidden_found = sum(1 for r in results if r['forbidden_found'])
axes[2, 1].bar(['使用禁语', '未使用禁语'], [forbidden_found, len(results)-forbidden_found], color=['red', 'green'])
axes[2, 1].set_title('禁语使用情况')
axes[2, 1].set_ylabel('数量')
plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig(output_path)
plt.close()
def stop(self):
"""停止分析"""
self.running = False
class CallQualityApp(QMainWindow):
"""主应用程序窗口"""
def __init__(self):
super().__init__()
self.setWindowTitle("外呼电话录音质检分析系统")
self.setGeometry(100, 100, 1000, 700)
# 初始化变量
self.audio_files = []
self.keyword_file = ""
self.worker = None
# 创建UI
self.init_ui()
def init_ui(self):
"""初始化用户界面"""
main_widget = QWidget()
main_layout = QVBoxLayout()
# 创建标签页
tab_widget = QTabWidget()
main_layout.addWidget(tab_widget)
# 分析页面
analysis_tab = QWidget()
tab_widget.addTab(analysis_tab, "分析")
analysis_layout = QVBoxLayout(analysis_tab)
# 文件选择区域
file_group = QGroupBox("文件选择")
file_layout = QVBoxLayout()
# 音频文件选择
audio_layout = QHBoxLayout()
self.audio_label = QLabel("未选择音频文件")
audio_btn = QPushButton("选择音频文件/文件夹")
audio_btn.clicked.connect(self.select_audio)
audio_layout.addWidget(audio_btn)
audio_layout.addWidget(self.audio_label)
file_layout.addLayout(audio_layout)
# 关键词文件选择
keyword_layout = QHBoxLayout()
self.keyword_label = QLabel("未选择关键词文件")
keyword_btn = QPushButton("选择关键词文件")
keyword_btn.clicked.connect(self.select_keyword_file)
keyword_layout.addWidget(keyword_btn)
keyword_layout.addWidget(self.keyword_label)
file_layout.addLayout(keyword_layout)
file_group.setLayout(file_layout)
analysis_layout.addWidget(file_group)
# 按钮区域
btn_layout = QHBoxLayout()
self.start_btn = QPushButton("开始分析")
self.start_btn.clicked.connect(self.start_analysis)
self.stop_btn = QPushButton("停止分析")
self.stop_btn.clicked.connect(self.stop_analysis)
self.stop_btn.setEnabled(False)
self.clear_btn = QPushButton("清空")
self.clear_btn.clicked.connect(self.clear_all)
btn_layout.addWidget(self.start_btn)
btn_layout.addWidget(self.stop_btn)
btn_layout.addWidget(self.clear_btn)
analysis_layout.addLayout(btn_layout)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_label = QLabel("准备就绪")
analysis_layout.addWidget(self.progress_bar)
analysis_layout.addWidget(self.progress_label)
# 输出区域
output_group = QGroupBox("输出信息")
output_layout = QVBoxLayout()
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
output_layout.addWidget(self.output_text)
output_group.setLayout(output_layout)
analysis_layout.addWidget(output_group)
# 结果页面
result_tab = QWidget()
tab_widget.addTab(result_tab, "结果")
result_layout = QVBoxLayout(result_tab)
# 结果表格
self.result_table = QTableWidget()
self.result_table.setColumnCount(11)
self.result_table.setHorizontalHeaderLabels([
"文件名", "通话时长(秒)", "开场白合规", "结束语合规", "使用禁语",
"客服情感", "客户情感", "客服语速", "客服清晰度", "客服音量", "问题解决"
])
self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
result_layout.addWidget(self.result_table)
# 图表区域
self.chart_label = QLabel()
self.chart_label.setAlignment(Qt.AlignCenter)
result_layout.addWidget(self.chart_label)
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
def select_audio(self):
"""选择音频文件或文件夹"""
options = QFileDialog.Options()
files, _ = QFileDialog.getOpenFileNames(
self, "选择音频文件", "",
"音频文件 (*.mp3 *.wav *.amr *.m4a);;所有文件 (*)",
options=options
)
if files:
self.audio_files = files
self.audio_label.setText(f"已选择 {len(files)} 个音频文件")
self.output_text.append(f"已选择 {len(files)} 个音频文件")
def select_keyword_file(self):
"""选择关键词文件"""
options = QFileDialog.Options()
file, _ = QFileDialog.getOpenFileName(
self, "选择关键词文件", "",
"Excel文件 (*.xlsx *.xls);;所有文件 (*)",
options=options
)
if file:
self.keyword_file = file
self.keyword_label.setText(os.path.basename(file))
self.output_text.append(f"已选择关键词文件: {os.path.basename(file)}")
def start_analysis(self):
"""开始分析"""
if not self.audio_files:
QMessageBox.warning(self, "警告", "请先选择音频文件!")
return
if not self.keyword_file:
QMessageBox.warning(self, "警告", "请先选择关键词文件!")
return
# 更新UI状态
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.output_text.append("开始分析...")
# 创建并启动工作线程
self.worker = AnalysisWorker(self.audio_files, self.keyword_file)
self.worker.progress_updated.connect(self.update_progress)
self.worker.analysis_completed.connect(self.analysis_finished)
self.worker.error_occurred.connect(self.handle_error)
self.worker.start()
def stop_analysis(self):
"""停止分析"""
if self.worker:
self.worker.stop()
self.worker = None
self.output_text.append("分析已停止")
self.progress_label.setText("分析已停止")
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
def clear_all(self):
"""清空所有内容"""
self.audio_files = []
self.keyword_file = ""
self.audio_label.setText("未选择音频文件")
self.keyword_label.setText("未选择关键词文件")
self.output_text.clear()
self.progress_bar.setValue(0)
self.progress_label.setText("准备就绪")
self.result_table.setRowCount(0)
self.chart_label.clear()
def update_progress(self, value, message):
"""更新进度"""
self.progress_bar.setValue(value)
self.progress_label.setText(message)
self.output_text.append(message)
def analysis_finished(self, report):
"""分析完成"""
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
# 显示结果
self.output_text.append(f"分析完成! 报告已保存到: {report['excel_path']}")
self.output_text.append(f"图表已保存到: {report['chart_path']}")
# 在结果表格中显示数据
self.show_results(report['results'])
# 显示图表
self.show_chart(report['chart_path'])
def handle_error(self, error_message):
"""处理错误"""
self.output_text.append(f"错误: {error_message}")
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
QMessageBox.critical(self, "错误", error_message)
def show_results(self, results):
"""在表格中显示结果"""
self.result_table.setRowCount(len(results))
for row, result in enumerate(results):
# 获取客服主要情感
agent_sentiment = max(result['agent_sentiment'], key=lambda s: s['score'])['label'] if result['agent_sentiment'] else "未知"
customer_sentiment = max(result['customer_sentiment'], key=lambda s: s['score'])['label'] if result['customer_sentiment'] else "未知"
# 填充表格数据
self.result_table.setItem(row, 0, QTableWidgetItem(result['file_name']))
self.result_table.setItem(row, 1, QTableWidgetItem(f"{result['duration']:.2f}"))
self.result_table.setItem(row, 2, QTableWidgetItem("是" if result['opening_found'] else "否"))
self.result_table.setItem(row, 3, QTableWidgetItem("是" if result['closing_found'] else "否"))
self.result_table.setItem(row, 4, QTableWidgetItem("是" if result['forbidden_found'] else "否"))
self.result_table.setItem(row, 5, QTableWidgetItem(agent_sentiment))
self.result_table.setItem(row, 6, QTableWidgetItem(customer_sentiment))
self.result_table.setItem(row, 7, QTableWidgetItem(f"{result['agent_speech_rate']:.2f}"))
self.result_table.setItem(row, 8, QTableWidgetItem(f"{result['agent_clarity']:.2f}"))
self.result_table.setItem(row, 9, QTableWidgetItem(f"{result['agent_volume']:.2f}"))
self.result_table.setItem(row, 10, QTableWidgetItem("是" if result['problem_solved'] else "否"))
def show_chart(self, chart_path):
"""显示图表"""
if os.path.exists(chart_path):
pixmap = plt.imread(chart_path)
# 这里简化处理,实际应用中可能需要调整图像大小
self.chart_label.setPixmap(pixmap)
else:
self.output_text.append(f"未找到图表文件: {chart_path}")
if __name__ == "__main__":
# 检查环境变量
if 'HUGGINGFACE_TOKEN' not in os.environ or os.environ['HUGGINGFACE_TOKEN'] == 'your_hugging_face_token_here':
print("请设置有效的Hugging Face Token!")
print("1. 访问 https://huggingface.co/settings/tokens 获取Token")
print("2. 在代码中设置环境变量")
sys.exit(1)
app = QApplication(sys.argv)
window = CallQualityApp()
window.show()
sys.exit(app.exec_())
最新发布