#下面程序运行地报错:
分析过程中发生错误
启动分析线程失败: 'MultiModalApp' object has no attribute 'update_progress'
Traceback (most recent call last):
File "D:\PyCharmMiscProject\2025-08-10——Rag高度增能.py", line 1044, in _analyze_image
self.analysis_thread.progress_updated.connect(self.update_progress)
^^^^^^^^^^^^^^^^^^^^
AttributeError: 'MultiModalApp' object has no attribute 'update_progress'
——————————————————————————————————————————————————————————————————————————————
import sys
import os
import base64
import json
import requests
import webbrowser
import traceback
import psutil
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTextEdit, QFileDialog, QGroupBox, QSlider,
QDoubleSpinBox, QProgressBar, QSplitter, QMessageBox,
QListWidget, QListWidgetItem, QSystemTrayIcon, QMenu, QAction,
QComboBox, QTabWidget, QScrollArea, QCheckBox, QInputDialog, QLineEdit,
QStyle, QSizePolicy
)
from PyQt5.QtGui import (
QPixmap, QFont, QPalette, QColor, QTextCursor, QIcon,
QTextDocumentWriter, QTextDocument, QImage, QPainter
)
from PyQt5.QtCore import (
Qt, QSize, QThread, pyqtSignal, QTimer, QSettings, QMutex,
QCoreApplication, QBuffer
)
from PIL import Image, ImageOps
# 配置OLLAMA API设置
OLLAMA_HOST = "http://localhost:11434"
HISTORY_FILE = "history.json"
SETTINGS_FILE = "settings.ini"
MAX_COMPRESSED_SIZE = 1024 # 最大压缩尺寸
MAX_THREADS = 2 # 最大并发线程数
# 全局锁防止资源冲突
analysis_mutex = QMutex()
class ModelLoaderThread(QThread):
models_loaded = pyqtSignal(list)
error_occurred = pyqtSignal(str)
def run(self):
try:
response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10)
response.raise_for_status()
models_data = response.json()
models = []
for model in models_data.get("models", []):
# 保留完整的模型名称(包含冒号)
model_name = model["name"]
models.append(model_name)
self.models_loaded.emit(models)
except Exception as e:
self.error_occurred.emit(f"模型加载失败: {str(e)}")
class ImageAnalysisThread(QThread):
analysis_complete = pyqtSignal(str, str) # 结果, 图片路径
progress_updated = pyqtSignal(int)
error_occurred = pyqtSignal(str)
stream_data = pyqtSignal(str)
def __init__(self, model_name, image_path, temperature, max_tokens, prompt, parent=None):
super().__init__(parent)
self.model_name = model_name
self.image_path = image_path
self.temperature = temperature
self.max_tokens = max_tokens
self.prompt = prompt
self._is_running = True
def run(self):
# 获取锁,防止多个线程同时访问资源
analysis_mutex.lock()
try:
# 检查图片文件是否存在
if not os.path.exists(self.image_path):
self.error_occurred.emit(f"图片文件不存在: {self.image_path}")
return
# 压缩图片
compressed_path = self.compress_image(self.image_path)
if not compressed_path:
compressed_path = self.image_path
# 读取并编码图片为base64
with open(compressed_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
# 构建请求数据
data = {
"model": self.model_name,
"prompt": self.prompt,
"images": [base64_image],
"stream": True,
"options": {
"temperature": self.temperature,
"num_predict": self.max_tokens
}
}
# 发送请求并处理流式响应
response = requests.post(
f"{OLLAMA_HOST}/api/generate",
json=data,
stream=True,
timeout=60
)
response.raise_for_status()
full_response = ""
for line in response.iter_lines():
if not self._is_running:
break
if line:
decoded_line = line.decode("utf-8")
try:
json_data = json.loads(decoded_line)
if "response" in json_data:
chunk = json_data["response"]
full_response += chunk
self.stream_data.emit(chunk)
if "done" in json_data and json_data["done"]:
break
if "error" in json_data:
self.error_occurred.emit(json_data["error"])
return
except json.JSONDecodeError:
self.error_occurred.emit("无效的API响应")
return
if self._is_running:
self.analysis_complete.emit(full_response, self.image_path)
except Exception as e:
error_msg = f"API调用失败: {str(e)}\n\n{traceback.format_exc()}"
self.error_occurred.emit(error_msg)
finally:
# 确保无论发生什么都会释放锁
analysis_mutex.unlock()
# 删除临时压缩文件
if compressed_path != self.image_path and os.path.exists(compressed_path):
try:
os.remove(compressed_path)
except:
pass
def compress_image(self, image_path):
"""压缩图片以减小内存占用"""
try:
# 检查文件大小
file_size = os.path.getsize(image_path) / (1024 * 1024) # MB
if file_size < 1: # 小于1MB不需要压缩
return image_path
# 打开图片
img = Image.open(image_path)
# 如果图片尺寸过大,调整尺寸
if max(img.size) > MAX_COMPRESSED_SIZE:
img.thumbnail((MAX_COMPRESSED_SIZE, MAX_COMPRESSED_SIZE), Image.LANCZOS)
# 创建临时文件路径
temp_path = f"temp_compressed_{os.path.basename(image_path)}"
# 保存压缩后的图片
if image_path.lower().endswith('.png'):
img.save(temp_path, optimize=True, quality=85, format='PNG')
else:
img.save(temp_path, optimize=True, quality=85)
return temp_path
except Exception as e:
print(f"图片压缩失败: {str(e)}")
return image_path
def stop(self):
self._is_running = False
class ExportThread(QThread):
export_finished = pyqtSignal(str, bool)
def __init__(self, content, file_path, format_type, parent=None):
super().__init__(parent)
self.content = content
self.file_path = file_path
self.format_type = format_type
def run(self):
try:
if self.format_type == "html":
with open(self.file_path, "w", encoding="utf-8") as f:
f.write(self.content)
elif self.format_type == "txt":
with open(self.file_path, "w", encoding="utf-8") as f:
f.write(self.content)
elif self.format_type == "pdf":
doc = QTextDocument()
doc.setHtml(self.content)
writer = QTextDocumentWriter(self.file_path)
writer.write(doc)
self.export_finished.emit(self.file_path, True)
except Exception as e:
self.export_finished.emit(str(e), False)
class MultiModalApp(QMainWindow):
def __init__(self):
super().__init__()
self.image_paths = []
self.current_image_index = 0
self.history = []
self.active_threads = 0
self.settings = QSettings(SETTINGS_FILE, QSettings.IniFormat)
self.initUI()
self.load_settings()
self.setWindowTitle("增强版多模态大模型图像解读系统")
self.setGeometry(100, 100, 1920, 1000)
# 初始化系统托盘
self.init_tray_icon()
# 创建资源监控定时器
self.resource_timer = QTimer(self)
self.resource_timer.timeout.connect(self.monitor_resources)
self.resource_timer.start(5000) # 每5秒监控一次
QTimer.singleShot(500, self.load_models)
def initUI(self):
# 创建暗色主题样式表
self.setStyleSheet("""
/* 主窗口样式 */
QMainWindow {
background-color: #0a192f;
}
/* 分组框样式 */
QGroupBox {
border: 2px solid #64ffda;
border-radius: 10px;
margin-top: 1ex;
color: #ccd6f6;
font-weight: bold;
}
/* 标签样式 */
QLabel {
color: #ccd6f6;
}
/* 按钮样式 */
QPushButton {
background-color: #112240;
color: #64ffda;
border: 1px solid #64ffda;
border-radius: 5px;
padding: 5px 10px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #0d1b30;
color: #4a8f7c;
border: 1px solid #4a8f7c;
}
/* 文本框样式 */
QTextEdit {
background-color: #0a192f;
color: #a8b2d1;
border: 1px solid #64ffda;
border-radius: 5px;
padding: 5px;
font-size: 12pt;
}
/* 选项卡样式 */
QTabWidget::pane {
border: 1px solid #64ffda;
border-radius: 5px;
background: #0a192f;
}
QTabBar::tab {
background: #112240;
color: #ccd6f6;
padding: 8px;
border: 1px solid #64ffda;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background: #233554;
color: #64ffda;
}
/* 列表样式 */
QListWidget {
background-color: #0a192f;
color: #a8b2d1;
border: 1px solid #64ffda;
border-radius: 5px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #233554;
color: #64ffda;
}
/* 进度条样式 */
QProgressBar {
border: 1px solid #64ffda;
border-radius: 5px;
text-align: center;
background-color: #0a192f;
color: #64ffda;
}
QProgressBar::chunk {
background-color: #64ffda;
width: 10px;
}
/* 状态标签样式 */
#statusLabel {
color: #64ffda;
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
background-color: #112240;
}
""")
# 设置主窗口布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 标题区域
title_layout = QHBoxLayout()
self.title_label = QLabel("增强版多模态大模型图像解读系统")
self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;")
title_layout.addWidget(self.title_label)
# 添加主题切换按钮
self.theme_button = QPushButton("切换主题")
self.theme_button.clicked.connect(self.toggle_theme)
title_layout.addWidget(self.theme_button)
main_layout.addLayout(title_layout)
# 主内容区域
splitter = QSplitter(Qt.Horizontal)
# 左侧控制面板
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(5, 5, 5, 5)
# 图片预览区域(改为选项卡形式)
self.image_tabs = QTabWidget()
self.image_tabs.setTabsClosable(True)
self.image_tabs.tabCloseRequested.connect(self.close_image_tab)
left_layout.addWidget(self.image_tabs, 3)
# 控制面板区域
control_tabs = QTabWidget()
# 模型控制选项卡
model_tab = QWidget()
self.setup_model_tab(model_tab)
control_tabs.addTab(model_tab, "模型设置")
# 参数控制选项卡
param_tab = QWidget()
self.setup_param_tab(param_tab)
control_tabs.addTab(param_tab, "参数设置")
# 预设控制选项卡
preset_tab = QWidget()
self.setup_preset_tab(preset_tab)
control_tabs.addTab(preset_tab, "预设管理")
left_layout.addWidget(control_tabs, 2)
# 右侧结果面板
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(5, 5, 5, 5)
# 结果展示区域
result_tabs = QTabWidget()
# 分析结果选项卡
self.result_tab = QWidget()
self.setup_result_tab(self.result_tab)
result_tabs.addTab(self.result_tab, "分析结果")
# 历史记录选项卡
self.history_tab = QWidget()
self.setup_history_tab(self.history_tab)
result_tabs.addTab(self.history_tab, "历史记录")
right_layout.addWidget(result_tabs)
# 添加面板到分割器
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([800, 1100])
main_layout.addWidget(splitter)
# 状态栏
self.status_bar = self.statusBar()
self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;")
# 资源监控标签
self.resource_label = QLabel()
self.resource_label.setObjectName("statusLabel")
self.status_bar.addPermanentWidget(self.resource_label)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setFixedWidth(200)
self.status_bar.addPermanentWidget(self.progress_bar)
# 活动线程标签
self.thread_label = QLabel("线程: 0")
self.thread_label.setObjectName("statusLabel")
self.status_bar.addPermanentWidget(self.thread_label)
self.status_bar.showMessage("系统已就绪")
def setup_model_tab(self, tab):
layout = QVBoxLayout(tab)
layout.setContentsMargins(5, 5, 5, 5)
# 模型选择区域
model_group = QGroupBox("模型选择")
model_layout = QVBoxLayout()
self.model_list = QListWidget()
self.model_list.setMaximumHeight(150)
self.model_list.addItem("正在加载模型...")
self.refresh_models_button = QPushButton("刷新模型列表")
self.refresh_models_button.clicked.connect(self.load_models)
model_layout.addWidget(self.model_list)
model_layout.addWidget(self.refresh_models_button)
model_group.setLayout(model_layout)
layout.addWidget(model_group)
# 图片操作区域
image_group = QGroupBox("图片操作")
image_layout = QVBoxLayout()
self.load_button = QPushButton("加载图片")
self.load_button.clicked.connect(self.load_image)
self.load_multiple_button = QPushButton("批量加载图片")
self.load_multiple_button.clicked.connect(self.load_multiple_images)
self.clear_images_button = QPushButton("清除所有图片")
self.clear_images_button.clicked.connect(self.clear_all_images)
image_layout.addWidget(self.load_button)
image_layout.addWidget(self.load_multiple_button)
image_layout.addWidget(self.clear_images_button)
image_group.setLayout(image_layout)
layout.addWidget(image_group)
# 分析控制区域
control_group = QGroupBox("分析控制")
control_layout = QVBoxLayout()
self.analyze_button = QPushButton("分析当前图片")
self.analyze_button.clicked.connect(self.analyze_image)
self.analyze_button.setEnabled(False)
self.analyze_all_button = QPushButton("批量分析所有图片")
self.analyze_all_button.clicked.connect(self.analyze_all_images)
self.analyze_all_button.setEnabled(False)
self.stop_button = QPushButton("停止分析")
self.stop_button.clicked.connect(self.stop_analysis)
self.stop_button.setEnabled(False)
control_layout.addWidget(self.analyze_button)
control_layout.addWidget(self.analyze_all_button)
control_layout.addWidget(self.stop_button)
control_group.setLayout(control_layout)
layout.addWidget(control_group)
def setup_param_tab(self, tab):
layout = QVBoxLayout(tab)
layout.setContentsMargins(5, 5, 5, 5)
# 温度控制
temp_group = QGroupBox("温度控制")
temp_layout = QHBoxLayout()
self.temp_slider = QSlider(Qt.Horizontal)
self.temp_slider.setRange(0, 100)
self.temp_slider.setValue(50)
self.temp_value = QDoubleSpinBox()
self.temp_value.setRange(0.0, 1.0)
self.temp_value.setSingleStep(0.1)
self.temp_value.setValue(0.5)
self.temp_value.setDecimals(1)
self.temp_slider.valueChanged.connect(lambda val: self.temp_value.setValue(val / 100))
self.temp_value.valueChanged.connect(lambda val: self.temp_slider.setValue(int(val * 100)))
temp_layout.addWidget(self.temp_slider)
temp_layout.addWidget(self.temp_value)
temp_group.setLayout(temp_layout)
layout.addWidget(temp_group)
# Token控制
token_group = QGroupBox("Token控制")
token_layout = QHBoxLayout()
self.token_spin = QDoubleSpinBox()
self.token_spin.setRange(100, 5000)
self.token_spin.setValue(1000)
self.token_spin.setSingleStep(100)
token_layout.addWidget(QLabel("最大Token:"))
token_layout.addWidget(self.token_spin)
token_group.setLayout(token_layout)
layout.addWidget(token_group)
# 提示词区域
prompt_group = QGroupBox("提示词设置")
prompt_layout = QVBoxLayout()
self.prompt_edit = QTextEdit()
self.prompt_edit.setPlainText(
"请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。")
self.prompt_edit.setMinimumHeight(150)
prompt_layout.addWidget(self.prompt_edit)
prompt_group.setLayout(prompt_layout)
layout.addWidget(prompt_group)
def setup_preset_tab(self, tab):
layout = QVBoxLayout(tab)
layout.setContentsMargins(5, 5, 5, 5)
# 预设管理
preset_group = QGroupBox("预设管理")
preset_layout = QVBoxLayout()
self.preset_combo = QComboBox()
self.preset_combo.addItems(["默认预设", "详细描述", "创意写作", "技术分析"])
button_layout = QHBoxLayout()
self.load_preset_button = QPushButton("加载预设")
self.load_preset_button.clicked.connect(self.load_preset)
self.save_preset_button = QPushButton("保存预设")
self.save_preset_button.clicked.connect(self.save_preset)
self.delete_preset_button = QPushButton("删除预设")
self.delete_preset_button.clicked.connect(self.delete_preset)
button_layout.addWidget(self.load_preset_button)
button_layout.addWidget(self.save_preset_button)
button_layout.addWidget(self.delete_preset_button)
preset_layout.addWidget(self.preset_combo)
preset_layout.addLayout(button_layout)
preset_group.setLayout(preset_layout)
layout.addWidget(preset_group)
# 自动保存设置
auto_save_group = QGroupBox("自动保存设置")
auto_save_layout = QVBoxLayout()
self.auto_save_check = QCheckBox("自动保存分析结果")
self.auto_save_check.setChecked(True)
self.auto_save_path_button = QPushButton("选择保存路径")
self.auto_save_path_button.clicked.connect(self.select_auto_save_path)
self.auto_save_path_label = QLabel("默认保存位置: 程序目录/results")
self.auto_save_path_label.setWordWrap(True)
auto_save_layout.addWidget(self.auto_save_check)
auto_save_layout.addWidget(self.auto_save_path_button)
auto_save_layout.addWidget(self.auto_save_path_label)
auto_save_group.setLayout(auto_save_layout)
layout.addWidget(auto_save_group)
def setup_result_tab(self, tab):
layout = QVBoxLayout(tab)
layout.setContentsMargins(5, 5, 5, 5)
# 结果展示区域
result_group = QGroupBox("分析结果")
result_layout = QVBoxLayout()
self.result_edit = QTextEdit()
self.result_edit.setReadOnly(True)
# 结果操作按钮
button_layout = QHBoxLayout()
self.save_result_button = QPushButton("保存结果")
self.save_result_button.clicked.connect(self.save_result)
self.copy_result_button = QPushButton("复制结果")
self.copy_result_button.clicked.connect(self.copy_result)
self.clear_result_button = QPushButton("清除结果")
self.clear_result_button.clicked.connect(self.clear_results)
button_layout.addWidget(self.save_result_button)
button_layout.addWidget(self.copy_result_button)
button_layout.addWidget(self.clear_result_button)
result_layout.addWidget(self.result_edit)
result_layout.addLayout(button_layout)
result_group.setLayout(result_layout)
layout.addWidget(result_group)
def setup_history_tab(self, tab):
layout = QVBoxLayout(tab)
layout.setContentsMargins(5, 5, 5, 5)
# 历史记录区域
history_group = QGroupBox("历史记录")
history_layout = QVBoxLayout()
self.history_list = QListWidget()
self.history_list.itemDoubleClicked.connect(self.load_history_item)
# 历史操作按钮
button_layout = QHBoxLayout()
self.load_history_button = QPushButton("加载历史")
self.load_history_button.clicked.connect(self.load_history)
self.clear_history_button = QPushButton("清除历史")
self.clear_history_button.clicked.connect(self.clear_history)
button_layout.addWidget(self.load_history_button)
button_layout.addWidget(self.clear_history_button)
history_layout.addWidget(self.history_list)
history_layout.addLayout(button_layout)
history_group.setLayout(history_layout)
layout.addWidget(history_group)
def init_tray_icon(self):
"""初始化系统托盘图标"""
self.tray_icon = QSystemTrayIcon(self)
# 使用系统默认图标
icon = self.style().standardIcon(QStyle.SP_ComputerIcon)
self.tray_icon.setIcon(icon)
tray_menu = QMenu()
show_action = QAction("显示窗口", self)
show_action.triggered.connect(self.show_normal)
tray_menu.addAction(show_action)
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.close)
tray_menu.addAction(exit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.show()
# 托盘图标点击事件
self.tray_icon.activated.connect(self.tray_icon_clicked)
def show_normal(self):
"""从托盘恢复窗口显示"""
self.showNormal()
self.activateWindow()
def tray_icon_clicked(self, reason):
"""处理托盘图标点击事件"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_normal()
def toggle_theme(self):
"""切换主题"""
if self.theme_button.text() == "切换主题":
# 切换到浅色主题
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
border: 2px solid #2c3e50;
border-radius: 10px;
margin-top: 1ex;
color: #2c3e50;
font-weight: bold;
}
QLabel {
color: #2c3e50;
}
QPushButton {
background-color: #ecf0f1;
color: #2c3e50;
border: 1px solid #bdc3c7;
border-radius: 5px;
padding: 5px 10px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #d5dbdb;
color: #7f8c8d;
border: 1px solid #bdc3c7;
}
QTextEdit {
background-color: #ffffff;
color: #2c3e50;
border: 1px solid #bdc3c7;
border-radius: 5px;
padding: 5px;
font-size: 12pt;
}
QListWidget {
background-color: #ffffff;
color: #2c3e50;
border: 1px solid #bdc3c7;
border-radius: 5px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #3498db;
color: #ffffff;
}
QProgressBar {
border: 1px solid #2c3e50;
border-radius: 5px;
text-align: center;
background-color: #ecf0f1;
color: #2c3e50;
}
QProgressBar::chunk {
background-color: #3498db;
width: 10px;
}
#statusLabel {
color: #2c3e50;
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
background-color: #ecf0f1;
}
""")
self.theme_button.setText("切换回暗色")
self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #2c3e50;")
self.status_bar.setStyleSheet("background-color: #ecf0f1; color: #2c3e50;")
else:
# 切换回暗色主题
self.setStyleSheet("""
QMainWindow {
background-color: #0a192f;
}
QGroupBox {
border: 2px solid #64ffda;
border-radius: 10px;
margin-top: 1ex;
color: #ccd6f6;
font-weight: bold;
}
QLabel {
color: #ccd6f6;
}
QPushButton {
background-color: #112240;
color: #64ffda;
border: 1px solid #64ffda;
border-radius: 5px;
padding: 5px 10px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #0d1b30;
color: #4a8f7c;
border: 1px solid #4a8f7c;
}
QTextEdit {
background-color: #0a192f;
color: #a8b2d1;
border: 1px solid #64ffda;
border-radius: 5px;
padding: 5px;
font-size: 12pt;
}
QListWidget {
background-color: #0a192f;
color: #a8b2d1;
border: 1px solid #64ffda;
border-radius: 5px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #233554;
color: #64ffda;
}
QProgressBar {
border: 1px solid #64ffda;
border-radius: 5px;
text-align: center;
background-color: #0a192f;
color: #64ffda;
}
QProgressBar::chunk {
background-color: #64ffda;
width: 10px;
}
#statusLabel {
color: #64ffda;
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
background-color: #112240;
}
""")
self.theme_button.setText("切换主题")
self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;")
self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;")
def load_settings(self):
"""加载程序设置"""
self.settings.beginGroup("MainWindow")
self.restoreGeometry(self.settings.value("geometry", self.saveGeometry()))
self.restoreState(self.settings.value("windowState", self.saveState()))
self.settings.endGroup()
# 加载历史记录
self.load_history()
# 加载自动保存路径
auto_save_path = self.settings.value("AutoSave/path", "results")
self.auto_save_path_label.setText(f"保存位置: {auto_save_path}")
def save_settings(self):
"""保存程序设置"""
self.settings.beginGroup("MainWindow")
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("windowState", self.saveState())
self.settings.endGroup()
# 保存历史记录
self.save_history()
def load_models(self):
"""加载可用模型列表"""
self.model_list.clear()
self.model_list.addItem("正在加载模型...")
self.model_loader_thread = ModelLoaderThread()
self.model_loader_thread.models_loaded.connect(self.update_model_list)
self.model_loader_thread.error_occurred.connect(self.handle_model_load_error)
self.model_loader_thread.start()
def update_model_list(self, models):
"""更新模型列表"""
self.model_list.clear()
if not models:
self.model_list.addItem("没有找到可用模型")
return
for model in models:
# 显示完整的模型名称(包含冒号)
item = QListWidgetItem(f"● {model}")
item.setData(Qt.UserRole, model)
self.model_list.addItem(item)
# 默认选择第一个模型
if models:
self.model_list.setCurrentRow(0)
def handle_model_load_error(self, error_msg):
"""处理模型加载错误"""
self.model_list.clear()
self.model_list.addItem(error_msg)
self.status_bar.showMessage(error_msg)
def load_image(self):
"""加载单张图片"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择图片", "",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)"
)
if file_path:
self.add_image_tab(file_path)
def load_multiple_images(self):
"""批量加载多张图片"""
file_paths, _ = QFileDialog.getOpenFileNames(
self, "选择多张图片", "",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)"
)
if file_paths:
for file_path in file_paths:
self.add_image_tab(file_path)
def add_image_tab(self, file_path):
"""添加图片选项卡"""
try:
pixmap = QPixmap(file_path)
if pixmap.isNull():
raise Exception("无法加载图片文件,可能格式不支持或文件已损坏")
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
image_label = QLabel()
image_label.setPixmap(pixmap.scaled(
800, 600,
Qt.KeepAspectRatio, Qt.SmoothTransformation
))
image_label.setAlignment(Qt.AlignCenter)
scroll_area.setWidget(image_label)
tab_index = self.image_tabs.addTab(
scroll_area, os.path.basename(file_path)
)
self.image_tabs.setCurrentIndex(tab_index)
self.image_paths.append(file_path)
self.analyze_button.setEnabled(True)
self.analyze_all_button.setEnabled(True)
self.status_bar.showMessage(f"已加载图片: {os.path.basename(file_path)}")
# 更新当前图片索引
self.current_image_index = tab_index
except Exception as e:
self.status_bar.showMessage(f"错误: {str(e)}")
self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}")
def close_image_tab(self, index):
"""关闭图片选项卡"""
if index < len(self.image_paths):
self.image_paths.pop(index)
self.image_tabs.removeTab(index)
if not self.image_paths:
self.analyze_button.setEnabled(False)
self.analyze_all_button.setEnabled(False)
def clear_all_images(self):
"""清除所有图片"""
self.image_tabs.clear()
self.image_paths.clear()
self.analyze_button.setEnabled(False)
self.analyze_all_button.setEnabled(False)
self.status_bar.showMessage("已清除所有图片")
def analyze_image(self):
"""分析当前图片"""
current_index = self.image_tabs.currentIndex()
if current_index < 0 or current_index >= len(self.image_paths):
self.status_bar.showMessage("错误: 没有可分析的图片")
return
self.current_image_index = current_index
self._analyze_image(self.image_paths[current_index])
def analyze_all_images(self):
"""批量分析所有图片"""
if not self.image_paths:
self.status_bar.showMessage("错误: 没有可分析的图片")
return
# 保存当前索引
saved_index = self.current_image_index
# 逐个分析图片
for i, image_path in enumerate(self.image_paths):
# 检查活动线程数
while self.active_threads >= MAX_THREADS:
QCoreApplication.processEvents() # 处理事件循环
self.current_image_index = i
self.image_tabs.setCurrentIndex(i)
self._analyze_image(image_path)
# 等待分析完成
while hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning():
QApplication.processEvents()
# 恢复原始索引
self.current_image_index = saved_index
self.image_tabs.setCurrentIndex(saved_index)
def _analyze_image(self, image_path):
"""实际执行图片分析的内部方法"""
selected_items = self.model_list.selectedItems()
if not selected_items:
self.status_bar.showMessage("错误: 请选择模型")
return
# 获取完整的模型名称(包含冒号)
model_name = selected_items[0].data(Qt.UserRole)
self.result_edit.clear()
self.progress_bar.setValue(0)
self.progress_bar.setFormat("正在分析图片...")
self.set_buttons_enabled(False)
self.stop_button.setEnabled(True)
temperature = self.temp_value.value()
max_tokens = int(self.token_spin.value())
prompt = self.prompt_edit.toPlainText().strip()
# 确保之前的线程已停止
if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning():
self.analysis_thread.stop()
self.analysis_thread.wait(2000) # 等待线程结束
try:
self.analysis_thread = ImageAnalysisThread(
model_name, image_path, temperature, max_tokens, prompt
)
self.analysis_thread.analysis_complete.connect(self.handle_analysis_result)
self.analysis_thread.progress_updated.connect(self.update_progress)
self.analysis_thread.error_occurred.connect(self.handle_analysis_error)
self.analysis_thread.stream_data.connect(self.analysis_stream_data)
self.analysis_thread.finished.connect(self.analysis_finished)
self.analysis_thread.start()
# 更新活动线程计数
self.active_threads += 1
self.thread_label.setText(f"线程: {self.active_threads}")
except Exception as e:
error_msg = f"启动分析线程失败: {str(e)}\n\n{traceback.format_exc()}"
self.handle_analysis_error(error_msg)
def analysis_stream_data(self, chunk):
"""处理流式数据"""
cursor = self.result_edit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(chunk)
self.result_edit.setTextCursor(cursor)
self.result_edit.ensureCursorVisible()
def handle_analysis_result(self, result, image_path):
"""处理分析结果"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
selected_items = self.model_list.selectedItems()
model_name = selected_items[0].text()[2:] if selected_items else "未知模型"
# 获取当前图片文件名
image_name = os.path.basename(image_path)
# 格式化结果
formatted_result = self.format_result(result)
result_html = f"""
<div style='color:#64ffda; font-size:14pt; font-weight:bold; margin-bottom:10px;'>
图片分析结果: {image_name}
</div>
<div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'>{formatted_result}</div>
<div style='margin-top: 20px; color: #8892b0; font-size: 10pt; border-top: 1px solid #233554; padding-top: 10px;'>
模型: <span style='color: #64ffda;'>{model_name}</span> |
时间: <span style='color: #64ffda;'>{timestamp}</span>
</div>
"""
self.result_edit.setHtml(result_html)
self.status_bar.showMessage(f"图片分析完成: {image_name}")
self.progress_bar.setValue(100)
self.progress_bar.setFormat("分析完成")
# 添加到历史记录
self.add_to_history(image_name, model_name, timestamp, result_html)
# 自动保存结果
if self.auto_save_check.isChecked():
self.auto_save_result(image_name, result_html)
def format_result(self, result):
"""格式化分析结果为HTML"""
# 将文本转换为段落
paragraphs = result.split("\n\n")
formatted_paragraphs = []
for para in paragraphs:
if para.strip():
# 替换缩进空格为HTML空格
formatted_para = para.replace(" ", " ")
formatted_paragraphs.append(f"<p style='text-indent: 2em;'>{formatted_para}</p>")
return "\n".join(formatted_paragraphs)
def add_to_history(self, image_name, model_name, timestamp, content):
"""添加到历史记录"""
history_item = {
"image": image_name,
"model": model_name,
"time": timestamp,
"content": content
}
self.history.insert(0, history_item)
self.update_history_list()
def update_history_list(self):
"""更新历史记录列表"""
self.history_list.clear()
for item in self.history[:50]: # 最多显示50条历史记录
list_item = QListWidgetItem(f"{item['image']} - {item['time']}")
list_item.setData(Qt.UserRole, item)
self.history_list.addItem(list_item)
def load_history_item(self, item):
"""加载历史记录项"""
history_data = item.data(Qt.UserRole)
self.result_edit.setHtml(history_data["content"])
def load_history(self):
"""从文件加载历史记录"""
try:
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
self.history = json.load(f)
self.update_history_list()
self.status_bar.showMessage(f"已加载 {len(self.history)} 条历史记录")
except Exception as e:
self.status_bar.showMessage(f"加载历史记录失败: {str(e)}")
def save_history(self):
"""保存历史记录到文件"""
try:
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(self.history, f, ensure_ascii=False, indent=2)
except Exception as e:
self.status_bar.showMessage(f"保存历史记录失败: {str(e)}")
def clear_history(self):
"""清除历史记录"""
reply = QMessageBox.question(
self, "确认清除",
"确定要清除所有历史记录吗? 此操作不可撤销!",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.history.clear()
self.history_list.clear()
self.status_bar.showMessage("已清除所有历史记录")
def auto_save_result(self, image_name, content):
"""自动保存结果"""
try:
save_dir = self.settings.value("AutoSave/path", "results")
if not os.path.exists(save_dir):
os.makedirs(save_dir)
# 生成文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
base_name = os.path.splitext(image_name)[0]
file_name = f"{base_name}_{timestamp}.html"
file_path = os.path.join(save_dir, file_name)
# 保存文件
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
self.status_bar.showMessage(f"结果已自动保存到: {file_path}")
except Exception as e:
self.status_bar.showMessage(f"自动保存失败: {str(e)}")
def select_auto_save_path(self):
"""选择自动保存路径"""
save_dir = QFileDialog.getExistingDirectory(
self, "选择自动保存目录",
self.settings.value("AutoSave/path", "results")
)
if save_dir:
self.settings.setValue("AutoSave/path", save_dir)
self.auto_save_path_label.setText(f"保存位置: {save_dir}")
def save_result(self):
"""保存结果到文件"""
if not self.result_edit.toPlainText():
self.status_bar.showMessage("错误: 没有可保存的内容")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存结果", "",
"HTML文件 (*.html);;文本文件 (*.txt);;PDF文件 (*.pdf)"
)
if file_path:
format_type = "html"
if file_path.endswith(".txt"):
format_type = "txt"
elif file_path.endswith(".pdf"):
format_type = "pdf"
content = self.result_edit.toHtml() if format_type != "txt" else self.result_edit.toPlainText()
self.export_thread = ExportThread(content, file_path, format_type)
self.export_thread.export_finished.connect(self.handle_export_finished)
self.export_thread.start()
self.status_bar.showMessage("正在导出结果...")
def handle_export_finished(self, message, success):
"""处理导出完成"""
if success:
self.status_bar.showMessage(f"结果已保存到: {message}")
if message.endswith(".html"):
webbrowser.open(message)
else:
self.status_bar.showMessage(f"导出失败: {message}")
self.show_error_dialog("导出错误", f"无法保存结果:\n{message}")
def copy_result(self):
"""复制结果到剪贴板"""
self.result_edit.selectAll()
self.result_edit.copy()
self.status_bar.showMessage("结果已复制到剪贴板")
def clear_results(self):
"""清除结果"""
self.result_edit.clear()
self.status_bar.showMessage("已清除结果")
def load_preset(self):
"""加载预设"""
preset_name = self.preset_combo.currentText()
if preset_name == "默认预设":
self.temp_value.setValue(0.5)
self.token_spin.setValue(1000)
self.prompt_edit.setPlainText(
"请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。")
elif preset_name == "详细描述":
self.temp_value.setValue(0.3)
self.token_spin.setValue(1500)
self.prompt_edit.setPlainText(
"请用中文详细描述这张图片中的每一个细节,包括但不限于场景、人物、物体、颜色、空间关系等。要求描述系统全面,层次分明,每段描述一个方面。")
elif preset_name == "创意写作":
self.temp_value.setValue(0.7)
self.token_spin.setValue(2000)
self.prompt_edit.setPlainText(
"请根据这张图片创作一个富有想象力的故事或诗歌。要求内容生动有趣,语言优美,可以适当发挥想象力,但不要偏离图片内容太远。")
elif preset_name == "技术分析":
self.temp_value.setValue(0.2)
self.token_spin.setValue(1200)
self.prompt_edit.setPlainText(
"请从技术角度分析这张图片,包括但不限于构图、色彩、光线、透视等专业要素。要求分析专业准确,使用适当的专业术语。")
self.status_bar.showMessage(f"已加载预设: {preset_name}")
def save_preset(self):
"""保存当前设置为预设"""
preset_name, ok = QInputDialog.getText(
self, "保存预设", "请输入预设名称:",
QLineEdit.Normal, self.preset_combo.currentText()
)
if ok and preset_name:
# 检查是否已存在
index = self.preset_combo.findText(preset_name)
if index == -1:
self.preset_combo.addItem(preset_name)
self.preset_combo.setCurrentText(preset_name)
self.status_bar.showMessage(f"预设 '{preset_name}' 已保存")
def delete_preset(self):
"""删除预设"""
preset_name = self.preset_combo.currentText()
if preset_name in ["默认预设", "详细描述", "创意写作", "技术分析"]:
QMessageBox.warning(self, "警告", "系统预设不能被删除")
return
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除预设 '{preset_name}' 吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
index = self.preset_combo.currentIndex()
self.preset_combo.removeItem(index)
self.status_bar.showMessage(f"预设 '{preset_name}' 已删除")
def stop_analysis(self):
"""停止分析"""
if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning():
self.analysis_thread.stop()
self.status_bar.showMessage("分析已停止")
self.set_buttons_enabled(True)
self.stop_button.setEnabled(False)
def analysis_finished(self):
"""分析完成后的清理工作"""
self.set_buttons_enabled(True)
self.stop_button.setEnabled(False)
# 更新活动线程计数
self.active_threads -= 1
if self.active_threads < 0:
self.active_threads = 0
self.thread_label.setText(f"线程: {self.active_threads}")
def handle_analysis_error(self, error_msg):
"""处理分析错误"""
# 显示详细的错误信息
self.status_bar.showMessage(f"错误: {error_msg.splitlines()[0]}")
self.progress_bar.setFormat("分析失败")
self.set_buttons_enabled(True)
self.stop_button.setEnabled(False)
# 在结果区域显示完整错误信息
error_html = f"""
<div style='color:#ff6b6b; font-size:14pt; font-weight:bold; margin-bottom:10px;'>
分析过程中发生错误
</div>
<div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'>
{error_msg.replace('\n', '<br>')}
</div>
"""
self.result_edit.setHtml(error_html)
# 记录错误日志
self.log_error(error_msg)
# 更新活动线程计数
self.active_threads -= 1
if self.active_threads < 0:
self.active_threads = 0
self.thread_label.setText(f"线程: {self.active_threads}")
def log_error(self, error_msg):
"""记录错误到日志文件"""
try:
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = os.path.join(log_dir, "error_log.txt")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"\n\n[{timestamp}] 发生错误:\n")
f.write(error_msg)
f.write("\n" + "-" * 80)
except Exception as e:
print(f"无法写入错误日志: {str(e)}")
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.load_button.setEnabled(enabled)
self.load_multiple_button.setEnabled(enabled)
self.clear_images_button.setEnabled(enabled)
self.analyze_button.setEnabled(enabled and len(self.image_paths) > 0)
self.analyze_all_button.setEnabled(enabled and len(self.image_paths) > 0)
self.model_list.setEnabled(enabled)
self.refresh_models_button.setEnabled(enabled)
self.preset_combo.setEnabled(enabled)
self.load_preset_button.setEnabled(enabled)
self.save_preset_button.setEnabled(enabled)
self.delete_preset_button.setEnabled(enabled)
self.auto_save_check.setEnabled(enabled)
self.auto_save_path_button.setEnabled(enabled)
self.save_result_button.setEnabled(enabled)
self.copy_result_button.setEnabled(enabled)
self.clear_result_button.setEnabled(enabled)
self.load_history_button.setEnabled(enabled)
self.clear_history_button.setEnabled(enabled)
def show_error_dialog(self, title, message):
"""显示错误对话框"""
QMessageBox.critical(self, title, message)
def monitor_resources(self):
"""监控系统资源使用情况"""
try:
cpu_percent = psutil.cpu_percent()
memory = psutil.virtual_memory()
self.resource_label.setText(
f"CPU: {cpu_percent}% | 内存: {memory.percent}%"
)
except:
pass
def closeEvent(self, event):
"""关闭窗口事件"""
# 停止所有运行中的线程
if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning():
self.analysis_thread.stop()
if hasattr(self, 'model_loader_thread') and self.model_loader_thread.isRunning():
self.model_loader_thread.quit()
self.model_loader_thread.wait(2000)
# 停止资源监控定时器
self.resource_timer.stop()
# 保存设置
# 接受关闭事件
event.accept()
def handle_exception(exc_type, exc_value, exc_traceback):
"""全局异常处理函数"""
error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
print(f"全局异常捕获:\n{error_msg}")
# 尝试显示错误对话框
try:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("程序发生未处理异常")
msg.setInformativeText(f"{exc_type.__name__}: {exc_value}")
msg.setWindowTitle("致命错误")
msg.setDetailedText(error_msg)
msg.exec_()
except:
pass
# 退出程序
sys.exit(1)
if __name__ == "__main__":
# 设置全局异常处理
sys.excepthook = handle_exception
app = QApplication(sys.argv)
window = MultiModalApp()
window.show()
sys.exit(app.exec_())
最新发布