fix(report_word): Minimax v2 接口兼容 + 缓存防毒化自愈

This commit is contained in:
DXC
2026-06-25 16:45:50 +08:00
parent 43f50ec07b
commit d327c6c267

View File

@ -33,6 +33,10 @@ from docx.oxml import OxmlElement
from docx import Document
from docx.shared import Inches, Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_SECTION
from docx.oxml.ns import qn
@ -229,10 +233,13 @@ class WaterQualityReportGenerator:
self._output_dir_is_default = output_dir is None
f"{param}_distribution_enhanced.png"
if output_dir is None:
self.output_dir = self.visualization_dir
else:
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
@ -320,10 +327,14 @@ class WaterQualityReportGenerator:
else os.environ.get("MINIMAX_API_KEY", "")
"""调用 Minimax 文本模型 /v1/text/chatcompletion_v2。"""
)
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
@ -332,10 +343,9 @@ class WaterQualityReportGenerator:
else os.environ.get("MINIMAX_VISION_MODEL", "abab6.5s-chat")
)
url=self.minimax_base_url,
self.minimax_text_model = (
cfg.minimax_text_model
@ -348,13 +358,27 @@ class WaterQualityReportGenerator:
self.minimax_timeout_s = (
int(cfg.minimax_timeout_s)
return (
obj.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
.strip()
or "(模型未返回内容)"
)
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:
self.enable_ai_analysis = bool(cfg.enable_ai_analysis)
else:
self.enable_ai_analysis = os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {
"0",
"false",
"False",
@ -363,9 +387,8 @@ class WaterQualityReportGenerator:
self.ai_cache_path = self.output_dir / "ollama_image_analyses_cache.json"
"""调用 Minimax 视觉模型(多模态),图片转为 base64 后通过 image_url 传入。"""
# 各参数的专业描述(完整版)
self.parameter_descriptions = {
@ -375,6 +398,10 @@ class WaterQualityReportGenerator:
"COD": """化学需氧量(COD)是衡量水体中有机污染物含量的综合指标,反映单位体积水体中还原性物质(主要是有机物)被氧化所消耗的氧化剂总量。COD值越高,表明水体受有机污染越严重。高COD会加剧溶解氧消耗,导致水体缺氧、水生生物死亡,甚至引发黑臭现象。COD也是污水处理效果和污染物排放管控的关键考核指标,其时空分布可为污染源识别与治理提供直接依据。""",
"DO": """溶解氧(DO)是维持水生生态系统健康的基础物质,指溶解在水中的分子态氧。其浓度受水温、盐度、藻类光合作用及有机物耗氧过程共同调控。DO低于一定阈值会导致水生生物窒息、底泥营养盐释放及水体自净能力下降。DO的实时监测与空间分布反演,对判断水体污染程度、预警鱼类死亡事件及评估生态修复成效具有重要价值。""",
@ -400,7 +427,7 @@ class WaterQualityReportGenerator:
"Cl-": """氯离子(Cl-)是天然水体中最稳定存在的阴离子之一,其来源包括岩石风化、海水侵入、工业废水及生活污水。氯离子含量升高可指示水体受咸潮或污染输入的影响,且在高浓度下会腐蚀管道、影响农业土壤结构。在饮用水消毒过程中,氯离子与有机物可能生成三氯甲烷等消毒副产物,因此其监测对水厂运行和水安全有重要警示作用。""",
url=self.minimax_base_url,
"NO3-N": """硝酸盐氮(NO3-N)是氮循环中氧化程度最高的形态,易溶于水,常通过农田径流、化粪池渗漏或工业废水进入水体。过量硝酸盐会刺激藻类过度生长,加速水体富营养化;饮用水中硝酸盐氮浓度超标会引发“蓝婴症”(高铁血红蛋白血症),对婴幼儿健康构成威胁。因此,硝酸盐氮是水质评价与饮用水安全监管的重点指标。""",
@ -413,13 +440,27 @@ class WaterQualityReportGenerator:
"BGA": """BGA(蓝绿藻,即蓝藻)是表征水体蓝藻生物量的关键生物参数,通常通过藻蓝蛋白等特征色素反演获得。蓝藻过量繁殖(水华)会释放藻毒素、消耗溶解氧、形成水面覆盖层,严重威胁饮用水安全和水生态系统健康。BGA浓度的空间分布能精准指示水华高发区域与迁移路径,是水华预警、蓝藻治理和生态修复措施制定不可或缺的输入信息。""",
return (
obj.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
.strip()
or "(模型未返回内容)"
)
"TT": """总氮(TT)是水体中有机氮、氨氮、硝酸盐氮、亚硝酸盐氮等各种形态氮的总和,综合反映了水体的氮营养水平。总氮是导致水体富营养化的主要限制因子之一,其浓度过高会引发藻类爆发、透明度下降、水质恶化。总氮的时空变化趋势可用于判断流域面源污染强度、评估氮减排措施成效,是水质管理和流域水环境保护的关键参考指标。"""
}
# 每个参数对应的图片顺序(统一5张图模式)
params_list = ["Chlorophyll", "COD", "DO", "PH", "Temperature",
"spCond", "Turbidity", "TDS", "Cl-", "NO3-N",
"NH3-N", "BGA", "TT"]
self.parameter_images = {
param: [
f"{param}_histogram.png",
f"{param}_spectrum_comparison.png",
f"{param}_scatter_with_confidence.png",
@ -428,7 +469,6 @@ class WaterQualityReportGenerator:
f"{param}_distribution_rendered.png" # 适配新版渲染分布图
] for param in params_list
}
@ -611,28 +651,42 @@ class WaterQualityReportGenerator:
return f"(Ollama解析失败:{e})"
"""分析单张图片并缓存,失败则返回可展示的提示文本。"""
def _call_minimax_text(self, system_prompt: str, user_prompt: str) -> str:
"""调用 Minimax 文本模型(自动兼容 OpenAI 标准端点)"""
if not self.minimax_api_key:
return "(Minimax API Key 未配置,请设置 MINIMAX_API_KEY 环境变量)"
cache = self._load_ai_cache()
cache_key = f"{image_path.name}::{image_path.stat().st_mtime_ns}::{self.ollama_vision_model}::{image_type}"
if cache_key in cache:
return str(cache[cache_key])
url = self.minimax_base_url
if "chatcompletion_v2" in url:
url = "https://api.minimax.chat/v1/chat/completions"
payload: Dict[str, Any] = {
"model": self.minimax_text_model,
"max_tokens": 4096,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = Request(
model=self.ollama_vision_model,
url=url,
data=data,
headers={
"Authorization": f"Bearer {self.minimax_api_key}",
"Content-Type": "application/json",
cache[cache_key] = text
self._save_ai_cache(cache)
return text
},
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)
if "base_resp" in obj and obj["base_resp"].get("status_code", 0) != 0:
return f"(Minimax API 拒绝请求:{obj['base_resp'].get('status_msg')})"
if "error" in obj:
err = obj["error"]
@ -828,102 +882,103 @@ class WaterQualityReportGenerator:
"analysis": (
self,
doc,
param: str,
vis_dir: Path,
param_index: int = 1,
start_figure_num: int = 1,
all_image_analyses: Optional[List[Dict[str, Any]]] = None,
progress=None,
"分析要点:\n"
f"- 结合 {param} 的固有光学特性,重点分析400-900nm区间内的特征波段响应(如吸收谷、反射峰、双峰效应等)。\n"
"""为单个参数添加报告章节(带编号和规范中英文图题)"""
"- 对比不同浓度组别的光谱差异,说明浓度变化是如何改变水体对光吸收和后向散射规律的。\n"
print(f"警告: 参数 {param} 没有预定义的描述")
"- 指出对该参数反演最具区分度的关键波段区间,验证模型的物理可解释性。"
),
"conclusion": "结论应聚焦:浓度梯度引起的光谱响应规律及其对应的光学机制验证。",
# 添加参数描述
},
# 设置首行缩进两个字符(中文排版规范)
# 设置正文样式:宋体小四,1.5倍行距
"scatter_with_confidence": {
"analysis": (
"分析要点:\n"
# 添加图片 - 支持子文件夹结构 + 中英文图题
"- 评估机器学习反演模型在该参数上的鲁棒性。点云对1:1线的贴合度反映了反演精度。\n"
"- 重点分析在极低值区或极高值区是否存在系统性高估/低估(这是水色遥感的常见难点,如高浓度下的光谱饱和效应)。\n"
"histogram": "直方图",
"spectrum_comparison": "光谱对比图",
"scatter_with_confidence": "模型散点图",
"boxplot": "箱型图",
"distribution": "分布图"
"- 结合置信带宽度,说明模型在不同浓度区间的预测不确定性。"
),
"conclusion": "结论应聚焦:反演模型的整体精度表现、局限性及可靠的浓度预测区间。",
},
"boxplot": {
"analysis": (
# 选择子文件夹与动态寻址
"分析要点:\n"
"- 结合中位数和四分位距,分析不同类别(或区域)间水质差异的显著性。\n"
img_path = vis_dir / img_name
"- 解释离散程度大(箱体长)可能代表的强烈时空异质性。\n"
"- 指出箱线图上下的离群点,探讨其作为局部水质突变信号的价值。"
if not img_path.exists():
img_path = vis_dir / img_name
),
"conclusion": "结论应聚焦:核心对比趋势及数据整体的时空变异特征。",
img_path = vis_dir / img_name
},
"distribution": {
"analysis": (
# 颜色地图由预测步骤生成,开启全盘指索
search_dirs = [
vis_dir.parent / "9_water_quality_prediction",
vis_dir.parent / "11_12_13_predictions",
vis_dir.parent / "9_Concentration" / "charts",
vis_dir.parent / "9_Concentration",
vis_dir
]
"分析要点:\n"
f"- 分析 {param} 高值区与低值区的空间异质性特征。\n"
candidates = list(s_dir.glob(f"*{param}*.png")) + list(s_dir.glob(f"*{param}*.jpg"))
# 剔除掉属于其他类型的图
candidates = [c for c in candidates if not any(x in c.name.lower() for x in ("scatter", "histogram", "spectrum", "boxplot", "preview"))]
"- 推断污染/物质来源类型:高值区呈斑块状/点状(通常提示点源排放或局部水华),还是呈沿岸带状/梯度扩散(通常提示面源径流或水动力扩散)。\n"
"- 结合常见水动力学特征,简述物质可能的输移趋势。"
),
"conclusion": "结论应聚焦:水质参数的空间格局特征及其指示的宏观环境动力学过程。",
img_path = vis_dir / img_name
},
"correlation_heatmap": {
"analysis": (
"分析要点:\n"
"- 挖掘关键水质参数间的生物地球化学联系。如叶绿素与总氮/总磷的正相关提示营养盐驱动,与浊度的正相关提示藻类为主导的悬浮物等。\n"
"- 识别拮抗作用(强负相关),并解释其潜在的生化机制(如高浊度遮蔽光照导致叶绿素降低)。\n"
"- 基于相关性聚类,推断水体中的核心主导污染因子群。"
),
"conclusion": "结论应聚焦:水质指标间的核心协同/拮抗机制及水环境的主要驱动力。",
},
}
default_spec = {
"analysis": "结合水环境遥感原理,深入解读图中展现的数据分布或空间格局特征。",
# 使用统一的图像插入方法(中文图题)
"conclusion": "结论应聚焦:该图表传递的核心水质遥感科学结论。",
}
# 添加英文图题:宋体小四(与中文图题一致)
@ -933,24 +988,17 @@ class WaterQualityReportGenerator:
user = (
# AI 分析:插入在图题之后
f"图号:图{figure_num}\n"
image_path=img_path,
image_type=title_key,
param=param,
figure_num=figure_num,
f"当前分析参数:{param}\n"
f"图表类型:{image_type}\n\n"
all_image_analyses.append(
{
"figure_num": figure_num,
"param": param,
"image_type": title_key,
"image_name": img_name,
"analysis": analysis_text,
}
)
"【专业要求】:\n"
f"{spec['analysis']}\n\n"
"【输出格式】:\n"
"直接输出一段(不要分段)150~300字的专业分析。前半部分描述关键数据现象并深挖其光学或生态机制,最后用一句“总之,…”作为全文的科学性总结。\n"
@ -959,14 +1007,13 @@ class WaterQualityReportGenerator:
)
return {"system": system, "user": user}
# 每处理完一张图(无论成功/失败)更新进度条
doc.add_paragraph() # 章节结束空行
def _style_figure_caption_simsun_xiaosi(self, paragraph):
"""图题格式:宋体、小四(12pt),中英文均设 eastAsia 为宋体。"""
@ -1241,7 +1288,7 @@ class WaterQualityReportGenerator:
on_progress=None) -> str:
"""
deglint_img_path = vis_dir / "glint_deglint_previews" / "deglint_deglint_goodman_preview.png"
生成 Word 报告 - 所有数据均来自工作目录(work_dir)
可视化图片、统计数据等均从 work_dir/14_visualization 和 work_dir/4_processed_data 中读取
@ -1734,7 +1781,7 @@ class WaterQualityReportGenerator:
run.font.size = Pt(36) # 增大标题字体
run._element.rPr.rFonts.set(qn('w:eastAsia'), self.title_font)
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff']
# 3. 公司名称和日期 - 紧挨着放在底部图片上方
@ -1787,3 +1834,4 @@ if __name__ == "__main__":