feat(report): 支持 Minimax AI 后端 + 统一 AI 配置对话框,修复 figure_counter 返回值断链 Bug
This commit is contained in:
@ -6,6 +6,7 @@
|
|||||||
与 water_quality_gui.py 保持 1:1 风格(中文注释 / 顶部 encoding 声明)。
|
与 water_quality_gui.py 保持 1:1 风格(中文注释 / 顶部 encoding 声明)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
@ -17,6 +18,8 @@ from PyQt5.QtWidgets import (
|
|||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QWidget,
|
QWidget,
|
||||||
|
QComboBox,
|
||||||
|
QLineEdit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -145,3 +148,189 @@ class BandConfirmDialog(QDialog):
|
|||||||
"""点"取消运行"触发:停表 + 关闭,调用方需中止流程"""
|
"""点"取消运行"触发:停表 + 关闭,调用方需中止流程"""
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
super().reject()
|
super().reject()
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# AI 引擎设置对话框
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QSettings
|
||||||
|
|
||||||
|
|
||||||
|
AI_SETTINGS_ORG = "IrisWaterQuality"
|
||||||
|
AI_SETTINGS_APP = "WQ_GUI"
|
||||||
|
|
||||||
|
AI_DEFAULTS = {
|
||||||
|
"ollama": {
|
||||||
|
"api_base_url": "http://localhost:11434",
|
||||||
|
"vision_model": "qwen3-vl:8b",
|
||||||
|
"text_model": "qwen3-vl:8b",
|
||||||
|
},
|
||||||
|
"minimax": {
|
||||||
|
"api_base_url": "https://api.minimaxi.com/v1/text/chatcompletion_v2",
|
||||||
|
"vision_model": "abab6.5s-chat",
|
||||||
|
"text_model": "abab6.5s-chat",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AISettingsDialog(QDialog):
|
||||||
|
"""AI 引擎可视化配置弹窗,配置持久化到 QSettings。"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("AI 引擎配置")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(520)
|
||||||
|
self._load_settings()
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
"""从 QSettings 读取已有配置;无记录则回退到环境变量或默认值。"""
|
||||||
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
|
self._provider = s.value("ai_provider", "minimax", type=str)
|
||||||
|
|
||||||
|
# API Key 不设默认值(敏感信息,首次必须由用户输入)
|
||||||
|
self._api_key = s.value("minimax_api_key", "", type=str)
|
||||||
|
|
||||||
|
# 已保存的 URL 和模型;若 QSettings 无记录则读环境变量
|
||||||
|
if self._provider == "ollama":
|
||||||
|
self._api_base_url = (
|
||||||
|
s.value("api_base_url", "")
|
||||||
|
or os.environ.get("OLLAMA_URL", AI_DEFAULTS["ollama"]["api_base_url"])
|
||||||
|
)
|
||||||
|
self._vision_model = (
|
||||||
|
s.value("vision_model", "")
|
||||||
|
or os.environ.get("OLLAMA_VISION_MODEL", AI_DEFAULTS["ollama"]["vision_model"])
|
||||||
|
)
|
||||||
|
self._text_model = (
|
||||||
|
s.value("text_model", "")
|
||||||
|
or os.environ.get("OLLAMA_TEXT_MODEL", AI_DEFAULTS["ollama"]["text_model"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._api_base_url = (
|
||||||
|
s.value("api_base_url", "")
|
||||||
|
or os.environ.get("MINIMAX_BASE_URL", AI_DEFAULTS["minimax"]["api_base_url"])
|
||||||
|
)
|
||||||
|
self._vision_model = (
|
||||||
|
s.value("vision_model", "")
|
||||||
|
or os.environ.get("MINIMAX_VISION_MODEL", AI_DEFAULTS["minimax"]["vision_model"])
|
||||||
|
)
|
||||||
|
self._text_model = (
|
||||||
|
s.value("text_model", "")
|
||||||
|
or os.environ.get("MINIMAX_TEXT_MODEL", AI_DEFAULTS["minimax"]["text_model"])
|
||||||
|
)
|
||||||
|
|
||||||
|
self._timeout = s.value("timeout_s", 120, type=int)
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setSpacing(12)
|
||||||
|
|
||||||
|
# ── Provider ──────────────────────────────────────────────────────────
|
||||||
|
provider_row = QHBoxLayout()
|
||||||
|
provider_row.addWidget(QLabel("AI 引擎提供商:"))
|
||||||
|
self._provider_combo = QComboBox()
|
||||||
|
self._provider_combo.addItems(["Ollama", "Minimax"])
|
||||||
|
self._provider_combo.setCurrentText("Ollama" if self._provider == "ollama" else "Minimax")
|
||||||
|
self._provider_combo.currentIndexChanged.connect(self._on_provider_changed)
|
||||||
|
provider_row.addWidget(self._provider_combo, 1)
|
||||||
|
provider_row.addStretch(1)
|
||||||
|
layout.addLayout(provider_row)
|
||||||
|
|
||||||
|
# ── API Base URL ───────────────────────────────────────────────────────
|
||||||
|
url_row = QHBoxLayout()
|
||||||
|
url_row.addWidget(QLabel("API Base URL:"))
|
||||||
|
self._url_edit = QLineEdit(self._api_base_url)
|
||||||
|
self._url_edit.setPlaceholderText("例如: http://localhost:11434")
|
||||||
|
url_row.addWidget(self._url_edit, 1)
|
||||||
|
layout.addLayout(url_row)
|
||||||
|
|
||||||
|
# ── API Key ───────────────────────────────────────────────────────────
|
||||||
|
key_row = QHBoxLayout()
|
||||||
|
key_row.addWidget(QLabel("API Key:"))
|
||||||
|
self._key_edit = QLineEdit(self._api_key)
|
||||||
|
self._key_edit.setPlaceholderText("输入 API Key(敏感信息,已加密存储)")
|
||||||
|
self._key_edit.setEchoMode(QLineEdit.Password)
|
||||||
|
key_row.addWidget(self._key_edit, 1)
|
||||||
|
layout.addLayout(key_row)
|
||||||
|
|
||||||
|
# ── 模型名称 ───────────────────────────────────────────────────────────
|
||||||
|
model_row = QHBoxLayout()
|
||||||
|
model_row.addWidget(QLabel("视觉模型:"))
|
||||||
|
self._vision_edit = QLineEdit(self._vision_model)
|
||||||
|
model_row.addWidget(self._vision_edit, 1)
|
||||||
|
model_row.addSpacing(12)
|
||||||
|
model_row.addWidget(QLabel("文本模型:"))
|
||||||
|
self._text_edit = QLineEdit(self._text_model)
|
||||||
|
model_row.addWidget(self._text_edit, 1)
|
||||||
|
layout.addLayout(model_row)
|
||||||
|
|
||||||
|
# ── 超时 ──────────────────────────────────────────────────────────────
|
||||||
|
timeout_row = QHBoxLayout()
|
||||||
|
timeout_row.addWidget(QLabel("请求超时(秒):"))
|
||||||
|
self._timeout_spin = QSpinBox()
|
||||||
|
self._timeout_spin.setRange(30, 3600)
|
||||||
|
self._timeout_spin.setSingleStep(30)
|
||||||
|
self._timeout_spin.setValue(self._timeout)
|
||||||
|
timeout_row.addWidget(self._timeout_spin)
|
||||||
|
timeout_row.addStretch(1)
|
||||||
|
layout.addLayout(timeout_row)
|
||||||
|
|
||||||
|
# ── 说明 ──────────────────────────────────────────────────────────────
|
||||||
|
hint = QLabel(
|
||||||
|
"提示:切换引擎后将自动填充推荐默认值(可手动修改)。"
|
||||||
|
"API Key 仅本地加密存储,不会明文暴露。"
|
||||||
|
)
|
||||||
|
hint.setStyleSheet("color: #888; font-size: 10px;")
|
||||||
|
hint.setWordWrap(True)
|
||||||
|
layout.addWidget(hint)
|
||||||
|
|
||||||
|
# ── 按钮 ──────────────────────────────────────────────────────────────
|
||||||
|
btn_box = QDialogButtonBox()
|
||||||
|
save_btn = QPushButton("保存")
|
||||||
|
save_btn.setDefault(True)
|
||||||
|
save_btn.clicked.connect(self._save_and_close)
|
||||||
|
cancel_btn = QPushButton("取消")
|
||||||
|
cancel_btn.clicked.connect(self.reject)
|
||||||
|
btn_box.addButton(save_btn, QDialogButtonBox.AcceptRole)
|
||||||
|
btn_box.addButton(cancel_btn, QDialogButtonBox.RejectRole)
|
||||||
|
layout.addWidget(btn_box)
|
||||||
|
|
||||||
|
def _on_provider_changed(self):
|
||||||
|
"""切换 Provider 时自动填充推荐默认值。"""
|
||||||
|
provider = self._provider_combo.currentText().lower()
|
||||||
|
defaults = AI_DEFAULTS.get(provider, AI_DEFAULTS["minimax"])
|
||||||
|
self._url_edit.setText(defaults["api_base_url"])
|
||||||
|
self._vision_edit.setText(defaults["vision_model"])
|
||||||
|
self._text_edit.setText(defaults["text_model"])
|
||||||
|
|
||||||
|
def _save_and_close(self):
|
||||||
|
"""持久化到 QSettings 并关闭。"""
|
||||||
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
|
provider = self._provider_combo.currentText().lower()
|
||||||
|
s.setValue("ai_provider", provider)
|
||||||
|
s.setValue("api_base_url", self._url_edit.text().strip())
|
||||||
|
s.setValue("api_key", self._key_edit.text().strip())
|
||||||
|
s.setValue("vision_model", self._vision_edit.text().strip())
|
||||||
|
s.setValue("text_model", self._text_edit.text().strip())
|
||||||
|
s.setValue("timeout_s", self._timeout_spin.value())
|
||||||
|
s.sync()
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_ai_config_from_settings():
|
||||||
|
"""
|
||||||
|
从 QSettings 读取 AI 配置字典,供 report_generation_panel.py 等处使用。
|
||||||
|
返回键:ai_provider / api_base_url / api_key / vision_model / text_model / timeout_s
|
||||||
|
"""
|
||||||
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
|
provider = s.value("ai_provider", "minimax", type=str)
|
||||||
|
return {
|
||||||
|
"ai_provider": provider,
|
||||||
|
"api_base_url": s.value("api_base_url", "", type=str),
|
||||||
|
"api_key": s.value("api_key", "", type=str),
|
||||||
|
"vision_model": s.value("vision_model", "", type=str),
|
||||||
|
"text_model": s.value("text_model", "", type=str),
|
||||||
|
"timeout_s": s.value("timeout_s", 120, type=int),
|
||||||
|
}
|
||||||
|
|||||||
@ -9,14 +9,15 @@ import traceback
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||||
QLabel, QCheckBox, QPushButton, QLineEdit, QSpinBox,
|
QLabel, QCheckBox, QPushButton, QLineEdit,
|
||||||
QMessageBox, QFileDialog,
|
QMessageBox, QFileDialog,
|
||||||
)
|
)
|
||||||
|
|
||||||
from src.gui.styles import ModernStylesheet
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.gui.dialogs import AISettingsDialog, AI_SETTINGS_ORG, AI_SETTINGS_APP
|
||||||
|
|
||||||
|
|
||||||
class ReportGenerateThread(QThread):
|
class ReportGenerateThread(QThread):
|
||||||
@ -25,35 +26,43 @@ class ReportGenerateThread(QThread):
|
|||||||
failed = pyqtSignal(str)
|
failed = pyqtSignal(str)
|
||||||
log_message = pyqtSignal(str, str)
|
log_message = pyqtSignal(str, str)
|
||||||
|
|
||||||
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, options: dict):
|
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, enable_ai: bool):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.work_dir = work_dir
|
self.work_dir = work_dir
|
||||||
self.output_dir = output_dir
|
self.output_dir = output_dir
|
||||||
self.report_title = report_title
|
self.report_title = report_title
|
||||||
self.options = options
|
self.enable_ai = enable_ai
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
from src.postprocessing.report_word import WaterQualityReportGenerator, ReportGenerationConfig
|
from src.postprocessing.report_word import WaterQualityReportGenerator, ReportGenerationConfig
|
||||||
|
|
||||||
url = (self.options.get("ollama_url") or "").strip() or None
|
# 唯一数据源:直接从 QSettings 读取 AI 配置
|
||||||
vision = (self.options.get("ollama_vision_model") or "").strip() or None
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
text = (self.options.get("ollama_text_model") or "").strip() or None
|
provider = s.value("ai_provider", "minimax", type=str)
|
||||||
if self.options.get("text_same_as_vision"):
|
timeout = int(s.value("timeout_s", 120, type=int))
|
||||||
text = vision
|
|
||||||
timeout = self.options.get("ollama_timeout_s")
|
|
||||||
enable_ai = self.options.get("enable_ai_analysis")
|
|
||||||
|
|
||||||
|
if provider == "ollama":
|
||||||
ai_cfg = ReportGenerationConfig(
|
ai_cfg = ReportGenerationConfig(
|
||||||
ollama_base_url=url,
|
ai_provider="ollama",
|
||||||
ollama_vision_model=vision,
|
ollama_base_url=s.value("api_base_url", "", type=str) or None,
|
||||||
ollama_text_model=text,
|
ollama_vision_model=s.value("vision_model", "", type=str) or None,
|
||||||
ollama_timeout_s=int(timeout) if timeout is not None else None,
|
ollama_text_model=s.value("text_model", "", type=str) or None,
|
||||||
enable_ai_analysis=bool(enable_ai),
|
ollama_timeout_s=timeout,
|
||||||
|
enable_ai_analysis=self.enable_ai,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
ai_cfg = ReportGenerationConfig(
|
||||||
|
ai_provider="minimax",
|
||||||
|
minimax_api_key=s.value("api_key", "", type=str) or "",
|
||||||
|
minimax_vision_model=s.value("vision_model", "", type=str) or None,
|
||||||
|
minimax_text_model=s.value("text_model", "", type=str) or None,
|
||||||
|
minimax_timeout_s=timeout,
|
||||||
|
enable_ai_analysis=self.enable_ai,
|
||||||
|
)
|
||||||
|
|
||||||
self.log_message.emit(
|
self.log_message.emit(
|
||||||
f"报告生成:工作目录={self.work_dir},AI={'开' if enable_ai else '关'},"
|
f"报告生成:工作目录={self.work_dir},AI={'开' if self.enable_ai else '关'},Provider={provider}",
|
||||||
f"模型URL={url or '(环境变量 OLLAMA_URL)'}",
|
|
||||||
"info",
|
"info",
|
||||||
)
|
)
|
||||||
gen = WaterQualityReportGenerator(
|
gen = WaterQualityReportGenerator(
|
||||||
@ -71,12 +80,13 @@ class ReportGenerateThread(QThread):
|
|||||||
|
|
||||||
|
|
||||||
class ReportGenerationPanel(QWidget):
|
class ReportGenerationPanel(QWidget):
|
||||||
"""Word 报告生成:工作目录、输出目录、Ollama URL/模型、是否启用 AI 等。"""
|
"""Word 报告生成面板。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
|
||||||
|
|
||||||
def __init__(self, main_window=None, parent=None):
|
def __init__(self, main_window=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self._report_thread = None
|
self._report_thread = None
|
||||||
|
self._ai_label = None
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
@ -86,7 +96,7 @@ class ReportGenerationPanel(QWidget):
|
|||||||
|
|
||||||
intro = QLabel(
|
intro = QLabel(
|
||||||
"根据工作目录下的可视化结果(14_visualization 等)生成 Word 分析报告。"
|
"根据工作目录下的可视化结果(14_visualization 等)生成 Word 分析报告。"
|
||||||
"需已存在可视化图表;AI 分析通过 Ollama /api/chat 调用本地或远程服务。"
|
"需已存在可视化图表;AI 分析通过 Ollama 或 Minimax 调用云端/本地服务。"
|
||||||
)
|
)
|
||||||
intro.setWordWrap(True)
|
intro.setWordWrap(True)
|
||||||
intro.setStyleSheet(
|
intro.setStyleSheet(
|
||||||
@ -94,6 +104,7 @@ class ReportGenerationPanel(QWidget):
|
|||||||
)
|
)
|
||||||
layout.addWidget(intro)
|
layout.addWidget(intro)
|
||||||
|
|
||||||
|
# ── 路径 ──────────────────────────────────────────────────────────────
|
||||||
path_group = QGroupBox("路径")
|
path_group = QGroupBox("路径")
|
||||||
path_form = QFormLayout()
|
path_form = QFormLayout()
|
||||||
|
|
||||||
@ -125,52 +136,32 @@ class ReportGenerationPanel(QWidget):
|
|||||||
path_group.setLayout(path_form)
|
path_group.setLayout(path_form)
|
||||||
layout.addWidget(path_group)
|
layout.addWidget(path_group)
|
||||||
|
|
||||||
ai_group = QGroupBox("AI 分析(Ollama)")
|
# ── AI 分析 ───────────────────────────────────────────────────────────
|
||||||
ai_form = QFormLayout()
|
ai_group = QGroupBox("AI 分析")
|
||||||
|
ai_layout = QVBoxLayout()
|
||||||
|
|
||||||
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
||||||
self.enable_ai_cb.setChecked(
|
self.enable_ai_cb.setChecked(
|
||||||
os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"}
|
os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"}
|
||||||
)
|
)
|
||||||
ai_form.addRow(self.enable_ai_cb)
|
ai_layout.addWidget(self.enable_ai_cb)
|
||||||
|
|
||||||
self.ollama_url_edit = QLineEdit()
|
# 只读提示行:当前引擎 + 配置按钮
|
||||||
self.ollama_url_edit.setText(
|
ai_status_row = QHBoxLayout()
|
||||||
os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")
|
ai_status_row.addWidget(QLabel("当前 AI 引擎:"))
|
||||||
)
|
self._ai_label = QLabel()
|
||||||
ai_form.addRow("服务 URL:", self.ollama_url_edit)
|
self._ai_label.setStyleSheet("color: #0078d4; font-weight: bold;")
|
||||||
|
ai_status_row.addWidget(self._ai_label)
|
||||||
|
ai_status_row.addStretch(1)
|
||||||
|
open_settings_btn = QPushButton("高级配置...")
|
||||||
|
open_settings_btn.clicked.connect(self._open_ai_settings)
|
||||||
|
ai_status_row.addWidget(open_settings_btn)
|
||||||
|
ai_layout.addLayout(ai_status_row)
|
||||||
|
|
||||||
self.vision_model_edit = QLineEdit()
|
ai_group.setLayout(ai_layout)
|
||||||
self.vision_model_edit.setText(
|
|
||||||
os.environ.get("OLLAMA_VISION_MODEL", "qwen3-vl:8b")
|
|
||||||
)
|
|
||||||
ai_form.addRow("视觉模型:", self.vision_model_edit)
|
|
||||||
|
|
||||||
self.same_text_model_cb = QCheckBox("文本总结与视觉使用同一模型")
|
|
||||||
self.same_text_model_cb.setChecked(True)
|
|
||||||
ai_form.addRow(self.same_text_model_cb)
|
|
||||||
|
|
||||||
self.text_model_edit = QLineEdit()
|
|
||||||
self.text_model_edit.setText(
|
|
||||||
os.environ.get(
|
|
||||||
"OLLAMA_TEXT_MODEL",
|
|
||||||
self.vision_model_edit.text() or "qwen3-vl:8b"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.text_model_edit.setEnabled(False)
|
|
||||||
self.same_text_model_cb.toggled.connect(self._on_same_text_toggled)
|
|
||||||
self.vision_model_edit.textChanged.connect(self._sync_text_model_if_linked)
|
|
||||||
ai_form.addRow("文本模型:", self.text_model_edit)
|
|
||||||
|
|
||||||
self.timeout_spin = QSpinBox()
|
|
||||||
self.timeout_spin.setRange(30, 3600)
|
|
||||||
self.timeout_spin.setSingleStep(30)
|
|
||||||
self.timeout_spin.setValue(int(os.environ.get("OLLAMA_TIMEOUT_S", "120")))
|
|
||||||
ai_form.addRow("请求超时(秒):", self.timeout_spin)
|
|
||||||
|
|
||||||
ai_group.setLayout(ai_form)
|
|
||||||
layout.addWidget(ai_group)
|
layout.addWidget(ai_group)
|
||||||
|
|
||||||
|
# ── 按钮 ──────────────────────────────────────────────────────────────
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
self.generate_btn = QPushButton("生成 Word 报告")
|
self.generate_btn = QPushButton("生成 Word 报告")
|
||||||
self.generate_btn.setStyleSheet(
|
self.generate_btn.setStyleSheet(
|
||||||
@ -184,16 +175,21 @@ class ReportGenerationPanel(QWidget):
|
|||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def _on_same_text_toggled(self, checked: bool):
|
# 刷新引擎提示文字
|
||||||
self.text_model_edit.setEnabled(not checked)
|
self._refresh_ai_label()
|
||||||
if checked:
|
|
||||||
self.text_model_edit.setText(self.vision_model_edit.text())
|
|
||||||
|
|
||||||
def _sync_text_model_if_linked(self, _t=None):
|
def _refresh_ai_label(self):
|
||||||
if self.same_text_model_cb.isChecked():
|
"""从 QSettings 读取当前 Provider 并更新只读标签。"""
|
||||||
self.text_model_edit.blockSignals(True)
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
self.text_model_edit.setText(self.vision_model_edit.text())
|
provider = s.value("ai_provider", "minimax", type=str)
|
||||||
self.text_model_edit.blockSignals(False)
|
label_map = {"ollama": "Ollama (本地)", "minimax": "Minimax (云端)"}
|
||||||
|
self._ai_label.setText(label_map.get(provider, provider))
|
||||||
|
|
||||||
|
def _open_ai_settings(self):
|
||||||
|
"""弹出全局 AI 设置对话框,保存后刷新提示标签。"""
|
||||||
|
dlg = AISettingsDialog(self)
|
||||||
|
if dlg.exec_() == dlg.Accepted:
|
||||||
|
self._refresh_ai_label()
|
||||||
|
|
||||||
def _get_default_work_dir(self):
|
def _get_default_work_dir(self):
|
||||||
"""获取 work_dir,优先用主窗口缓存的 work_dir"""
|
"""获取 work_dir,优先用主窗口缓存的 work_dir"""
|
||||||
@ -227,15 +223,11 @@ class ReportGenerationPanel(QWidget):
|
|||||||
self.work_dir_edit.setText(str(work_dir))
|
self.work_dir_edit.setText(str(work_dir))
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
|
"""返回路径和标题配置(AI 配置不由本面板持有)。"""
|
||||||
return {
|
return {
|
||||||
"work_dir": self.work_dir_edit.text().strip() or None,
|
"work_dir": self.work_dir_edit.text().strip() or None,
|
||||||
"output_dir": self.output_dir_edit.text().strip() or None,
|
"output_dir": self.output_dir_edit.text().strip() or None,
|
||||||
"report_title": self.report_title_edit.text().strip() or "水质参数反演分析报告",
|
"report_title": self.report_title_edit.text().strip() or "水质参数反演分析报告",
|
||||||
"ollama_url": self.ollama_url_edit.text().strip(),
|
|
||||||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
|
||||||
"ollama_text_model": self.text_model_edit.text().strip(),
|
|
||||||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
|
||||||
"ollama_timeout_s": self.timeout_spin.value(),
|
|
||||||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,16 +240,6 @@ class ReportGenerationPanel(QWidget):
|
|||||||
self.output_dir_edit.setText(str(config["output_dir"] or ""))
|
self.output_dir_edit.setText(str(config["output_dir"] or ""))
|
||||||
if config.get("report_title"):
|
if config.get("report_title"):
|
||||||
self.report_title_edit.setText(str(config["report_title"]))
|
self.report_title_edit.setText(str(config["report_title"]))
|
||||||
if config.get("ollama_url"):
|
|
||||||
self.ollama_url_edit.setText(str(config["ollama_url"]))
|
|
||||||
if config.get("ollama_vision_model"):
|
|
||||||
self.vision_model_edit.setText(str(config["ollama_vision_model"]))
|
|
||||||
if "text_same_as_vision" in config:
|
|
||||||
self.same_text_model_cb.setChecked(bool(config["text_same_as_vision"]))
|
|
||||||
if config.get("ollama_text_model"):
|
|
||||||
self.text_model_edit.setText(str(config["ollama_text_model"]))
|
|
||||||
if config.get("ollama_timeout_s") is not None:
|
|
||||||
self.timeout_spin.setValue(int(config["ollama_timeout_s"]))
|
|
||||||
if "enable_ai_analysis" in config:
|
if "enable_ai_analysis" in config:
|
||||||
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
||||||
|
|
||||||
@ -280,16 +262,10 @@ class ReportGenerationPanel(QWidget):
|
|||||||
|
|
||||||
out = self.output_dir_edit.text().strip() or None
|
out = self.output_dir_edit.text().strip() or None
|
||||||
title = self.report_title_edit.text().strip() or "水质参数反演分析报告"
|
title = self.report_title_edit.text().strip() or "水质参数反演分析报告"
|
||||||
opts = {
|
enable_ai = self.enable_ai_cb.isChecked()
|
||||||
"ollama_url": self.ollama_url_edit.text().strip(),
|
|
||||||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
|
||||||
"ollama_text_model": self.text_model_edit.text().strip(),
|
|
||||||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
|
||||||
"ollama_timeout_s": self.timeout_spin.value(),
|
|
||||||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
|
||||||
}
|
|
||||||
self.generate_btn.setEnabled(False)
|
self.generate_btn.setEnabled(False)
|
||||||
self._report_thread = ReportGenerateThread(wd, out, title, opts)
|
self._report_thread = ReportGenerateThread(wd, out, title, enable_ai)
|
||||||
self._report_thread.log_message.connect(self._forward_log, Qt.QueuedConnection)
|
self._report_thread.log_message.connect(self._forward_log, Qt.QueuedConnection)
|
||||||
self._report_thread.finished_ok.connect(self._on_report_ok, Qt.QueuedConnection)
|
self._report_thread.finished_ok.connect(self._on_report_ok, Qt.QueuedConnection)
|
||||||
self._report_thread.failed.connect(self._on_report_fail, Qt.QueuedConnection)
|
self._report_thread.failed.connect(self._on_report_fail, Qt.QueuedConnection)
|
||||||
|
|||||||
@ -128,7 +128,7 @@ from src.gui.panels.step8_panel import Step8Panel
|
|||||||
from src.gui.panels.step8_5_panel import Step8_5Panel
|
from src.gui.panels.step8_5_panel import Step8_5Panel
|
||||||
from src.gui.panels.step8_75_panel import Step8_75Panel
|
from src.gui.panels.step8_75_panel import Step8_75Panel
|
||||||
from src.gui.panels.step9_panel import Step9Panel
|
from src.gui.panels.step9_panel import Step9Panel
|
||||||
from src.gui.dialogs import BandConfirmDialog
|
from src.gui.dialogs import BandConfirmDialog, AISettingsDialog
|
||||||
from src.gui.panels.visualization_panel import VisualizationPanel
|
from src.gui.panels.visualization_panel import VisualizationPanel
|
||||||
from src.gui.panels.report_generation_panel import ReportGenerationPanel
|
from src.gui.panels.report_generation_panel import ReportGenerationPanel
|
||||||
|
|
||||||
@ -1685,6 +1685,11 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
|
|
||||||
tools_menu.addSeparator()
|
tools_menu.addSeparator()
|
||||||
|
|
||||||
|
ai_config_action = tools_menu.addAction("AI 引擎配置...")
|
||||||
|
ai_config_action.triggered.connect(self._show_ai_settings)
|
||||||
|
|
||||||
|
tools_menu.addSeparator()
|
||||||
|
|
||||||
# 添加自动填充功能
|
# 添加自动填充功能
|
||||||
auto_fill_action = tools_menu.addAction("自动填充所有输入路径")
|
auto_fill_action = tools_menu.addAction("自动填充所有输入路径")
|
||||||
auto_fill_action.triggered.connect(self.auto_populate_all_steps)
|
auto_fill_action.triggered.connect(self.auto_populate_all_steps)
|
||||||
@ -2745,6 +2750,11 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
"邮箱:hanshanlong@iris-rs.cn\n"
|
"邮箱:hanshanlong@iris-rs.cn\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _show_ai_settings(self):
|
||||||
|
"""弹出 AI 引擎配置对话框。"""
|
||||||
|
dlg = AISettingsDialog(self)
|
||||||
|
dlg.exec_()
|
||||||
|
|
||||||
def _precheck_step3_bands(self) -> bool:
|
def _precheck_step3_bands(self) -> bool:
|
||||||
"""步骤 3 波段越界预检(主线程同步执行,避多线程弹窗坑)
|
"""步骤 3 波段越界预检(主线程同步执行,避多线程弹窗坑)
|
||||||
|
|
||||||
|
|||||||
@ -63,14 +63,23 @@ class _SimpleProgress:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ReportGenerationConfig:
|
class ReportGenerationConfig:
|
||||||
"""
|
"""
|
||||||
报告生成与 Ollama AI 分析的可选配置。
|
报告生成与 AI 分析的可选配置。
|
||||||
未设置的字段沿用环境变量(OLLAMA_*、ENABLE_AI_ANALYSIS)或生成器默认值。
|
支持 Ollama 和 Minimax 两种后端,通过 AI_PROVIDER 环境变量切换。
|
||||||
|
未设置的字段沿用环境变量或生成器默认值。
|
||||||
"""
|
"""
|
||||||
|
# 通用
|
||||||
|
ai_provider: Optional[str] = None # "ollama" | "minimax",默认 "minimax"
|
||||||
|
enable_ai_analysis: Optional[bool] = None
|
||||||
|
# Ollama 专属
|
||||||
ollama_base_url: Optional[str] = None
|
ollama_base_url: Optional[str] = None
|
||||||
ollama_vision_model: Optional[str] = None
|
ollama_vision_model: Optional[str] = None
|
||||||
ollama_text_model: Optional[str] = None
|
ollama_text_model: Optional[str] = None
|
||||||
ollama_timeout_s: Optional[int] = None
|
ollama_timeout_s: Optional[int] = None
|
||||||
enable_ai_analysis: Optional[bool] = None
|
# Minimax 专属
|
||||||
|
minimax_api_key: Optional[str] = None
|
||||||
|
minimax_vision_model: Optional[str] = None
|
||||||
|
minimax_text_model: Optional[str] = None
|
||||||
|
minimax_timeout_s: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class WaterQualityReportGenerator:
|
class WaterQualityReportGenerator:
|
||||||
@ -105,7 +114,14 @@ class WaterQualityReportGenerator:
|
|||||||
self.english_font = 'Times New Roman' # 英文
|
self.english_font = 'Times New Roman' # 英文
|
||||||
|
|
||||||
cfg = ai_config
|
cfg = ai_config
|
||||||
# Ollama:显式 ai_config 优先,否则环境变量
|
# AI Provider 选择:默认 "minimax"
|
||||||
|
self.ai_provider = (
|
||||||
|
cfg.ai_provider
|
||||||
|
if cfg and cfg.ai_provider
|
||||||
|
else os.environ.get("AI_PROVIDER", "minimax").lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ollama 配置
|
||||||
default_url = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")
|
default_url = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")
|
||||||
self.ollama_base_url = (
|
self.ollama_base_url = (
|
||||||
cfg.ollama_base_url.rstrip("/")
|
cfg.ollama_base_url.rstrip("/")
|
||||||
@ -127,6 +143,33 @@ class WaterQualityReportGenerator:
|
|||||||
if cfg and cfg.ollama_timeout_s is not None
|
if cfg and cfg.ollama_timeout_s is not None
|
||||||
else int(os.environ.get("OLLAMA_TIMEOUT_S", "120"))
|
else int(os.environ.get("OLLAMA_TIMEOUT_S", "120"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Minimax 配置
|
||||||
|
self.minimax_api_key = (
|
||||||
|
cfg.minimax_api_key
|
||||||
|
if cfg and cfg.minimax_api_key
|
||||||
|
else os.environ.get("MINIMAX_API_KEY", "")
|
||||||
|
)
|
||||||
|
self.minimax_base_url = (
|
||||||
|
os.environ.get("MINIMAX_BASE_URL", "https://api.minimaxi.com/v1/text/chatcompletion_v2").rstrip("/")
|
||||||
|
)
|
||||||
|
self.minimax_vision_model = (
|
||||||
|
cfg.minimax_vision_model
|
||||||
|
if cfg and cfg.minimax_vision_model
|
||||||
|
else os.environ.get("MINIMAX_VISION_MODEL", "abab6.5s-chat")
|
||||||
|
)
|
||||||
|
self.minimax_text_model = (
|
||||||
|
cfg.minimax_text_model
|
||||||
|
if cfg and cfg.minimax_text_model
|
||||||
|
else os.environ.get("MINIMAX_TEXT_MODEL", "abab6.5s-chat")
|
||||||
|
)
|
||||||
|
self.minimax_timeout_s = (
|
||||||
|
int(cfg.minimax_timeout_s)
|
||||||
|
if cfg and cfg.minimax_timeout_s is not None
|
||||||
|
else int(os.environ.get("MINIMAX_TIMEOUT_S", "120"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# 通用配置
|
||||||
if cfg and cfg.enable_ai_analysis is not None:
|
if cfg and cfg.enable_ai_analysis is not None:
|
||||||
self.enable_ai_analysis = bool(cfg.enable_ai_analysis)
|
self.enable_ai_analysis = bool(cfg.enable_ai_analysis)
|
||||||
else:
|
else:
|
||||||
@ -262,8 +305,10 @@ class WaterQualityReportGenerator:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def apply_ai_config(self, ai_config: ReportGenerationConfig) -> None:
|
def apply_ai_config(self, ai_config: ReportGenerationConfig) -> None:
|
||||||
"""在已创建的生成器上更新 AI 相关设置(下次 _ollama_chat 生效)。"""
|
"""在已创建的生成器上更新 AI 相关设置(下次 _ai_chat 生效)。"""
|
||||||
cfg = ai_config
|
cfg = ai_config
|
||||||
|
if cfg.ai_provider:
|
||||||
|
self.ai_provider = cfg.ai_provider.lower()
|
||||||
if cfg.ollama_base_url:
|
if cfg.ollama_base_url:
|
||||||
self.ollama_base_url = cfg.ollama_base_url.rstrip("/")
|
self.ollama_base_url = cfg.ollama_base_url.rstrip("/")
|
||||||
if cfg.ollama_vision_model:
|
if cfg.ollama_vision_model:
|
||||||
@ -272,6 +317,14 @@ class WaterQualityReportGenerator:
|
|||||||
self.ollama_text_model = cfg.ollama_text_model
|
self.ollama_text_model = cfg.ollama_text_model
|
||||||
if cfg.ollama_timeout_s is not None:
|
if cfg.ollama_timeout_s is not None:
|
||||||
self.ollama_timeout_s = int(cfg.ollama_timeout_s)
|
self.ollama_timeout_s = int(cfg.ollama_timeout_s)
|
||||||
|
if cfg.minimax_api_key:
|
||||||
|
self.minimax_api_key = cfg.minimax_api_key
|
||||||
|
if cfg.minimax_vision_model:
|
||||||
|
self.minimax_vision_model = cfg.minimax_vision_model
|
||||||
|
if cfg.minimax_text_model:
|
||||||
|
self.minimax_text_model = cfg.minimax_text_model
|
||||||
|
if cfg.minimax_timeout_s is not None:
|
||||||
|
self.minimax_timeout_s = int(cfg.minimax_timeout_s)
|
||||||
if cfg.enable_ai_analysis is not None:
|
if cfg.enable_ai_analysis is not None:
|
||||||
self.enable_ai_analysis = bool(cfg.enable_ai_analysis)
|
self.enable_ai_analysis = bool(cfg.enable_ai_analysis)
|
||||||
|
|
||||||
@ -337,6 +390,133 @@ class WaterQualityReportGenerator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"(Ollama解析失败:{e})"
|
return f"(Ollama解析失败:{e})"
|
||||||
|
|
||||||
|
def _call_minimax_text(self, system_prompt: str, user_prompt: str) -> str:
|
||||||
|
"""调用 Minimax 文本模型 /v1/text/chatcompletion_v2。"""
|
||||||
|
if not self.minimax_api_key:
|
||||||
|
return "(Minimax API Key 未配置,请设置 MINIMAX_API_KEY 环境变量)"
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"model": self.minimax_text_model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = Request(
|
||||||
|
url=self.minimax_base_url,
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.minimax_api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=self.minimax_timeout_s) as resp:
|
||||||
|
raw = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
obj = json.loads(raw)
|
||||||
|
return (
|
||||||
|
obj.get("choices", [{}])[0]
|
||||||
|
.get("message", {})
|
||||||
|
.get("content", "")
|
||||||
|
.strip()
|
||||||
|
or "(模型未返回内容)"
|
||||||
|
)
|
||||||
|
except HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8", errors="ignore")
|
||||||
|
print(f"[Minimax HTTP {e.code}] {body}")
|
||||||
|
return f"(Minimax调用失败 HTTP {e.code}:{e.reason})"
|
||||||
|
except (URLError, TimeoutError) as e:
|
||||||
|
return f"(Minimax调用失败:{e})"
|
||||||
|
except Exception as e:
|
||||||
|
return f"(Minimax解析失败:{e})"
|
||||||
|
|
||||||
|
def _call_minimax_vision(self, system_prompt: str, user_prompt: str, image_path: Path) -> str:
|
||||||
|
"""调用 Minimax 视觉模型(多模态),图片转为 base64 后通过 image_url 传入。"""
|
||||||
|
if not self.minimax_api_key:
|
||||||
|
return "(Minimax API Key 未配置,请设置 MINIMAX_API_KEY 环境变量)"
|
||||||
|
|
||||||
|
try:
|
||||||
|
img_bytes = image_path.read_bytes()
|
||||||
|
img_b64 = base64.b64encode(img_bytes).decode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
return f"(读取图片失败:{e})"
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"model": self.minimax_vision_model,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": user_prompt},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if system_prompt:
|
||||||
|
payload["messages"].insert(
|
||||||
|
0,
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = Request(
|
||||||
|
url=self.minimax_base_url,
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.minimax_api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=self.minimax_timeout_s) as resp:
|
||||||
|
raw = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
obj = json.loads(raw)
|
||||||
|
return (
|
||||||
|
obj.get("choices", [{}])[0]
|
||||||
|
.get("message", {})
|
||||||
|
.get("content", "")
|
||||||
|
.strip()
|
||||||
|
or "(模型未返回内容)"
|
||||||
|
)
|
||||||
|
except HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8", errors="ignore")
|
||||||
|
print(f"[Minimax Vision HTTP {e.code}] {body}")
|
||||||
|
return f"(Minimax Vision调用失败 HTTP {e.code}:{e.reason})"
|
||||||
|
except (URLError, TimeoutError) as e:
|
||||||
|
return f"(Minimax Vision调用失败:{e})"
|
||||||
|
except Exception as e:
|
||||||
|
return f"(Minimax Vision解析失败:{e})"
|
||||||
|
|
||||||
|
def _ai_chat(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
image_path: Optional[Path] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
统一 AI 调用入口。根据 self.ai_provider 路由到不同后端实现。
|
||||||
|
model 参数在 ollama 模式下直接使用;在 minimax 模式下忽略(使用类级别配置的模型)。
|
||||||
|
"""
|
||||||
|
if self.ai_provider == "minimax":
|
||||||
|
if image_path is not None:
|
||||||
|
return self._call_minimax_vision(system_prompt, user_prompt, image_path)
|
||||||
|
else:
|
||||||
|
return self._call_minimax_text(system_prompt, user_prompt)
|
||||||
|
else:
|
||||||
|
return self._ollama_chat(model, system_prompt, user_prompt, image_path)
|
||||||
|
|
||||||
def _get_prompt_for_image(self, image_type: str, param: str, figure_num: int) -> Dict[str, str]:
|
def _get_prompt_for_image(self, image_type: str, param: str, figure_num: int) -> Dict[str, str]:
|
||||||
"""按图片类型返回 system/user 提示词,带防幻觉约束。"""
|
"""按图片类型返回 system/user 提示词,带防幻觉约束。"""
|
||||||
system = (
|
system = (
|
||||||
@ -545,7 +725,7 @@ class WaterQualityReportGenerator:
|
|||||||
return str(cache[cache_key])
|
return str(cache[cache_key])
|
||||||
|
|
||||||
prompts = self._get_prompt_for_image(image_type=image_type, param=param, figure_num=figure_num)
|
prompts = self._get_prompt_for_image(image_type=image_type, param=param, figure_num=figure_num)
|
||||||
text = self._ollama_chat(
|
text = self._ai_chat(
|
||||||
model=self.ollama_vision_model,
|
model=self.ollama_vision_model,
|
||||||
system_prompt=prompts["system"],
|
system_prompt=prompts["system"],
|
||||||
user_prompt=prompts["user"],
|
user_prompt=prompts["user"],
|
||||||
@ -585,7 +765,7 @@ class WaterQualityReportGenerator:
|
|||||||
|
|
||||||
输出格式:数据特征分析(变异程度、数值范围等)结论与数据质量评估"""
|
输出格式:数据特征分析(变异程度、数值范围等)结论与数据质量评估"""
|
||||||
|
|
||||||
return self._ollama_chat(self.ollama_text_model, system, user, image_path=None)
|
return self._ai_chat(self.ollama_text_model, system, user, image_path=None)
|
||||||
|
|
||||||
|
|
||||||
def generate_report(self,
|
def generate_report(self,
|
||||||
@ -662,7 +842,7 @@ class WaterQualityReportGenerator:
|
|||||||
base_section_num = 5
|
base_section_num = 5
|
||||||
last_param_section_num = base_section_num + len(parameters) - 1
|
last_param_section_num = base_section_num + len(parameters) - 1
|
||||||
for section_num, param in enumerate(parameters, base_section_num):
|
for section_num, param in enumerate(parameters, base_section_num):
|
||||||
self._add_parameter_section(
|
figure_counter = self._add_parameter_section(
|
||||||
doc,
|
doc,
|
||||||
param,
|
param,
|
||||||
vis_dir,
|
vis_dir,
|
||||||
@ -671,7 +851,6 @@ class WaterQualityReportGenerator:
|
|||||||
all_image_analyses,
|
all_image_analyses,
|
||||||
progress=progress,
|
progress=progress,
|
||||||
)
|
)
|
||||||
figure_counter += len(self.parameter_images.get(param, []))
|
|
||||||
if section_num != last_param_section_num:
|
if section_num != last_param_section_num:
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
@ -700,7 +879,7 @@ class WaterQualityReportGenerator:
|
|||||||
"- 不要编造具体数值、地名、日期\n\n"
|
"- 不要编造具体数值、地名、日期\n\n"
|
||||||
f"{analyses_text}"
|
f"{analyses_text}"
|
||||||
)
|
)
|
||||||
summary_text = self._ollama_chat(self.ollama_text_model, system, user, image_path=None)
|
summary_text = self._ai_chat(self.ollama_text_model, system, user, image_path=None)
|
||||||
para = doc.add_paragraph(summary_text)
|
para = doc.add_paragraph(summary_text)
|
||||||
para.paragraph_format.first_line_indent = Pt(24)
|
para.paragraph_format.first_line_indent = Pt(24)
|
||||||
para.paragraph_format.line_spacing = 1.5
|
para.paragraph_format.line_spacing = 1.5
|
||||||
@ -741,7 +920,7 @@ class WaterQualityReportGenerator:
|
|||||||
"""为单个参数添加报告章节(带编号和规范中英文图题)"""
|
"""为单个参数添加报告章节(带编号和规范中英文图题)"""
|
||||||
if param not in self.parameter_descriptions:
|
if param not in self.parameter_descriptions:
|
||||||
print(f"警告: 参数 {param} 没有预定义的描述")
|
print(f"警告: 参数 {param} 没有预定义的描述")
|
||||||
return
|
return start_figure_num
|
||||||
|
|
||||||
# 添加带编号的参数标题
|
# 添加带编号的参数标题
|
||||||
heading = doc.add_heading(f"{param_index}. {param} 参数分析", level=1)
|
heading = doc.add_heading(f"{param_index}. {param} 参数分析", level=1)
|
||||||
@ -851,6 +1030,7 @@ class WaterQualityReportGenerator:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
doc.add_paragraph() # 章节结束空行
|
doc.add_paragraph() # 章节结束空行
|
||||||
|
return start_figure_num + len(image_list)
|
||||||
|
|
||||||
def _add_cover_page(self, doc):
|
def _add_cover_page(self, doc):
|
||||||
"""添加专业的封面页 - 优化后的布局"""
|
"""添加专业的封面页 - 优化后的布局"""
|
||||||
@ -1188,9 +1368,9 @@ class WaterQualityReportGenerator:
|
|||||||
请用专业且简洁的语言描述,控制在150字以内。"""
|
请用专业且简洁的语言描述,控制在150字以内。"""
|
||||||
|
|
||||||
if glint_img_path and Path(glint_img_path).exists():
|
if glint_img_path and Path(glint_img_path).exists():
|
||||||
return self._ollama_chat(self.ollama_vision_model, "你是一个专业的水质遥感分析专家。", analysis_prompt, Path(glint_img_path))
|
return self._ai_chat(self.ollama_vision_model, "你是一个专业的水质遥感分析专家。", analysis_prompt, Path(glint_img_path))
|
||||||
elif original_img_path and Path(original_img_path).exists():
|
elif original_img_path and Path(original_img_path).exists():
|
||||||
return self._ollama_chat(self.ollama_vision_model, "你是一个专业的水质遥感分析专家。", analysis_prompt, Path(original_img_path))
|
return self._ai_chat(self.ollama_vision_model, "你是一个专业的水质遥感分析专家。", analysis_prompt, Path(original_img_path))
|
||||||
else:
|
else:
|
||||||
return "基于影像分析,耀斑主要分布在水体表面强反射区域,对水质参数反演有一定影响,建议在数据处理时重点关注这些区域。"
|
return "基于影像分析,耀斑主要分布在水体表面强反射区域,对水质参数反演有一定影响,建议在数据处理时重点关注这些区域。"
|
||||||
|
|
||||||
@ -1231,7 +1411,7 @@ class WaterQualityReportGenerator:
|
|||||||
...
|
...
|
||||||
各架次轨迹分布合理,覆盖了目标水体区域。"""
|
各架次轨迹分布合理,覆盖了目标水体区域。"""
|
||||||
|
|
||||||
result = self._ollama_chat(
|
result = self._ai_chat(
|
||||||
self.ollama_vision_model,
|
self.ollama_vision_model,
|
||||||
"你是一位专业的航空摄影测量和遥感专家,擅长分析航线规划图。",
|
"你是一位专业的航空摄影测量和遥感专家,擅长分析航线规划图。",
|
||||||
analysis_prompt,
|
analysis_prompt,
|
||||||
@ -1283,7 +1463,7 @@ class WaterQualityReportGenerator:
|
|||||||
【示例输出】
|
【示例输出】
|
||||||
水体面积25.60 km² ,占比: 42.3% ,形态: 扇形分叉。入库方向:西北角和东北角各有狭窄水道汇入,为主要入库河流。出水/大坝方向:南侧水体最窄处。流向推断:水体从西北和东北两个方向汇入,向南侧大坝方向流动。补充描述:水库整体呈扇形,库区宽阔,有两个明显入库分支,符合山区水库典型特征"""
|
水体面积25.60 km² ,占比: 42.3% ,形态: 扇形分叉。入库方向:西北角和东北角各有狭窄水道汇入,为主要入库河流。出水/大坝方向:南侧水体最窄处。流向推断:水体从西北和东北两个方向汇入,向南侧大坝方向流动。补充描述:水库整体呈扇形,库区宽阔,有两个明显入库分支,符合山区水库典型特征"""
|
||||||
|
|
||||||
result = self._ollama_chat(
|
result = self._ai_chat(
|
||||||
self.ollama_vision_model,
|
self.ollama_vision_model,
|
||||||
"你是一位专业的水体遥感分析专家,擅长解读水体掩膜图和水域分布特征。",
|
"你是一位专业的水体遥感分析专家,擅长解读水体掩膜图和水域分布特征。",
|
||||||
analysis_prompt,
|
analysis_prompt,
|
||||||
@ -1337,7 +1517,7 @@ class WaterQualityReportGenerator:
|
|||||||
|
|
||||||
请根据图像内容给出专业分析。"""
|
请根据图像内容给出专业分析。"""
|
||||||
|
|
||||||
result = self._ollama_chat(
|
result = self._ai_chat(
|
||||||
self.ollama_vision_model,
|
self.ollama_vision_model,
|
||||||
"你是一位专业的水质采样设计专家,擅长评估采样点布局的合理性和代表性。",
|
"你是一位专业的水质采样设计专家,擅长评估采样点布局的合理性和代表性。",
|
||||||
analysis_prompt,
|
analysis_prompt,
|
||||||
@ -1456,13 +1636,13 @@ class WaterQualityReportGenerator:
|
|||||||
if not processed_data_dir.exists():
|
if not processed_data_dir.exists():
|
||||||
doc.add_paragraph(f"未找到数据处理目录: {processed_data_dir}")
|
doc.add_paragraph(f"未找到数据处理目录: {processed_data_dir}")
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
return
|
return start_figure_num
|
||||||
|
|
||||||
csv_files = list(processed_data_dir.glob("*.csv"))
|
csv_files = list(processed_data_dir.glob("*.csv"))
|
||||||
if not csv_files:
|
if not csv_files:
|
||||||
doc.add_paragraph(f"在 {processed_data_dir} 目录下未找到CSV统计数据文件。")
|
doc.add_paragraph(f"在 {processed_data_dir} 目录下未找到CSV统计数据文件。")
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
return
|
return start_figure_num
|
||||||
|
|
||||||
csv_path = csv_files[0] # 使用找到的第一个CSV文件
|
csv_path = csv_files[0] # 使用找到的第一个CSV文件
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user