fix(report_word): Minimax v2 接口兼容 + 缓存防毒化自愈
This commit is contained in:
@ -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__":
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user