Update Progress(Time o…

本文解决了在使用MyEclipse进行应用部署或redeploy操作时出现的UpdateProgress(Timeoferror)问题。通过删除特定配置文件即可修复此问题。

报错信息:

当使用MyEclipse部署应用或执行redeploy时,会狂报Update Progress(Time of error)问题Update Progress(Time of error)问题,detail中没有更详细的信息。

解决方法:

D:\MyEclipse 6.5 \eclipse\configuration\org.eclipse.update下面把last.config.stamp删除掉,就ok了。(重启MyEclipse会自动重新生成个该文件)

# 请缩小结果框中文字的字号,扩大该框的面积,程序界面可扩大一些;给一个计时器放在显著位置,每次开始图片分析时即开时计时,结果出完后,停止计时 import sys import os import base64 import json import requests import time from datetime import datetime from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QFrame, QSplitter, QMessageBox, QListWidget, QListWidgetItem) from PyQt5.QtGui import QPixmap, QImage, QFont, QPalette, QColor, QIcon, QPainter, QLinearGradient, QBrush from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QPoint, QTimer # 配置Ollama API设置 - 使用默认端口11434 OLLAMA_HOST = "http://localhost:11434" class ModelLoaderThread(QThread): models_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) def run(self): try: # 先检查Ollama服务是否可用 health_url = f"{OLLAMA_HOST}" try: response = requests.get(health_url, timeout=10) if response.status_code != 200: raise Exception(f"Ollama服务不可用 (HTTP {response.status_code})") except Exception as e: self.error_occurred.emit(f"Ollama服务检查失败: {str(e)}") return # 请求Ollama获取可用模型列表 response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=15) if response.status_code == 200: models_data = response.json() model_list = [model["name"] for model in models_data.get("models", [])] if model_list: self.models_loaded.emit(model_list) else: self.error_occurred.emit("Ollama服务返回了空模型列表") else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("连接Ollama服务超时。请检查网络连接或增加超时设置。") except Exception as e: self.error_occurred.emit(f"加载模型时发生错误: {str(e)}") class StreamAnalysisThread(QThread): chunk_received = pyqtSignal(str) # 新增:接收流式数据块的信号 analysis_complete = pyqtSignal(str) progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) def __init__(self, model, image_path, temperature, max_tokens, prompt, parent=None): super().__init__(parent) self.model = model self.image_path = image_path self.temperature = temperature self.max_tokens = max_tokens self.prompt = prompt self._is_running = True def stop(self): self._is_running = False def run(self): try: # 检查图片文件是否存在 if not os.path.exists(self.image_path): self.error_occurred.emit(f"图片文件不存在: {self.image_path}") return # 读取图片并转换为base64 self.progress_updated.emit(10) with open(self.image_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode("utf-8") # 构造API请求数据 - 启用流式传输 data = { "model": self.model, "prompt": self.prompt, "images": [base64_image], "options": { "temperature": self.temperature, "num_predict": self.max_tokens }, "stream": True # 启用流式传输 } # 发送流式请求到Ollama API self.progress_updated.emit(30) response = requests.post( f"{OLLAMA_HOST}/api/generate", json=data, headers={"Content-Type": "application/json"}, stream=True, # 启用流式响应 timeout=180 ) if response.status_code == 200: full_response = "" for line in response.iter_lines(): if not self._is_running: break if line: try: # 解析JSON响应 json_response = json.loads(line.decode('utf-8')) # 检查是否包含响应内容 if 'response' in json_response: chunk = json_response['response'] full_response += chunk # 发射单个数据块 self.chunk_received.emit(chunk) # 检查是否完成 if json_response.get('done', False): break except json.JSONDecodeError: continue self.progress_updated.emit(90) self.analysis_complete.emit(full_response) else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") self.progress_updated.emit(100) except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("分析请求超时。请尝试减小图片大小或降低模型复杂度。") except Exception as e: self.error_occurred.emit(f"分析图片时发生错误: {str(e)}") class GradientLabel(QLabel): """带渐变背景的标签""" def __init__(self, text, parent=None): super().__init__(text, parent) self.setAlignment(Qt.AlignCenter) self.setMinimumHeight(60) def paintEvent(self, event): painter = QPainter(self) gradient = QLinearGradient(0, 0, self.width(), 0) gradient.setColorAt(0, QColor("#6a11cb")) gradient.setColorAt(1, QColor("#2575fc")) painter.fillRect(self.rect(), QBrush(gradient)) painter.setPen(Qt.white) painter.setFont(QFont("Microsoft YaHei UI", 18, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, self.text()) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_path = "" self.analysis_thread = None self.model_loader_thread = None self.current_response = "" # 新增:存储当前响应内容 self.initUI() self.setWindowTitle("多模态大模型图片解读系统") self.setGeometry(100, 100, 1200, 800) # 延迟加载模型,确保UI完全初始化 from PyQt5.QtCore import QTimer QTimer.singleShot(500, self.load_models) def initUI(self): # 设置主窗口样式 - 中性色调背景 self.setStyleSheet(""" QMainWindow { background-color: #f0f4f8; } QGroupBox { border: 2px solid #4a86e8; border-radius: 12px; margin-top: 20px; background-color: rgba(255, 255, 255, 0.95); color: #2c3e50; font-weight: bold; font-size: 12pt; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 8px 20px; /* 增加内边距 */ background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; border-radius: 8px; top: -0px; /* 向上移动更多 */ min-height: 35px; /* 增加高度 */ } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; font-weight: bold; font-size: 11pt; min-height: 35px; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } QPushButton:pressed { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3a76d8, stop:1 #5a01bb); } QPushButton:disabled { background-color: #b0c4de; color: #777777; } QComboBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: white; color: #2c3e50; selection-background-color: #e0f0ff; border-radius: 8px; border: 1px solid #cccccc; font-size: 11pt; } QTextEdit { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; font-size: 12pt; } QSlider::groove:horizontal { border: 1px solid #cccccc; height: 10px; background: #e0e0e0; margin: 2px 0; border-radius: 5px; } QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #6a11cb, stop:1 #4a86e8); border: 1px solid #4a76b0; width: 22px; margin: -6px 0; border-radius: 11px; } QDoubleSpinBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QProgressBar { border: 1px solid #cccccc; border-radius: 8px; text-align: center; color: #2c3e50; background-color: white; font-size: 11pt; height: 25px; } QProgressBar::chunk { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #4a86e8); border-radius: 7px; } QListWidget { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; font-size: 11pt; } QSplitter::handle { background-color: #4a86e8; width: 4px; } """) # 设置字体 app_font = QFont("Microsoft YaHei UI", 10) app_font.setBold(False) QApplication.setFont(app_font) # 创建主窗口部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # 标题 - 使用渐变背景 title_label = QLabel("多模态大模型图片解读系统") title_label.setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-size: 24pt; font-weight: bold; padding: 15px; border-radius: 12px; """) title_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(title_label) # 创建分割器 splitter = QSplitter(Qt.Horizontal) splitter.setChildrenCollapsible(False) # 左侧面板(图片和控制) left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setSpacing(15) left_layout.setContentsMargins(10, 15, 10, 10) # 图片显示区域 - 修复标题显示问题 self.image_group = QGroupBox("图片预览") self.image_group.setMinimumHeight(300) image_layout = QVBoxLayout(self.image_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) image_layout.setContentsMargins(10, 35, 10, 10) # 上边距增加10px self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(""" background-color: #ffffff; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; """) self.image_label.setText("请选择图片进行分析") self.image_label.setFont(QFont("Microsoft YaHei UI", 12)) image_layout.addWidget(self.image_label) # 控制面板 - 修复标题显示问题 control_group = QGroupBox("分析控制") control_layout = QVBoxLayout(control_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) control_layout.setContentsMargins(15, 35, 15, 15) # 上边距增加10px # 模型选择 model_layout = QHBoxLayout() model_label = QLabel("选择模型:") model_label.setFixedWidth(100) model_label.setStyleSheet("font-weight: bold;") self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") model_layout.addWidget(model_label) model_layout.addWidget(self.model_list) control_layout.addLayout(model_layout) # 温度控制 temp_layout = QHBoxLayout() temp_label = QLabel("温度:") temp_label.setFixedWidth(100) temp_label.setStyleSheet("font-weight: bold;") 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(temp_label) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) control_layout.addLayout(temp_layout) # 最大token数 token_layout = QHBoxLayout() token_label = QLabel("最大Token:") token_label.setFixedWidth(100) token_label.setStyleSheet("font-weight: bold;") self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(token_label) token_layout.addWidget(self.token_spin) control_layout.addLayout(token_layout) # 自定义提示 prompt_layout = QVBoxLayout() prompt_label = QLabel("提示词:") prompt_label.setStyleSheet("font-weight: bold;") self.prompt_edit = QTextEdit() # 设置默认提示词 self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") self.prompt_edit.setMaximumHeight(100) prompt_layout.addWidget(prompt_label) prompt_layout.addWidget(self.prompt_edit) control_layout.addLayout(prompt_layout) # 按钮区域 button_layout = QHBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.setIcon(QIcon.fromTheme("document-open")) self.load_button.clicked.connect(self.load_image) self.analyze_button = QPushButton("分析图片") self.analyze_button.setIcon(QIcon.fromTheme("system-search")) self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.clear_button = QPushButton("清除结果") self.clear_button.setIcon(QIcon.fromTheme("edit-clear")) self.clear_button.clicked.connect(self.clear_results) self.refresh_models_button = QPushButton("刷新模型") self.refresh_models_button.setIcon(QIcon.fromTheme("view-refresh")) self.refresh_models_button.clicked.connect(self.load_models) self.stop_button = QPushButton("停止分析") # 新增:停止按钮 self.stop_button.setIcon(QIcon.fromTheme("process-stop")) self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) button_layout.addWidget(self.load_button) button_layout.addWidget(self.analyze_button) button_layout.addWidget(self.stop_button) # 添加停止按钮 button_layout.addWidget(self.clear_button) button_layout.addWidget(self.refresh_models_button) control_layout.addLayout(button_layout) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) self.progress_bar.setFormat("等待操作...") control_layout.addWidget(self.progress_bar) # 右侧面板(结果) - 修复标题显示问题 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) result_group = QGroupBox("分析结果") result_layout = QVBoxLayout(result_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) result_layout.setContentsMargins(10, 35, 10, 10) # 上边距增加10px self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) self.result_edit.setStyleSheet(""" font-size: 12pt; line-height: 1.5; background-color: white; color: #2c3e50; padding: 15px; border: 1px solid #cccccc; border-radius: 8px; """) result_layout.addWidget(self.result_edit) # 添加面板到分割器 left_layout.addWidget(self.image_group) left_layout.addWidget(control_group) right_layout.addWidget(result_group) splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([400, 600]) main_layout.addWidget(splitter) # 状态栏 self.statusBar().setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-weight: bold; padding-left: 10px; """) self.statusBar().showMessage("正在初始化...") def show_error_dialog(self, title, message): """显示错误对话框""" msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle(title) msg.setText("发生错误") msg.setInformativeText(message) # 添加更多操作按钮 if "Ollama" in title: msg.addButton("打开Ollama网站", QMessageBox.ActionRole) msg.addButton("重试", QMessageBox.ActionRole) msg.addButton(QMessageBox.Ok) msg.setStyleSheet(""" QMessageBox { background-color: #f0f4f8; border: 2px solid #4a86e8; border-radius: 12px; } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; min-width: 80px; font-weight: bold; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } """) result = msg.exec_() # 处理自定义按钮点击 if result == QMessageBox.Ok: pass elif msg.clickedButton().text() == "打开Ollama网站": import webbrowser webbrowser.open("https://ollama.com/") elif msg.clickedButton().text() == "重试": self.load_models() def load_models(self): """从Ollama加载可用模型列表""" self.model_list.clear() self.model_list.addItem("正在加载模型...") self.refresh_models_button.setEnabled(False) self.statusBar().showMessage("正在从Ollama获取模型列表...") # 创建并启动模型加载线程 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.finished.connect(self.model_loader_finished) self.model_loader_thread.start() def update_model_list(self, model_list): """更新模型列表控件""" self.model_list.clear() if model_list: # 筛选多模态模型 multimodal_models = [model for model in model_list if "vision" in model.lower() or "llava" in model.lower()] # 添加多模态模型标题 title_item = QListWidgetItem("=== 多模态大模型 ===") # 使用QListWidgetItem self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) # 蓝色 title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) # 不可选择 # 添加多模态模型 for model in multimodal_models: item = QListWidgetItem(f"● {model}") # 使用QListWidgetItem item.setForeground(QColor("#2c3e50")) # 深灰色 item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) # 存储原始模型名 self.model_list.addItem(item) # 添加其他模型标题 other_models = [model for model in model_list if model not in multimodal_models] if other_models: title_item = QListWidgetItem("=== 其他模型 ===") # 使用QListWidgetItem self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) # 蓝色 title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) # 不可选择 # 添加其他模型 for model in other_models: item = QListWidgetItem(f"○ {model}") # 使用QListWidgetItem item.setForeground(QColor("#2c3e50")) # 深灰色 item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) # 存储原始模型名 self.model_list.addItem(item) self.statusBar().showMessage(f"已加载 {len(model_list)} 个模型") # 默认选择第一个多模态模型 if multimodal_models: self.model_list.setCurrentRow(1) # 第一项是标题,第二项是第一个模型 else: self.model_list.addItem("未找到可用模型") self.statusBar().showMessage("未找到可用模型") def handle_model_load_error(self, error): """处理模型加载错误""" self.model_list.clear() self.model_list.addItem("加载模型失败") self.statusBar().showMessage(error) # 显示错误对话框 self.show_error_dialog("模型加载错误", f"{error}\n\n" "可能原因:\n" f"1. Ollama服务未启动 (当前地址: {OLLAMA_HOST})\n" "2. Ollama未正确安装\n" "3. 网络连接问题\n\n" "解决方案:\n" "1. 下载并安装Ollama: https://ollama.com/download\n" "2. 启动Ollama服务\n" "3. 检查网络连接") def model_loader_finished(self): """模型加载线程完成""" self.refresh_models_button.setEnabled(True) def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: try: self.image_path = file_path pixmap = QPixmap(file_path) # 检查图片是否有效 if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") # 保存原始图片用于后续处理 self.original_pixmap = pixmap.copy() # 计算缩放尺寸 label_width = self.image_label.width() - 20 label_height = self.image_label.height() - 20 # 保持纵横比缩放 scaled_pixmap = pixmap.scaled( label_width, label_height, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) self.analyze_button.setEnabled(True) self.statusBar().showMessage(f"已加载图片: {os.path.basename(file_path)}") self.progress_bar.setFormat("图片已加载,准备分析") except Exception as e: self.statusBar().showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def analyze_image(self): if not self.image_path: self.statusBar().showMessage("错误: 请先加载图片") return # 检查模型选择是否有效 selected_items = self.model_list.selectedItems() if not selected_items: self.statusBar().showMessage("错误: 请选择模型") return selected_item = selected_items[0] if selected_item.text().startswith("==="): self.statusBar().showMessage("错误: 请选择有效的模型") return # 提取模型名称 model_name = selected_item.data(Qt.UserRole) # 从UserRole获取原始模型名 # 清空当前响应 self.current_response = "" # 初始化结果框,显示开始分析的消息 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.result_edit.setHtml(f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'> 图片分析结果 </div> <div style='color: #2c3e50; font-size: 12pt; line-height: 1.6;'> <p style='color: #4a86e8; font-weight: bold;'>正在分析图片,请稍候...</p> </div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 10pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>开始时间:</span> {timestamp} </div> """) 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() # 创建并启动流式分析线程 self.analysis_thread = StreamAnalysisThread( model_name, self.image_path, temperature, max_tokens, prompt ) # 连接信号 self.analysis_thread.chunk_received.connect(self.handle_stream_chunk) # 新增:处理流式数据 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.finished.connect(self.analysis_finished) self.analysis_thread.start() def handle_stream_chunk(self, chunk): """处理流式数据块""" # 累加当前响应 self.current_response += chunk # 格式化当前响应 formatted_result = self.format_result(self.current_response) # 获取模型信息 selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 更新结果框 result_html = f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'> 图片分析结果 </div> <div style='color: #2c3e50; font-size: 12pt; line-height: 1.6;'> {formatted_result} </div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 10pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>时间:</span> {timestamp}   <span style='color: #27ae60; font-weight: bold;'>● 流式传输中...</span> </div> """ self.result_edit.setHtml(result_html) # 自动滚动到末尾 cursor = self.result_edit.textCursor() cursor.movePosition(cursor.End) self.result_edit.setTextCursor(cursor) self.result_edit.ensureCursorVisible() def handle_analysis_result(self, result): """处理最终分析结果""" # 这里不再需要更新结果框,因为流式传输已经实时更新了 # 只需要更新状态信息 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 "未知模型" # 更新底部状态信息,去掉"流式传输中"的提示 current_html = self.result_edit.toHtml() updated_html = current_html.replace( '<span style=\'color: #27ae60; font-weight: bold;\'>● 流式传输中...</span>', '<span style=\'color: #2ecc71; font-weight: bold;\'>✓ 分析完成</span>' ) self.result_edit.setHtml(updated_html) self.statusBar().showMessage("图片分析完成") def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() self.analysis_thread.wait(2000) # 等待2秒 self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析已停止") self.statusBar().showMessage("分析已停止") def format_result(self, result): """格式化结果文本:分段落显示,确保使用汉字""" # 替换英文标点为中文标点 formatted = result.replace(".", "。").replace(",", ",").replace(":", ":") # 分割为段落 paragraphs = formatted.split("\n\n") # 如果只有一段,尝试按句号分割 if len(paragraphs) == 1: paragraphs = formatted.split("。") # 在每个句号后添加换行(除了最后一个) paragraphs = [p.strip() + ("。" if i < len(paragraphs) - 1 else "") for i, p in enumerate(paragraphs) if p.strip()] # 构建HTML段落 html_paragraphs = [] for i, p in enumerate(paragraphs): if p.strip(): # 添加标题样式到第一个段落 if i == 0: html_paragraphs.append(f"<p style='margin:15px 0; font-size:13pt; font-weight: bold;'>{p}</p>") else: html_paragraphs.append(f"<p style='margin:10px 0; text-indent: 2em;'>{p}</p>") return "".join(html_paragraphs) def handle_analysis_error(self, error): # 显示错误信息 self.result_edit.setPlainText(f"错误: {error}") self.statusBar().showMessage(f"错误: {error}") self.progress_bar.setFormat("分析失败") # 显示错误对话框 self.show_error_dialog("分析错误", error) def update_progress(self, value): self.progress_bar.setValue(value) # 更新进度条文本 if value < 30: self.progress_bar.setFormat("准备分析... %p%") elif value < 70: self.progress_bar.setFormat("发送请求到Ollama... %p%") elif value < 90: self.progress_bar.setFormat("处理响应... %p%") else: self.progress_bar.setFormat("完成分析... %p%") def analysis_finished(self): self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析完成") def set_buttons_enabled(self, enabled): self.load_button.setEnabled(enabled) self.analyze_button.setEnabled(enabled and bool(self.image_path)) self.clear_button.setEnabled(enabled) self.refresh_models_button.setEnabled(enabled) def clear_results(self): self.result_edit.clear() self.image_label.clear() self.image_label.setText("请选择图片进行分析") self.image_path = "" self.analyze_button.setEnabled(False) self.progress_bar.setValue(0) self.progress_bar.setFormat("等待操作...") self.statusBar().showMessage("已清除结果") def closeEvent(self, event): # 确保在关闭窗口时停止任何正在运行的线程 if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() if not self.analysis_thread.wait(2000): # 等待2秒 self.analysis_thread.terminate() if self.model_loader_thread and self.model_loader_thread.isRunning(): self.model_loader_thread.quit() if not self.model_loader_thread.wait(2000): # 等待2秒 self.model_loader_thread.terminate() event.accept() if __name__ == "__main__": # 添加全局异常处理 def exception_handler(exctype, value, traceback): """全局异常处理器""" error_msg = f"程序发生未捕获的异常:\n\n类型: {exctype.__name__}\n\n描述: {value}" print(error_msg) # 尝试显示错误对话框 try: app = QApplication.instance() if app is not None: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("程序遇到严重错误") msg.setInformativeText(error_msg) msg.setWindowTitle("未处理的异常") msg.setStandardButtons(QMessageBox.Ok) msg.exec_() except: pass # 调用默认的异常处理器 sys.__excepthook__(exctype, value, traceback) # 设置全局异常处理器 sys.excepthook = exception_handler app = QApplication(sys.argv) app.setApplicationName("多模态大模型图片解读系统") # 设置应用程序样式 app.setStyle("Fusion") # 创建调色板 - 调整为中性主题 palette = QPalette() palette.setColor(QPalette.Window, QColor(200, 104, 248)) # 浅蓝色背景 palette.setColor(QPalette.WindowText, QColor(44, 62, 20)) # 深灰色文本 palette.setColor(QPalette.Base, QColor(255, 255, 255)) # 白色基础 palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240)) # 浅灰色交替基础色 palette.setColor(QPalette.ToolTipBase, Qt.white) # 工具提示基础 palette.setColor(QPalette.ToolTipText, Qt.black) # 工具提示文本 palette.setColor(QPalette.Text, QColor(44, 62, 20)) # 文本颜色 palette.setColor(QPalette.Button, QColor(74, 134, 232)) # 按钮颜色 (蓝色) palette.setColor(QPalette.ButtonText, Qt.white) # 按钮文本 (白色) palette.setColor(QPalette.BrightText, Qt.red) # 亮文本 palette.setColor(QPalette.Highlight, QColor(106, 17, 203)) # 高亮 (紫色) palette.setColor(QPalette.HighlightedText, Qt.white) # 高亮文本 app.setPalette(palette) try: window = MultiModalApp() window.show() sys.exit(app.exec_()) except Exception as e: print(f"应用程序启动失败: {str(e)}") QMessageBox.critical(None, "启动错误", f"应用程序启动失败:\n{str(e)}")
10-13
#下面程序运行地报错: 分析过程中发生错误 启动分析线程失败: '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_())
08-11
# 请将标题行设为全宽,即与界面全宽,计时器可叠加到其上 import sys import os import base64 import json import requests import time from datetime import datetime from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QFrame, QSplitter, QMessageBox, QListWidget, QListWidgetItem) from PyQt5.QtGui import QPixmap, QImage, QFont, QPalette, QColor, QIcon, QPainter, QLinearGradient, QBrush from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QPoint, QTimer # 配置Ollama API设置 - 使用默认端口11434 OLLAMA_HOST = "http://localhost:11434" class ModelLoaderThread(QThread): models_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) def run(self): try: health_url = f"{OLLAMA_HOST}" try: response = requests.get(health_url, timeout=10) if response.status_code != 200: raise Exception(f"Ollama服务不可用 (HTTP {response.status_code})") except Exception as e: self.error_occurred.emit(f"Ollama服务检查失败: {str(e)}") return response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=15) if response.status_code == 200: models_data = response.json() model_list = [model["name"] for model in models_data.get("models", [])] if model_list: self.models_loaded.emit(model_list) else: self.error_occurred.emit("Ollama服务返回了空模型列表") else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("连接Ollama服务超时。请检查网络连接或增加超时设置。") except Exception as e: self.error_occurred.emit(f"加载模型时发生错误: {str(e)}") class StreamAnalysisThread(QThread): chunk_received = pyqtSignal(str) analysis_complete = pyqtSignal(str) progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) def __init__(self, model, image_path, temperature, max_tokens, prompt, parent=None): super().__init__(parent) self.model = model self.image_path = image_path self.temperature = temperature self.max_tokens = max_tokens self.prompt = prompt self._is_running = True def stop(self): self._is_running = False def run(self): try: if not os.path.exists(self.image_path): self.error_occurred.emit(f"图片文件不存在: {self.image_path}") return self.progress_updated.emit(10) with open(self.image_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode("utf-8") data = { "model": self.model, "prompt": self.prompt, "images": [base64_image], "options": { "temperature": self.temperature, "num_predict": self.max_tokens }, "stream": True } self.progress_updated.emit(30) response = requests.post( f"{OLLAMA_HOST}/api/generate", json=data, headers={"Content-Type": "application/json"}, stream=True, timeout=180 ) if response.status_code == 200: full_response = "" for line in response.iter_lines(): if not self._is_running: break if line: try: json_response = json.loads(line.decode('utf-8')) if 'response' in json_response: chunk = json_response['response'] full_response += chunk self.chunk_received.emit(chunk) if json_response.get('done', False): break except json.JSONDecodeError: continue self.progress_updated.emit(90) self.analysis_complete.emit(full_response) else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") self.progress_updated.emit(100) except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("分析请求超时。请尝试减小图片大小或降低模型复杂度。") except Exception as e: self.error_occurred.emit(f"分析图片时发生错误: {str(e)}") class GradientLabel(QLabel): def __init__(self, text, parent=None): super().__init__(text, parent) self.setAlignment(Qt.AlignCenter) self.setMinimumHeight(60) def paintEvent(self, event): painter = QPainter(self) gradient = QLinearGradient(0, 0, self.width(), 0) gradient.setColorAt(0, QColor("#6a11cb")) gradient.setColorAt(1, QColor("#2575fc")) painter.fillRect(self.rect(), QBrush(gradient)) painter.setPen(Qt.white) painter.setFont(QFont("Microsoft YaHei UI", 18, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, self.text()) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_path = "" self.analysis_thread = None self.model_loader_thread = None self.current_response = "" self.start_time = None self.timer = QTimer() self.elapsed_time = 0 self.initUI() self.setWindowTitle("多模态大模型图片解读系统") self.setGeometry(100, 100, 1400, 900) self.timer.timeout.connect(self.update_timer) QTimer.singleShot(500, self.load_models) def initUI(self): self.setStyleSheet(""" QMainWindow { background-color: #f0f4f8; } QGroupBox { border: 2px solid #4a86e8; border-radius: 12px; margin-top: 20px; background-color: rgba(255, 255, 255, 0.95); color: #2c3e50; font-weight: bold; font-size: 12pt; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 8px 20px; background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; border-radius: 8px; top: -0px; min-height: 35px; } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; font-weight: bold; font-size: 11pt; min-height: 35px; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } QPushButton:pressed { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3a76d8, stop:1 #5a01bb); } QPushButton:disabled { background-color: #b0c4de; color: #777777; } QComboBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: white; color: #2c3e50; selection-background-color: #e0f0ff; border-radius: 8px; border: 1px solid #cccccc; font-size: 11pt; } QTextEdit { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; font-size: 10pt; } QSlider::groove:horizontal { border: 1px solid #cccccc; height: 10px; background: #e0e0e0; margin: 2px 0; border-radius: 5px; } QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #6a11cb, stop:1 #4a86e8); border: 1px solid #4a76b0; width: 22px; margin: -6px 0; border-radius: 11px; } QDoubleSpinBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QProgressBar { border: 1px solid #cccccc; border-radius: 8px; text-align: center; color: #2c3e50; background-color: white; font-size: 11pt; height: 25px; } QProgressBar::chunk { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #4a86e8); border-radius: 7px; } QListWidget { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; font-size: 11pt; } QSplitter::handle { background-color: #4a86e8; width: 4px; } """) app_font = QFont("Microsoft YaHei UI", 10) app_font.setBold(False) QApplication.setFont(app_font) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(10) main_layout.setContentsMargins(15, 10, 15, 15) timer_row = QWidget() timer_row.setFixedHeight(80) timer_layout = QHBoxLayout(timer_row) timer_layout.setContentsMargins(0, 0, 0, 0) title_label = QLabel("多模态大模型图片解读系统") title_label.setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-size: 24pt; font-weight: bold; padding: 15px; border-radius: 12px; """) title_label.setAlignment(Qt.AlignCenter) timer_widget = QWidget() timer_widget.setFixedWidth(200) timer_widget_layout = QVBoxLayout(timer_widget) timer_widget_layout.setContentsMargins(10, 5, 10, 5) timer_label = QLabel("分析用时") timer_label.setStyleSheet(""" font-weight: bold; font-size: 12pt; color: white; background-color: rgba(255, 255, 255, 0.2); padding: 5px; border-radius: 5px; """) timer_label.setAlignment(Qt.AlignCenter) self.timer_display = QLabel("00:00:00") self.timer_display.setStyleSheet(""" background-color: rgba(255, 255, 255, 0.9); color: #2c3e50; font-size: 18pt; font-weight: bold; padding: 8px 12px; border-radius: 8px; border: 2px solid rgba(255, 255, 255, 0.5); """) self.timer_display.setAlignment(Qt.AlignCenter) self.timer_display.setFixedHeight(45) timer_widget_layout.addWidget(timer_label) timer_widget_layout.addWidget(self.timer_display) timer_layout.addWidget(title_label, 1) timer_layout.addWidget(timer_widget) main_layout.addWidget(timer_row) splitter = QSplitter(Qt.Horizontal) splitter.setChildrenCollapsible(False) left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setSpacing(15) left_layout.setContentsMargins(10, 15, 10, 10) self.image_group = QGroupBox("图片预览") self.image_group.setMinimumHeight(350) image_layout = QVBoxLayout(self.image_group) image_layout.setContentsMargins(10, 35, 10, 10) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(""" background-color: #ffffff; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; """) self.image_label.setText("请选择图片进行分析") self.image_label.setFont(QFont("Microsoft YaHei UI", 12)) image_layout.addWidget(self.image_label) control_group = QGroupBox("分析控制") control_layout = QVBoxLayout(control_group) control_layout.setContentsMargins(15, 35, 15, 15) model_layout = QHBoxLayout() model_label = QLabel("选择模型:") model_label.setFixedWidth(100) model_label.setStyleSheet("font-weight: bold;") self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") model_layout.addWidget(model_label) model_layout.addWidget(self.model_list) control_layout.addLayout(model_layout) temp_layout = QHBoxLayout() temp_label = QLabel("温度:") temp_label.setFixedWidth(100) temp_label.setStyleSheet("font-weight: bold;") 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(temp_label) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) control_layout.addLayout(temp_layout) token_layout = QHBoxLayout() token_label = QLabel("最大Token:") token_label.setFixedWidth(100) token_label.setStyleSheet("font-weight: bold;") self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(token_label) token_layout.addWidget(self.token_spin) control_layout.addLayout(token_layout) prompt_layout = QVBoxLayout() prompt_label = QLabel("提示词:") prompt_label.setStyleSheet("font-weight: bold;") self.prompt_edit = QTextEdit() self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") self.prompt_edit.setMaximumHeight(100) prompt_layout.addWidget(prompt_label) prompt_layout.addWidget(self.prompt_edit) control_layout.addLayout(prompt_layout) button_layout = QHBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.setIcon(QIcon.fromTheme("document-open")) self.load_button.clicked.connect(self.load_image) self.analyze_button = QPushButton("分析图片") self.analyze_button.setIcon(QIcon.fromTheme("system-search")) self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.clear_button = QPushButton("清除结果") self.clear_button.setIcon(QIcon.fromTheme("edit-clear")) self.clear_button.clicked.connect(self.clear_results) self.refresh_models_button = QPushButton("刷新模型") self.refresh_models_button.setIcon(QIcon.fromTheme("view-refresh")) self.refresh_models_button.clicked.connect(self.load_models) self.stop_button = QPushButton("停止分析") self.stop_button.setIcon(QIcon.fromTheme("process-stop")) self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) button_layout.addWidget(self.load_button) button_layout.addWidget(self.analyze_button) button_layout.addWidget(self.stop_button) button_layout.addWidget(self.clear_button) button_layout.addWidget(self.refresh_models_button) control_layout.addLayout(button_layout) self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) self.progress_bar.setFormat("等待操作...") control_layout.addWidget(self.progress_bar) right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) result_group = QGroupBox("分析结果") result_layout = QVBoxLayout(result_group) result_layout.setContentsMargins(10, 35, 10, 10) self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) self.result_edit.setStyleSheet(""" font-size: 10pt; line-height: 1.15; background-color: white; color: #2c3e50; padding: 15px; border: 1px solid #cccccc; border-radius: 8px; """) result_layout.addWidget(self.result_edit) left_layout.addWidget(self.image_group) left_layout.addWidget(control_group) right_layout.addWidget(result_group) splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([500, 900]) main_layout.addWidget(splitter) self.statusBar().setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-weight: bold; padding-left: 10px; """) self.statusBar().showMessage("正在初始化...") def update_timer(self): self.elapsed_time += 1 hours = self.elapsed_time // 3600 minutes = (self.elapsed_time % 3600) // 60 seconds = self.elapsed_time % 60 self.timer_display.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}") def start_timer(self): self.elapsed_time = 0 self.timer.start(1000) self.timer_display.setText("00:00:00") def stop_timer(self): self.timer.stop() def show_error_dialog(self, title, message): msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle(title) msg.setText("发生错误") msg.setInformativeText(message) if "Ollama" in title: msg.addButton("打开Ollama网站", QMessageBox.ActionRole) msg.addButton("重试", QMessageBox.ActionRole) msg.addButton(QMessageBox.Ok) msg.setStyleSheet(""" QMessageBox { background-color: #f0f4f8; border: 2px solid #4a86e8; border-radius: 12px; } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; min-width: 80px; font-weight: bold; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } """) result = msg.exec_() if result == QMessageBox.Ok: pass elif msg.clickedButton().text() == "打开Ollama网站": import webbrowser webbrowser.open("https://ollama.com/") elif msg.clickedButton().text() == "重试": self.load_models() def load_models(self): self.model_list.clear() self.model_list.addItem("正在加载模型...") self.refresh_models_button.setEnabled(False) self.statusBar().showMessage("正在从Ollama获取模型列表...") 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.finished.connect(self.model_loader_finished) self.model_loader_thread.start() def update_model_list(self, model_list): self.model_list.clear() if model_list: multimodal_models = [model for model in model_list if "vision" in model.lower() or "llava" in model.lower()] title_item = QListWidgetItem("=== 多模态大模型 ===") self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) for model in multimodal_models: item = QListWidgetItem(f"● {model}") item.setForeground(QColor("#2c3e50")) item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) self.model_list.addItem(item) other_models = [model for model in model_list if model not in multimodal_models] if other_models: title_item = QListWidgetItem("=== 其他模型 ===") self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) for model in other_models: item = QListWidgetItem(f"○ {model}") item.setForeground(QColor("#2c3e50")) item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) self.model_list.addItem(item) self.statusBar().showMessage(f"已加载 {len(model_list)} 个模型") if multimodal_models: self.model_list.setCurrentRow(1) else: self.model_list.addItem("未找到可用模型") self.statusBar().showMessage("未找到可用模型") def handle_model_load_error(self, error): self.model_list.clear() self.model_list.addItem("加载模型失败") self.statusBar().showMessage(error) self.show_error_dialog("模型加载错误", f"{error}\n\n可能原因:\n1. Ollama服务未启动 (当前地址: {OLLAMA_HOST})\n2. Ollama未正确安装\n3. 网络连接问题\n\n解决方案:\n1. 下载并安装Ollama: https://ollama.com/download\n2. 启动Ollama服务\n3. 检查网络连接") def model_loader_finished(self): self.refresh_models_button.setEnabled(True) def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: try: self.image_path = file_path pixmap = QPixmap(file_path) if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") self.original_pixmap = pixmap.copy() label_width = self.image_label.width() - 20 label_height = self.image_label.height() - 20 scaled_pixmap = pixmap.scaled( label_width, label_height, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) self.analyze_button.setEnabled(True) self.statusBar().showMessage(f"已加载图片: {os.path.basename(file_path)}") self.progress_bar.setFormat("图片已加载,准备分析") except Exception as e: self.statusBar().showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def analyze_image(self): if not self.image_path: self.statusBar().showMessage("错误: 请先加载图片") return selected_items = self.model_list.selectedItems() if not selected_items: self.statusBar().showMessage("错误: 请选择模型") return selected_item = selected_items[0] if selected_item.text().startswith("==="): self.statusBar().showMessage("错误: 请选择有效的模型") return model_name = selected_item.data(Qt.UserRole) self.current_response = "" self.start_timer() timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.result_edit.setHtml(f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'>图片分析结果</div> <div style='color: #2c3e50; font-size: 10pt; line-height: 1.15;'><p style='color: #4a86e8; font-weight: bold;'>正在分析图片,请稍候...</p></div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 9pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>开始时间:</span> {timestamp} </div> """) 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() self.analysis_thread = StreamAnalysisThread(model_name, self.image_path, temperature, max_tokens, prompt) self.analysis_thread.chunk_received.connect(self.handle_stream_chunk) 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.finished.connect(self.analysis_finished) self.analysis_thread.start() def handle_stream_chunk(self, chunk): self.current_response += chunk formatted_result = self.format_result(self.current_response) selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") result_html = f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'>图片分析结果</div> <div style='color: #2c3e50; font-size: 10pt; line-height: 1.15;'>{formatted_result}</div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 9pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>时间:</span> {timestamp}   <span style='color: #27ae60; font-weight: bold;'>● 流式传输中...</span> </div> """ self.result_edit.setHtml(result_html) cursor = self.result_edit.textCursor() cursor.movePosition(cursor.End) self.result_edit.setTextCursor(cursor) self.result_edit.ensureCursorVisible() def handle_analysis_result(self, result): self.stop_timer() 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 "未知模型" current_html = self.result_edit.toHtml() updated_html = current_html.replace( '<span style=\'color: #27ae60; font-weight: bold;\'>● 流式传输中...</span>', f'<span style=\'color: #2ecc71; font-weight: bold;\'>✓ 分析完成 - 用时: {self.timer_display.text()}</span>' ) self.result_edit.setHtml(updated_html) self.statusBar().showMessage("图片分析完成") def stop_analysis(self): if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() self.analysis_thread.wait(2000) self.stop_timer() self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析已停止") self.statusBar().showMessage("分析已停止") def format_result(self, result): formatted = result formatted = formatted.replace("# ", "").replace("## ", "").replace("### ", "") formatted = formatted.replace("**", "").replace("*", "").replace("__", "").replace("_", "") formatted = formatted.replace("- ", "• ").replace("• ", " • ") formatted = formatted.replace(".", "。").replace(",", ",").replace(":", ":") paragraphs = [] double_newline_paras = formatted.split("\n\n") for para in double_newline_paras: if "\n" in para: sub_paras = para.split("\n") paragraphs.extend(sub_paras) else: paragraphs.append(para) if len(paragraphs) == 1: paragraphs = formatted.split("。") paragraphs = [p.strip() + ("。" if i < len(paragraphs) - 1 else "") for i, p in enumerate(paragraphs) if p.strip()] html_paragraphs = [] for i, p in enumerate(paragraphs): p = p.strip() if p: if i == 0: html_paragraphs.append( f"<div style='margin: 10px 0; padding: 10px; background-color: #f8f9fa; border-left: 4px solid #4a86e8;'><p style='font-size: 12pt; font-weight: bold; color: #2c3e50; margin: 0; line-height: 1.15;'>{p}</p></div>") else: if p.strip().startswith("•"): html_paragraphs.append( f"<div style='margin: 8px 0; padding-left: 20px;'><p style='font-size: 10pt; color: #2c3e50; margin: 0; line-height: 1.15;'>{p}</p></div>") else: html_paragraphs.append( f"<div style='margin: 8px 0; padding: 8px; background-color: #ffffff; border-radius: 5px; border: 1px solid #f0f0f0;'><p style='font-size: 10pt; color: #2c3e50; margin: 0; text-indent: 2em; line-height: 1.15;'>{p}</p></div>") return "".join(html_paragraphs) def handle_analysis_error(self, error): self.stop_timer() self.result_edit.setPlainText(f"错误: {error}") self.statusBar().showMessage(f"错误: {error}") self.progress_bar.setFormat("分析失败") self.show_error_dialog("分析错误", error) def update_progress(self, value): self.progress_bar.setValue(value) if value < 30: self.progress_bar.setFormat("准备分析... %p%") elif value < 70: self.progress_bar.setFormat("发送请求到Ollama... %p%") elif value < 90: self.progress_bar.setFormat("处理响应... %p%") else: self.progress_bar.setFormat("完成分析... %p%") def analysis_finished(self): self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析完成") def set_buttons_enabled(self, enabled): self.load_button.setEnabled(enabled) self.analyze_button.setEnabled(enabled and bool(self.image_path)) self.clear_button.setEnabled(enabled) self.refresh_models_button.setEnabled(enabled) def clear_results(self): self.result_edit.clear() self.image_label.clear() self.image_label.setText("请选择图片进行分析") self.image_path = "" self.analyze_button.setEnabled(False) self.progress_bar.setValue(0) self.progress_bar.setFormat("等待操作...") self.statusBar().showMessage("已清除结果") self.stop_timer() self.timer_display.setText("00:00:00") def closeEvent(self, event): if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() if not self.analysis_thread.wait(2000): self.analysis_thread.terminate() if self.model_loader_thread and self.model_loader_thread.isRunning(): self.model_loader_thread.quit() if not self.model_loader_thread.wait(2000): self.model_loader_thread.terminate() self.timer.stop() event.accept() if __name__ == "__main__": def exception_handler(exctype, value, traceback): error_msg = f"程序发生未捕获的异常:\n\n类型: {exctype.__name__}\n\n描述: {value}" print(error_msg) try: app = QApplication.instance() if app is not None: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("程序遇到严重错误") msg.setInformativeText(error_msg) msg.setWindowTitle("未处理的异常") msg.setStandardButtons(QMessageBox.Ok) msg.exec_() except: pass sys.__excepthook__(exctype, value, traceback) sys.excepthook = exception_handler app = QApplication(sys.argv) app.setApplicationName("多模态大模型图片解读系统") app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.Window, QColor(200, 104, 248)) palette.setColor(QPalette.WindowText, QColor(44, 62, 20)) palette.setColor(QPalette.Base, QColor(255, 255, 255)) palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240)) palette.setColor(QPalette.ToolTipBase, Qt.white) palette.setColor(QPalette.ToolTipText, Qt.black) palette.setColor(QPalette.Text, QColor(44, 62, 20)) palette.setColor(QPalette.Button, QColor(74, 134, 232)) palette.setColor(QPalette.ButtonText, Qt.white) palette.setColor(QPalette.BrightText, Qt.red) palette.setColor(QPalette.Highlight, QColor(106, 17, 203)) palette.setColor(QPalette.HighlightedText, Qt.white) app.setPalette(palette) try: window = MultiModalApp() window.show() sys.exit(app.exec_()) except Exception as e: print(f"应用程序启动失败: {str(e)}") QMessageBox.critical(None, "启动错误", f"应用程序启动失败:\n{str(e)}")
最新发布
10-14
<template> <div :class="['talk-item', isLeft? 'left' : 'right']" :key="data.timestamp" v-if="isTemp == 1? true : text" > <!-- 遍历布局节点 --> <template v-for="it in layout[isLeft? 'left' : 'right']"> <!-- 头像部分 --> <div v-if="it === LayoutNode.AVATAR" :key="`${data.startFrame}-${it}`" class="talk-avatar" > <el-avatar class="avatar" size="large" :style="{ 'background-image': backgroundImg }" > <!-- avatar 用于展示头像 --> </el-avatar> </div> <!-- 文本段落部分 --> <div v-if="it === LayoutNode.PARAGRAPH" :key="`${data.startFrame}-${it}`" class="highlighted" :class="{ 'talk-paragraph': true, 'highlight': highlight, 'blink': isBlink }" > <!-- 时间和其他操作按钮 --> <div class="time"> <!-- 左侧时间显示 --> <span v-if="isLeft" style="font-size: 12px;"> {{ timestamp | formatDate('hh:mm:ss') }} </span> <!-- 复制按钮 --> <i v-if="canCopy && data.text.length > 1" class="el-icon-s-claim" @click="setAnswer" ></i> <!--编辑按钮--> <el-tooltip v-if="canEdit" effect="dark" content="编辑" placement="top-start" class="item" > <i class="el-icon-edit" @click="changeEditStatus" v-if="(data.text.length > 0 && status == RealTimeStatus.History) || (data.text.length > 0 && status == RealTimeStatus.RealTime)" ></i> </el-tooltip> <!-- 右侧时间显示 --> <span v-if="!isLeft" style="font-size: 11px;"> {{ timestamp | formatDate('hh:mm:ss') }} </span> </div> <!-- 文本内容容器 --> <div :class="['text-container']" ref="textContainer" @dblclick="fetchTalkItem"> <!-- 统一前置 --> <i class="isIcon iconfont icon-yinbo"></i> <!-- 编辑模式下的文本框 --> <el-input v-if="isEdit" :class="['text-box',{highlighted: isPlaying }]" type="textarea" style="font-size: 16px;" :rows="3" v-model="data.text" @input="handleInput" ></el-input> <!-- 非编辑模式下的文本显示 --> <div v-else v-html="`${text}  `" :class="['text-box']" style="font-size: 16px;"></div> <!-- 段落进度条 --> <div class="progress-highlight" :style="{ width: progressWidth }"></div> </div> <!-- 命中标签 --> <div class="isHit" v-if="matchedHitRuleNames.length > 0"> <i style="color:#007bff;" class="el-icon-circle-check"></i> <span v-for="(name, index) in matchedHitRuleNames" :key="index">  {{ name }} </span> </div> </div> </template> </div> </template> <script lang="ts"> import { Message, MessageBox } from "element-ui"; import { Component, Prop, Vue, Watch } from "nuxt-property-decorator"; import { Keyword, TalkItem, TalkState } from "../../../types"; import * as dayjs from "dayjs"; import '../../../assets/iconFont/iconfont.css' // import img_police from "~/assets/img/anonymity-police.jpg" // 设置谈话人 被谈话人的位置 import img_police from "../../../assets/img/anonymity-telephonist.jpg"; import img_usr from "../../../assets/img/anonymity-square.png"; import { State } from "vuex-class"; const enum LayoutNode { AVATAR, PARAGRAPH, } /** * ZKer 设置谈话人是否在左边 */ const IsLeft = true; @Component({ name: "talkItem", components: {}, filters: { formatDate(value) { return dayjs(new Date(value)).format("YYYY-MM-DD HH:mm:ss"); }, }, }) export default class extends Vue { @Prop({ type: Object, required: true, default: {} }) data!: TalkItem; @Prop({ type: Boolean, required: true }) highlight: boolean; @Prop({ type: Boolean, required: true }) isPolice: boolean; @Prop({ type: Boolean, required: true }) canCopy: boolean; @Prop({ required: true }) canEdit; // 判断编辑是否可修改(历史谈话可/实时谈话) @Prop({ type: [], required: true }) RealTimeStatus; // 历史 实时谈话状态 @Prop({ required: true }) status; @Prop() busGroups!: any; isEdit: boolean = false; isLeft: boolean = false; isBlink: boolean = false; isTemp: any = null; // 判断用户删除(监听input的输入事件时 证明在删除 否则反之) timestamp: string | number = " "; text: string = " "; backgroundImg: string = `url(${this.isPolice? img_police : img_usr}`; LayoutNode: any = LayoutNode; layout: any = { // 统计左(对话数据)右() left: [LayoutNode.AVATAR, LayoutNode.PARAGRAPH], right: [LayoutNode.PARAGRAPH, LayoutNode.AVATAR], }; @State talk!: TalkState; matchedHitRuleNames: string[] = []; // -=-= @Prop({ type: Boolean, default: false }) isPlaying!: boolean; progressWidth: string = '0%'; duration: number = null; // 播放完一段 animationFrameId: number | null = null; @Watch('isPlaying') onIsPlayingChange(isPlaying: boolean) { if (isPlaying) { this.startProgressAnimation(); } else { this.stopProgressAnimation(); } } startProgressAnimation() { const startTime = performance.now(); const animate = (now: number) => { const elapsed = now - startTime; const progress = Math.min(elapsed / this.duration, 1); this.progressWidth = `${progress * 100}%`; if (progress < 1) { this.animationFrameId = requestAnimationFrame(animate); } }; this.animationFrameId = requestAnimationFrame(animate); } stopProgressAnimation() { if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } this.progressWidth = '0%'; } beforeDestroy() { this.stopProgressAnimation(); } // -=-= @Watch("data", { deep: true, immediate: true }) onDataChange(newData: TalkItem) { // if (!newData.policy) { // this.console.debug(newData,newData.last, newData.text, JSON.stringify(newData.keywords), newData.startFrame) // } this.text = newData.text; this.setKeyword(true); } @Watch("canEdit") onCanEditChange(newValue: boolean) { if (newValue) { this.$store.dispatch("talk/FetchKeywords"); } } // handleClick(){//单击文本字段 // if (this.status == this.RealTimeStatus.History) { // this.$emit('jump', this.data) // console.log('this.newData',this.data); // // } // console.log(121212); // // } created() { // 对讲内容 // console.log(this.data, "opop"); // 命中次数 // console.log(this.busGroups.hitRules, "1212121"); } /** * 统一中英文标点符号为英文格式 */ normalizePunctuation(text: string): string { const punctuationMap: { [key: string]: string } = { ',': ',', '。': '.', '?': '?', '!': '!', ';': ';', ':': ':', '“': '"', '”': '"', '‘': "'", '’': "'", '(': '(', ')': ')', '【': '[', '】': ']', '《': '<', '》': '>', '、': '\\', '——': '-', '…': '...', '—': '-', '·': '.' }; return text.replace(/[^\u0000-\u00ff]/g, ch => punctuationMap[ch] || ch); } scrollToParagraph() {//添加 scrollToParagraph 方法,用于滚动到指定段落 const element = this.$el; element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } mounted() { this.text = this.data.text; this.isLeft = Boolean(Number(this.isPolice) ^ Number(IsLeft)); this.timestamp = this.data.timestamp; this.setKeyword(); if (this.data.startFrame && this.data.endFrame) { this.duration = this.data.endFrame - this.data.startFrame; // 动态计算段落的时间长度 } // **新增延迟逻辑:通过 $watch 监听 busGroups 变化** this.$watch('busGroups', (newGroups) => { if (newGroups && newGroups.hitRules) { this.matchHitRules(newGroups.hitRules); // 自定义标签匹配方法 } }, { deep: true, immediate: true }); } // 新增标签匹配方法 matchHitRules(hitRules: any[]) { const matchedRuleNamesSet = new Set<string>(); const normalizedDataText = this.normalizePunctuation(this.text).toLowerCase(); const currentSpeakerPrefix = this.data.policy ? '管教民警:' : '在押人员:'; hitRules.forEach(rule => { const sentences = this.splitSentences(rule.hitSentence); sentences.forEach(sentence => { const normalizedSentence = this.normalizePunctuation(sentence).toLowerCase(); if (normalizedSentence.startsWith(currentSpeakerPrefix.toLowerCase()) && normalizedSentence.includes(normalizedDataText)) { matchedRuleNamesSet.add(rule.hitRuleName); } }); }); this.matchedHitRuleNames = Array.from(matchedRuleNamesSet); } // 按管教民警或被监管人分割句子 splitSentences(text: string): string[] { const pattern = /(管教民警|被监管人):/g; const matches = text.match(pattern); const sentences: string[] = []; let startIndex = 0; if (matches) { matches.forEach((match, index) => { const endIndex = index === matches.length - 1 ? text.length : text.indexOf(matches[index + 1]); const sentence = text.substring(startIndex, endIndex).trim(); if (sentence) { sentences.push(sentence); } startIndex = endIndex; }); } return sentences; } handleInput(value) { this.isTemp = 1; // 如果输入框的输入发生改变证明在删除 赋值为1 {上面的判断证明如果输入框变化 为true 否则循环的text(文本)有值显示 没值不显示} // 监听input输入的文本长度 if (value.length == 1) { this.$message.error("输入字符长度至少1位"); // 阻止表单提交 } else { this.data.text = value; } } changeEditStatus() { const initialText = this.data.text; this.$prompt("", "文本修正", { confirmButtonText: "确定", cancelButtonText: "取消", inputPlaceholder: "", inputValue: initialText, // 设置$prompt的input初始值为编辑的文本 }) .then(({ value }) => { if (value.length < 1) { Message({ message: "最少保留1个字符内容!", type: "error" }); return; } else { // 正常输入 this.data.text = value; this.submitChange(); } }) .catch(() => { this.$message({ type: "info", message: "取消操作", }); }); } /** * 修改文本提交 */ async submitChange() { this.$emit("editContent", Number(this.data.startFrame), this.data.text, this.data.policy); this.setKeyword(false, this.talk.keywords.map((it: Keyword) => { return it.text; })); } /** * 关键字用label标签 */ setKeyword(forceUpdate: boolean = false, keywords: string[] = this.data.keywords) { // const keywords = this.canEdit? this.talk.keywords.map((it: Keyword) => { // return it.text // }) : this.data.keywords if (keywords == null || keywords.length == 0) { return; } if (this.data.policy /*|| !this.data.last*/) { return; } keywords.forEach((it) => { if (it.length == 0) { return; } this.text = this.text.replace(new RegExp(it, "gm"), `<label class='keyword'>${it}</label>`); }); if (forceUpdate) { this.$nextTick(() => { this.$forceUpdate(); }); } } /** * 搜索字用span标签 */ setSearchWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), `<span class='searched'>${word}</span>`); } setGaugesWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), `<span class='gauges'>${word}</span>`); } clearSearchWord() { this.text = this.text.replace(new RegExp("<span class='searched'>", "gm"), ""); this.text = this.text.replace(new RegExp("</span>", "gm"), ""); } copy() { this.$copyText(this.text) .then(() => { this.$notify({ title: "成功", message: "谈话内容已成功复制到粘贴板!", duration: 3000, type: "success", }); }) .catch(() => { this.$notify({ title: "失败", message: "谈话内容复制到粘贴板失败!", duration: 3000, type: "error", }); }); } fetchTalkItem() {//单个段落 console.log("this.data",this.data); // 确保有有效的startFrame和endFrame if (this.data.startFrame && this.data.endFrame && this.data.endFrame - this.data.startFrame > 10) { // 最小10ms播放 this.$emit("fetchTalkItem", this.data); } else { console.warn("段落时间太短,不播放:", this.data); } } setAnswer() { this.$emit("setAnswer", this.data.text); } blink() { this.isBlink = true; this.console.debug(this); setTimeout(() => { this.isBlink = false; }, 1500); } } </script> <style lang="less" scoped> @import "../../../assets/styles/variables"; // 对讲内容是否命中 .isHit { font-size: 13px; color: #8b9199; margin: 8px 0 0 0; align-self: flex-start; } // 谈话框样式 公共配置 .talk-item { color: #47494e; padding: 0 0.5rem; .talk-avatar { display: inline-block; .el-avatar { position: relative; } .avatar { border-radius: 50%; background-size: 40px; } } .talk-paragraph { display: inline-block; padding: 0 1rem 10px 1rem!important; max-width: 70%!important; margin-bottom:8px!important; box-sizing: border-box; .time { padding-left: 1rem; padding-bottom: 0.3rem; font-size: 1rem; color: #ccc; /*min-height: 14px;*/ i { cursor: pointer; color: #7ea1de; } i:hover { color: slateblue; } } .progress-highlight{ position: absolute; top: 0; left: 0; border-radius: 8px; height: 93%; background-color: rgba(61, 111, 205, 0.5); /* 半透明蓝色 */ z-index: 0; transition: width 0.1s linear; } .text-container { display: flow-root; border-radius: 8px; min-width:120px; height: fit-content !important; overflow-x: hidden; .text-box { position: relative; display: inline-block; border-radius: 8px; padding: 10px; user-select: text; word-wrap: break-word; // 确保长单词换行 word-break: break-word; // 处理中文换行 overflow-wrap: break-word; // 处理长URL等 min-width:85%; max-width: 28ch; /* 限制最大宽度为大约28个字符的宽度包括符号(宽度自适应) */ overflow-x: hidden; /* 隐藏水平溢出 */ white-space: pre-wrap; /* 保留空白符序列,但正常地进行换行 */ } } } // -=-=-=-=-=-start播放命中高亮 .text-box.highlighted { background-color: #3d6fcd !important; color: white !important; transition: background-color 0.2s ease; } .talk-item.left .text-container:hover .isIcon.iconfont.icon-yinbo, .talk-item.right .text-container:hover .isIcon.iconfont.icon-yinbo, .text-container .isIcon.iconfont.icon-yinbo[style*="display: inline-block"] { display: inline-block !important; } // -=-=--=-=-end .talk-paragraph::before { display: inline-block; content: ""; width: 0; height: 0; line-height: normal; // border-width: 10px; // border-style: solid; // border-color: transparent; position: relative; top: 49px; } .highlight { .text-box { background-color: @talkItemHighlightBGColor !important; // background-color: #3d6fcd !important; // 原为 @talkItemHighlightBGColor,改为悬停色 // color: white !important; // 新增文字颜色 } } .blink { .text-box { background-color: @talkItemHighlightBGColor !important; animation: blink 0.5s 3; -webkit-animation-name: blink; -webkit-animation-duration: 500ms; -webkit-animation-iteration-count: 3; -webkit-animation-timing-function: ease-in-out; } } @keyframes blink { 0% { color: #fab4b4; } 25% { color: #fa6161; } 50% { color: #ff0000; } 75% { color: #fa6161; } 100% { color: #fab4b4; } } &:last-child { margin-bottom: 30px; } } ////////////////////////////////////////////////////////////////////////// // 左侧谈话框样式 .talk-item.left { display: flex; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { .el-avatar { top: 38px; } } .talk-paragraph { .time { i { margin-left: 1rem; } } .text-container { position: relative; .isIcon.iconfont.icon-yinbo{ display: none; /* 默认隐藏 */ position: absolute; right: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { background-color: @talkItemLeftBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block; /* 悬停时显示图标 */ } } } } .talk-paragraph::before { border-right-width: 10px; border-right-color: @talkItemLeftBGColor; left: -19px; } .highlight::before, .blink::before { border-right-color: @talkItemHighlightBGColor !important; } } ////////////////////////////////////////////////////////////////////////// // 右侧谈话框样式 .talk-item.right { text-align: right; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { float: right; .el-avatar { top: 36px; } } .talk-paragraph { .time { i { margin-right: 1rem; } } .text-container {//右侧谈话 position: relative; .isIcon.iconfont.icon-yinbo { display: none; /* 默认隐藏 */ position: absolute; left: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { text-align: left; background-color: @talkItemRightBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } } } } .talk-paragraph::before { border-left-width: 10px; border-left-color: @talkItemRightBGColor; right: -19px; } .highlight::before, .blink::before { border-left-color: @talkItemHighlightBGColor !important; } } </style> <style lang="less"> .text-container { .text-box { .searched { color: #f54646; background-color: #f3f35d; } .keyword { color: red; font-weight: bold; } .gauges { color: #2fefd8; } } .text-box.el-input { padding: 5px !important; .el-input__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } .text-box.el-textarea { width: 350px; padding: 5px !important; .el-textarea__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } } .talk-item.right { .text-box.el-input, .text-box.el-textarea { float: right; right: 0px; } } </style> import jsonData from '../public/123.json' 基于引用数据来操作<template> <div class="main"> <div class="sec"> <div v-for="item in list" :key="item.timestamp"> {{ item.text }} </div> </div> </div> </template> <script lang="ts"> import { reactive, toRefs, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import jsonData from '../public/123.json' // 定义 content 中每项的类型 interface ContentItem { count: number policy: boolean speaker: string timestamp: number text: string startFrame: string endFrame: string emotions: null keywords: null last: boolean emotionSeg: Record<string, unknown> } export default { name: '', setup() { const router = useRouter() const route = useRoute() const data = reactive<{ list: ContentItem[] }>({ list: [] // 初始化为空数组但有明确类型 }) onMounted(() => { data.list = jsonData.data.content as ContentItem[] console.log(data.list) console.log(jsonData.data.content, 'jsonData') }) const refData = toRefs(data) return { ...refData } } } </script> <style lang="scss" scoped> .main{ width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; .sec{ width: 800px; height: 600px; border: 1px solid red; } } </style> 修复一下吧亲 基于我提供代码
08-20
#将计时器行合到最顶上的标题行上,优化结果文本框中的显示格式,尽量消除不必要的#、*符事情 import sys import os import base64 import json import requests import time from datetime import datetime from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QFrame, QSplitter, QMessageBox, QListWidget, QListWidgetItem) from PyQt5.QtGui import QPixmap, QImage, QFont, QPalette, QColor, QIcon, QPainter, QLinearGradient, QBrush from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QPoint, QTimer # 配置Ollama API设置 - 使用默认端口11434 OLLAMA_HOST = "http://localhost:11434" class ModelLoaderThread(QThread): models_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) def run(self): try: # 先检查Ollama服务是否可用 health_url = f"{OLLAMA_HOST}" try: response = requests.get(health_url, timeout=10) if response.status_code != 200: raise Exception(f"Ollama服务不可用 (HTTP {response.status_code})") except Exception as e: self.error_occurred.emit(f"Ollama服务检查失败: {str(e)}") return # 请求Ollama获取可用模型列表 response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=15) if response.status_code == 200: models_data = response.json() model_list = [model["name"] for model in models_data.get("models", [])] if model_list: self.models_loaded.emit(model_list) else: self.error_occurred.emit("Ollama服务返回了空模型列表") else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("连接Ollama服务超时。请检查网络连接或增加超时设置。") except Exception as e: self.error_occurred.emit(f"加载模型时发生错误: {str(e)}") class StreamAnalysisThread(QThread): chunk_received = pyqtSignal(str) # 新增:接收流式数据块的信号 analysis_complete = pyqtSignal(str) progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) def __init__(self, model, image_path, temperature, max_tokens, prompt, parent=None): super().__init__(parent) self.model = model self.image_path = image_path self.temperature = temperature self.max_tokens = max_tokens self.prompt = prompt self._is_running = True def stop(self): self._is_running = False def run(self): try: # 检查图片文件是否存在 if not os.path.exists(self.image_path): self.error_occurred.emit(f"图片文件不存在: {self.image_path}") return # 读取图片并转换为base64 self.progress_updated.emit(10) with open(self.image_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode("utf-8") # 构造API请求数据 - 启用流式传输 data = { "model": self.model, "prompt": self.prompt, "images": [base64_image], "options": { "temperature": self.temperature, "num_predict": self.max_tokens }, "stream": True # 启用流式传输 } # 发送流式请求到Ollama API self.progress_updated.emit(30) response = requests.post( f"{OLLAMA_HOST}/api/generate", json=data, headers={"Content-Type": "application/json"}, stream=True, # 启用流式响应 timeout=180 ) if response.status_code == 200: full_response = "" for line in response.iter_lines(): if not self._is_running: break if line: try: # 解析JSON响应 json_response = json.loads(line.decode('utf-8')) # 检查是否包含响应内容 if 'response' in json_response: chunk = json_response['response'] full_response += chunk # 发射单个数据块 self.chunk_received.emit(chunk) # 检查是否完成 if json_response.get('done', False): break except json.JSONDecodeError: continue self.progress_updated.emit(90) self.analysis_complete.emit(full_response) else: self.error_occurred.emit(f"API错误 ({response.status_code}): {response.text[:200]}") self.progress_updated.emit(100) except requests.exceptions.ConnectionError: self.error_occurred.emit("无法连接到Ollama服务。请确保Ollama已启动并正在运行。") except requests.exceptions.Timeout: self.error_occurred.emit("分析请求超时。请尝试减小图片大小或降低模型复杂度。") except Exception as e: self.error_occurred.emit(f"分析图片时发生错误: {str(e)}") class GradientLabel(QLabel): """带渐变背景的标签""" def __init__(self, text, parent=None): super().__init__(text, parent) self.setAlignment(Qt.AlignCenter) self.setMinimumHeight(60) def paintEvent(self, event): painter = QPainter(self) gradient = QLinearGradient(0, 0, self.width(), 0) gradient.setColorAt(0, QColor("#6a11cb")) gradient.setColorAt(1, QColor("#2575fc")) painter.fillRect(self.rect(), QBrush(gradient)) painter.setPen(Qt.white) painter.setFont(QFont("Microsoft YaHei UI", 18, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, self.text()) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_path = "" self.analysis_thread = None self.model_loader_thread = None self.current_response = "" # 新增:存储当前响应内容 self.start_time = None # 新增:计时器开始时间 self.timer = QTimer() # 新增:计时器 self.elapsed_time = 0 # 新增:已用时间 self.initUI() self.setWindowTitle("多模态大模型图片解读系统") self.setGeometry(100, 100, 1400, 900) # 扩大窗口尺寸 # 连接计时器 self.timer.timeout.connect(self.update_timer) # 延迟加载模型,确保UI完全初始化 QTimer.singleShot(500, self.load_models) # 修复:直接使用QTimer def initUI(self): # 设置主窗口样式 - 中性色调背景 self.setStyleSheet(""" QMainWindow { background-color: #f0f4f8; } QGroupBox { border: 2px solid #4a86e8; border-radius: 12px; margin-top: 20px; background-color: rgba(255, 255, 255, 0.95); color: #2c3e50; font-weight: bold; font-size: 12pt; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 8px 20px; /* 增加内边距 */ background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; border-radius: 8px; top: -0px; /* 向上移动更多 */ min-height: 35px; /* 增加高度 */ } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; font-weight: bold; font-size: 11pt; min-height: 35px; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } QPushButton:pressed { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3a76d8, stop:1 #5a01bb); } QPushButton:disabled { background-color: #b0c4de; color: #777777; } QComboBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: white; color: #2c3e50; selection-background-color: #e0f0ff; border-radius: 8px; border: 1px solid #cccccc; font-size: 11pt; } QTextEdit { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; font-size: 10pt; /* 缩小结果框字体 */ } QSlider::groove:horizontal { border: 1px solid #cccccc; height: 10px; background: #e0e0e0; margin: 2px 0; border-radius: 5px; } QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #6a11cb, stop:1 #4a86e8); border: 1px solid #4a76b0; width: 22px; margin: -6px 0; border-radius: 11px; } QDoubleSpinBox { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; padding: 5px 10px; min-height: 30px; font-size: 11pt; } QProgressBar { border: 1px solid #cccccc; border-radius: 8px; text-align: center; color: #2c3e50; background-color: white; font-size: 11pt; height: 25px; } QProgressBar::chunk { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #4a86e8); border-radius: 7px; } QListWidget { background-color: white; color: #2c3e50; border: 1px solid #cccccc; border-radius: 8px; font-size: 11pt; } QSplitter::handle { background-color: #4a86e8; width: 4px; } """) # 设置字体 app_font = QFont("Microsoft YaHei UI", 10) app_font.setBold(False) QApplication.setFont(app_font) # 创建主窗口部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # 标题 - 使用渐变背景 title_label = QLabel("多模态大模型图片解读系统") title_label.setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-size: 24pt; font-weight: bold; padding: 15px; border-radius: 12px; """) title_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(title_label) # 创建计时器显示 timer_layout = QHBoxLayout() timer_label = QLabel("分析用时:") timer_label.setStyleSheet("font-weight: bold; font-size: 12pt; color: #2c3e50;") self.timer_display = QLabel("00:00:00") self.timer_display.setStyleSheet(""" background-color: #2c3e50; color: #ffffff; font-size: 16pt; font-weight: bold; padding: 8px 16px; border-radius: 8px; min-width: 120px; """) self.timer_display.setAlignment(Qt.AlignCenter) timer_layout.addWidget(timer_label) timer_layout.addWidget(self.timer_display) timer_layout.addStretch() # 添加弹性空间 main_layout.addLayout(timer_layout) # 创建分割器 splitter = QSplitter(Qt.Horizontal) splitter.setChildrenCollapsible(False) # 左侧面板(图片和控制) left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setSpacing(15) left_layout.setContentsMargins(10, 15, 10, 10) # 图片显示区域 - 修复标题显示问题 self.image_group = QGroupBox("图片预览") self.image_group.setMinimumHeight(350) # 增加高度 image_layout = QVBoxLayout(self.image_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) image_layout.setContentsMargins(10, 35, 10, 10) # 上边距增加10px self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet(""" background-color: #ffffff; border: 1px solid #cccccc; border-radius: 8px; padding: 10px; """) self.image_label.setText("请选择图片进行分析") self.image_label.setFont(QFont("Microsoft YaHei UI", 12)) image_layout.addWidget(self.image_label) # 控制面板 - 修复标题显示问题 control_group = QGroupBox("分析控制") control_layout = QVBoxLayout(control_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) control_layout.setContentsMargins(15, 35, 15, 15) # 上边距增加10px # 模型选择 model_layout = QHBoxLayout() model_label = QLabel("选择模型:") model_label.setFixedWidth(100) model_label.setStyleSheet("font-weight: bold;") self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") model_layout.addWidget(model_label) model_layout.addWidget(self.model_list) control_layout.addLayout(model_layout) # 温度控制 temp_layout = QHBoxLayout() temp_label = QLabel("温度:") temp_label.setFixedWidth(100) temp_label.setStyleSheet("font-weight: bold;") 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(temp_label) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) control_layout.addLayout(temp_layout) # 最大token数 token_layout = QHBoxLayout() token_label = QLabel("最大Token:") token_label.setFixedWidth(100) token_label.setStyleSheet("font-weight: bold;") self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(token_label) token_layout.addWidget(self.token_spin) control_layout.addLayout(token_layout) # 自定义提示 prompt_layout = QVBoxLayout() prompt_label = QLabel("提示词:") prompt_label.setStyleSheet("font-weight: bold;") self.prompt_edit = QTextEdit() # 设置默认提示词 self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") self.prompt_edit.setMaximumHeight(100) prompt_layout.addWidget(prompt_label) prompt_layout.addWidget(self.prompt_edit) control_layout.addLayout(prompt_layout) # 按钮区域 button_layout = QHBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.setIcon(QIcon.fromTheme("document-open")) self.load_button.clicked.connect(self.load_image) self.analyze_button = QPushButton("分析图片") self.analyze_button.setIcon(QIcon.fromTheme("system-search")) self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.clear_button = QPushButton("清除结果") self.clear_button.setIcon(QIcon.fromTheme("edit-clear")) self.clear_button.clicked.connect(self.clear_results) self.refresh_models_button = QPushButton("刷新模型") self.refresh_models_button.setIcon(QIcon.fromTheme("view-refresh")) self.refresh_models_button.clicked.connect(self.load_models) self.stop_button = QPushButton("停止分析") # 新增:停止按钮 self.stop_button.setIcon(QIcon.fromTheme("process-stop")) self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) button_layout.addWidget(self.load_button) button_layout.addWidget(self.analyze_button) button_layout.addWidget(self.stop_button) # 添加停止按钮 button_layout.addWidget(self.clear_button) button_layout.addWidget(self.refresh_models_button) control_layout.addLayout(button_layout) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) self.progress_bar.setFormat("等待操作...") control_layout.addWidget(self.progress_bar) # 右侧面板(结果) - 修复标题显示问题 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) result_group = QGroupBox("分析结果") result_layout = QVBoxLayout(result_group) # 增加上边距确保标题显示完整(向下移动1/3标签高度) result_layout.setContentsMargins(10, 35, 10, 10) # 上边距增加10px self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) self.result_edit.setStyleSheet(""" font-size: 10pt; /* 缩小结果框字体 */ line-height: 1.5; background-color: white; color: #2c3e50; padding: 15px; border: 1px solid #cccccc; border-radius: 8px; """) result_layout.addWidget(self.result_edit) # 添加面板到分割器 left_layout.addWidget(self.image_group) left_layout.addWidget(control_group) right_layout.addWidget(result_group) splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([500, 900]) # 扩大结果框面积 main_layout.addWidget(splitter) # 状态栏 self.statusBar().setStyleSheet(""" background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #6a11cb, stop:1 #2575fc); color: white; font-weight: bold; padding-left: 10px; """) self.statusBar().showMessage("正在初始化...") def update_timer(self): """更新计时器显示""" self.elapsed_time += 1 hours = self.elapsed_time // 3600 minutes = (self.elapsed_time % 3600) // 60 seconds = self.elapsed_time % 60 self.timer_display.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}") def start_timer(self): """开始计时""" self.elapsed_time = 0 self.timer.start(1000) # 每秒更新一次 self.timer_display.setText("00:00:00") def stop_timer(self): """停止计时""" self.timer.stop() def show_error_dialog(self, title, message): """显示错误对话框""" msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle(title) msg.setText("发生错误") msg.setInformativeText(message) # 添加更多操作按钮 if "Ollama" in title: msg.addButton("打开Ollama网站", QMessageBox.ActionRole) msg.addButton("重试", QMessageBox.ActionRole) msg.addButton(QMessageBox.Ok) msg.setStyleSheet(""" QMessageBox { background-color: #f0f4f8; border: 2px solid #4a86e8; border-radius: 12px; } QLabel { color: #2c3e50; } QPushButton { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4a86e8, stop:1 #6a11cb); color: white; border: none; border-radius: 8px; padding: 8px 16px; min-width: 80px; font-weight: bold; } QPushButton:hover { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5a96f8, stop:1 #7a21db); } """) result = msg.exec_() # 处理自定义按钮点击 if result == QMessageBox.Ok: pass elif msg.clickedButton().text() == "打开Ollama网站": import webbrowser webbrowser.open("https://ollama.com/") elif msg.clickedButton().text() == "重试": self.load_models() def load_models(self): """从Ollama加载可用模型列表""" self.model_list.clear() self.model_list.addItem("正在加载模型...") self.refresh_models_button.setEnabled(False) self.statusBar().showMessage("正在从Ollama获取模型列表...") # 创建并启动模型加载线程 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.finished.connect(self.model_loader_finished) self.model_loader_thread.start() def update_model_list(self, model_list): """更新模型列表控件""" self.model_list.clear() if model_list: # 筛选多模态模型 multimodal_models = [model for model in model_list if "vision" in model.lower() or "llava" in model.lower()] # 添加多模态模型标题 title_item = QListWidgetItem("=== 多模态大模型 ===") # 使用QListWidgetItem self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) # 蓝色 title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) # 不可选择 # 添加多模态模型 for model in multimodal_models: item = QListWidgetItem(f"● {model}") # 使用QListWidgetItem item.setForeground(QColor("#2c3e50")) # 深灰色 item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) # 存储原始模型名 self.model_list.addItem(item) # 添加其他模型标题 other_models = [model for model in model_list if model not in multimodal_models] if other_models: title_item = QListWidgetItem("=== 其他模型 ===") # 使用QListWidgetItem self.model_list.addItem(title_item) title_item.setForeground(QColor("#4a86e8")) # 蓝色 title_item.setFont(QFont("Microsoft YaHei UI", 10, QFont.Bold)) title_item.setFlags(Qt.NoItemFlags) # 不可选择 # 添加其他模型 for model in other_models: item = QListWidgetItem(f"○ {model}") # 使用QListWidgetItem item.setForeground(QColor("#2c3e50")) # 深灰色 item.setFont(QFont("Microsoft YaHei UI", 10)) item.setData(Qt.UserRole, model) # 存储原始模型名 self.model_list.addItem(item) self.statusBar().showMessage(f"已加载 {len(model_list)} 个模型") # 默认选择第一个多模态模型 if multimodal_models: self.model_list.setCurrentRow(1) # 第一项是标题,第二项是第一个模型 else: self.model_list.addItem("未找到可用模型") self.statusBar().showMessage("未找到可用模型") def handle_model_load_error(self, error): """处理模型加载错误""" self.model_list.clear() self.model_list.addItem("加载模型失败") self.statusBar().showMessage(error) # 显示错误对话框 self.show_error_dialog("模型加载错误", f"{error}\n\n" "可能原因:\n" f"1. Ollama服务未启动 (当前地址: {OLLAMA_HOST})\n" "2. Ollama未正确安装\n" "3. 网络连接问题\n\n" "解决方案:\n" "1. 下载并安装Ollama: https://ollama.com/download\n" "2. 启动Ollama服务\n" "3. 检查网络连接") def model_loader_finished(self): """模型加载线程完成""" self.refresh_models_button.setEnabled(True) def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: try: self.image_path = file_path pixmap = QPixmap(file_path) # 检查图片是否有效 if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") # 保存原始图片用于后续处理 self.original_pixmap = pixmap.copy() # 计算缩放尺寸 label_width = self.image_label.width() - 20 label_height = self.image_label.height() - 20 # 保持纵横比缩放 scaled_pixmap = pixmap.scaled( label_width, label_height, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) self.analyze_button.setEnabled(True) self.statusBar().showMessage(f"已加载图片: {os.path.basename(file_path)}") self.progress_bar.setFormat("图片已加载,准备分析") except Exception as e: self.statusBar().showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def analyze_image(self): if not self.image_path: self.statusBar().showMessage("错误: 请先加载图片") return # 检查模型选择是否有效 selected_items = self.model_list.selectedItems() if not selected_items: self.statusBar().showMessage("错误: 请选择模型") return selected_item = selected_items[0] if selected_item.text().startswith("==="): self.statusBar().showMessage("错误: 请选择有效的模型") return # 提取模型名称 model_name = selected_item.data(Qt.UserRole) # 从UserRole获取原始模型名 # 清空当前响应 self.current_response = "" # 开始计时 self.start_timer() # 初始化结果框,显示开始分析的消息 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.result_edit.setHtml(f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'> 图片分析结果 </div> <div style='color: #2c3e50; font-size: 10pt; line-height: 1.6;'> <p style='color: #4a86e8; font-weight: bold;'>正在分析图片,请稍候...</p> </div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 9pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>开始时间:</span> {timestamp} </div> """) 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() # 创建并启动流式分析线程 self.analysis_thread = StreamAnalysisThread( model_name, self.image_path, temperature, max_tokens, prompt ) # 连接信号 self.analysis_thread.chunk_received.connect(self.handle_stream_chunk) # 新增:处理流式数据 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.finished.connect(self.analysis_finished) self.analysis_thread.start() def handle_stream_chunk(self, chunk): """处理流式数据块""" # 累加当前响应 self.current_response += chunk # 格式化当前响应 formatted_result = self.format_result(self.current_response) # 获取模型信息 selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 更新结果框 result_html = f""" <div style='background: linear-gradient(to right, #6a11cb, #2575fc); color: white; font-size: 16pt; font-weight: bold; padding: 12px; border-radius: 8px; margin-bottom: 15px;'> 图片分析结果 </div> <div style='color: #2c3e50; font-size: 10pt; line-height: 1.6;'> {formatted_result} </div> <div style='margin-top: 20px; color: #7f8c8d; font-size: 9pt; border-top: 1px solid #ecf0f1; padding-top: 10px;'> <span style='color: #6a11cb; font-weight: bold;'>模型:</span> {model_name}   <span style='color: #6a11cb; font-weight: bold;'>时间:</span> {timestamp}   <span style='color: #27ae60; font-weight: bold;'>● 流式传输中...</span> </div> """ self.result_edit.setHtml(result_html) # 自动滚动到末尾 cursor = self.result_edit.textCursor() cursor.movePosition(cursor.End) self.result_edit.setTextCursor(cursor) self.result_edit.ensureCursorVisible() def handle_analysis_result(self, result): """处理最终分析结果""" # 停止计时器 self.stop_timer() # 这里不再需要更新结果框,因为流式传输已经实时更新了 # 只需要更新状态信息 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 "未知模型" # 更新底部状态信息,去掉"流式传输中"的提示 current_html = self.result_edit.toHtml() updated_html = current_html.replace( '<span style=\'color: #27ae60; font-weight: bold;\'>● 流式传输中...</span>', f'<span style=\'color: #2ecc71; font-weight: bold;\'>✓ 分析完成 - 用时: {self.timer_display.text()}</span>' ) self.result_edit.setHtml(updated_html) self.statusBar().showMessage("图片分析完成") def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() self.analysis_thread.wait(2000) # 等待2秒 # 停止计时器 self.stop_timer() self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析已停止") self.statusBar().showMessage("分析已停止") def format_result(self, result): """格式化结果文本:分段落显示,确保使用汉字""" # 替换英文标点为中文标点 formatted = result.replace(".", "。").replace(",", ",").replace(":", ":") # 分割为段落 paragraphs = formatted.split("\n\n") # 如果只有一段,尝试按句号分割 if len(paragraphs) == 1: paragraphs = formatted.split("。") # 在每个句号后添加换行(除了最后一个) paragraphs = [p.strip() + ("。" if i < len(paragraphs) - 1 else "") for i, p in enumerate(paragraphs) if p.strip()] # 构建HTML段落 html_paragraphs = [] for i, p in enumerate(paragraphs): if p.strip(): # 添加标题样式到第一个段落 if i == 0: html_paragraphs.append(f"<p style='margin:15px 0; font-size:11pt; font-weight: bold;'>{p}</p>") else: html_paragraphs.append(f"<p style='margin:10px 0; text-indent: 2em;'>{p}</p>") return "".join(html_paragraphs) def handle_analysis_error(self, error): # 停止计时器 self.stop_timer() # 显示错误信息 self.result_edit.setPlainText(f"错误: {error}") self.statusBar().showMessage(f"错误: {error}") self.progress_bar.setFormat("分析失败") # 显示错误对话框 self.show_error_dialog("分析错误", error) def update_progress(self, value): self.progress_bar.setValue(value) # 更新进度条文本 if value < 30: self.progress_bar.setFormat("准备分析... %p%") elif value < 70: self.progress_bar.setFormat("发送请求到Ollama... %p%") elif value < 90: self.progress_bar.setFormat("处理响应... %p%") else: self.progress_bar.setFormat("完成分析... %p%") def analysis_finished(self): self.set_buttons_enabled(True) self.stop_button.setEnabled(False) self.progress_bar.setFormat("分析完成") def set_buttons_enabled(self, enabled): self.load_button.setEnabled(enabled) self.analyze_button.setEnabled(enabled and bool(self.image_path)) self.clear_button.setEnabled(enabled) self.refresh_models_button.setEnabled(enabled) def clear_results(self): self.result_edit.clear() self.image_label.clear() self.image_label.setText("请选择图片进行分析") self.image_path = "" self.analyze_button.setEnabled(False) self.progress_bar.setValue(0) self.progress_bar.setFormat("等待操作...") self.statusBar().showMessage("已清除结果") # 重置计时器 self.stop_timer() self.timer_display.setText("00:00:00") def closeEvent(self, event): # 确保在关闭窗口时停止任何正在运行的线程 if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.quit() if not self.analysis_thread.wait(2000): # 等待2秒 self.analysis_thread.terminate() if self.model_loader_thread and self.model_loader_thread.isRunning(): self.model_loader_thread.quit() if not self.model_loader_thread.wait(2000): # 等待2秒 self.model_loader_thread.terminate() # 停止计时器 self.timer.stop() event.accept() if __name__ == "__main__": # 添加全局异常处理 def exception_handler(exctype, value, traceback): """全局异常处理器""" error_msg = f"程序发生未捕获的异常:\n\n类型: {exctype.__name__}\n\n描述: {value}" print(error_msg) # 尝试显示错误对话框 try: app = QApplication.instance() if app is not None: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("程序遇到严重错误") msg.setInformativeText(error_msg) msg.setWindowTitle("未处理的异常") msg.setStandardButtons(QMessageBox.Ok) msg.exec_() except: pass # 调用默认的异常处理器 sys.__excepthook__(exctype, value, traceback) # 设置全局异常处理器 sys.excepthook = exception_handler app = QApplication(sys.argv) app.setApplicationName("多模态大模型图片解读系统") # 设置应用程序样式 app.setStyle("Fusion") # 创建调色板 - 调整为中性主题 palette = QPalette() palette.setColor(QPalette.Window, QColor(200, 104, 248)) # 浅蓝色背景 palette.setColor(QPalette.WindowText, QColor(44, 62, 20)) # 深灰色文本 palette.setColor(QPalette.Base, QColor(255, 255, 255)) # 白色基础 palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240)) # 浅灰色交替基础色 palette.setColor(QPalette.ToolTipBase, Qt.white) # 工具提示基础 palette.setColor(QPalette.ToolTipText, Qt.black) # 工具提示文本 palette.setColor(QPalette.Text, QColor(44, 62, 20)) # 文本颜色 palette.setColor(QPalette.Button, QColor(74, 134, 232)) # 按钮颜色 (蓝色) palette.setColor(QPalette.ButtonText, Qt.white) # 按钮文本 (白色) palette.setColor(QPalette.BrightText, Qt.red) # 亮文本 palette.setColor(QPalette.Highlight, QColor(106, 17, 203)) # 高亮 (紫色) palette.setColor(QPalette.HighlightedText, Qt.white) # 高亮文本 app.setPalette(palette) try: window = MultiModalApp() window.show() sys.exit(app.exec_()) except Exception as e: print(f"应用程序启动失败: {str(e)}") QMessageBox.critical(None, "启动错误", f"应用程序启动失败:\n{str(e)}")
10-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jarry.liu

如果对您有帮助,鼓励下博主吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值