feat(new-arch):主窗口全功能增强(图标系统 + 全链路参数同步 + 服务输出统一解析 + Step12 分类浏览)

1. main_view.py:图标系统 + 全链路参数自动传导
   - 新增 _res() 解析项目根的相对路径,PyInstaller 打包后兼容 sys._MEIPASS。
   - 新增 QListWidgetItem / QMessageBox 导入,左侧导航列表支持右键菜单 + 错误弹窗。
   - ROUTES 12 条全部新增 icon 字段("1.png" 等),侧边栏显示业务图标。
   - 新增 step_outputs 缓存机制:每个 step 完成后把 output_path 写入 self.step_outputs。
   - 新增 _sync_dependencies() 同步函数 + _safe_set_config() 包装器,
     按依赖图把上游产物推给下游 view:
       step1 → step6 water_mask_path
       step3 → step4 / step6 / step10 deglint_img_path / bsq_path
       step4 → step9 sampling_csv_path
       step5 → step6 csv_path
       step6 → step7 / step8 training_csv_path
       step8 → step9 models_dir(父目录)
       step9 → step11 prediction_csv_dir / prediction_csv_path(双推)
       step10 → step11 geotiff_dir / geotiff_path(双推)

2. services/step1-13:统一输出解析器集成
   - 新增 src/new/services/_output_resolver.py,提供 resolve_output_dir /
     copy_to_user_path / get_user_output_path / is_user_specified 四个共享工具。
   - 每个 service 把原有的私有 _resolve_xxx_dir 改为调用 resolve_output_dir,
     强制执行"用户优先"规则(用户指定 output_path 时用其父目录,否则用 work_dir/<subdir>)。
   - 用户指定文件名 vs 底层硬编码文件名的"事后劫持"通过 copy_to_user_path 完成
     (覆盖 step2、step4、step7、step8 等底层 step 不接受 output_path 关键字的步骤)。

3. views/step12_view.py:恢复 ImageCategoryTree + ImageViewerWidget 高级组件
   - 删掉精简版占位 Label,挂回旧版的 ImageCategoryTree(按"模型评估/光谱分析/
     统计图表/处理结果/含量分布图"五类自动归类工作目录下的图像文件)。
   - 挂回 ImageViewerWidget(滚轮缩放 0.1x-5x + 50ms 防抖 + FastTransformation/
     SmoothTransformation 智能切换 + Ctrl+Wheel + 工具栏)。
   - 扫描按钮接通 image_tree.scan_directory(),选中节点即时加载到 image_viewer。
   - 按钮样式切换为 ModernStylesheet(success/primary)统一视觉。
This commit is contained in:
DXC
2026-06-17 13:28:58 +08:00
parent 9cb3c8ed0d
commit 6a962f5e8f
15 changed files with 607 additions and 116 deletions

View File

@ -38,6 +38,8 @@ from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_waterindex_csv(formula_csv_path: Optional[str], work_dir: str) -> str:
"""解析 waterindex.csv 路径(与 WaterIndexProcessor.__init__ 默认逻辑保持一致)"""
@ -70,11 +72,19 @@ def _resolve_water_mask_path(water_mask_path: Optional[str], work_dir: str) -> O
return None
def _resolve_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
"""根据 output_dir / work_dir 计算水色指数反演结果输出目录"""
if output_dir:
return Path(output_dir)
return Path(work_dir) / "10_WaterIndex_Images"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算水色指数反演结果输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
(step10 的 output_dir 本身就是一个目录),否则用 work_dir/10_WaterIndex_Images 默认。
注意:step10 与其他步骤不同——output_dir 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_dir", "output_path")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "10_WaterIndex_Images", "default"
def execute_step10(config: Dict[str, Any]) -> Dict[str, Any]:
@ -97,7 +107,7 @@ def execute_step10(config: Dict[str, Any]) -> Dict[str, Any]:
enabled: bool = bool(config.get("enabled", True))
work_dir: str = config.get("work_dir") or "."
output_path = _resolve_output_dir(output_dir, work_dir)
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "watercolor_inversion"
# ---------- 提前失败检查 ----------

View File

@ -51,12 +51,22 @@ from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
"""根据 output_dir / work_dir 计算专题图输出目录"""
if output_dir:
return Path(output_dir)
return Path(work_dir) / "11_Thematic_Map"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算专题图输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
(step11 的 output_dir 本身就是一个目录),否则用 work_dir/11_Thematic_Map 默认。
注意:step11 与其他步骤不同——output_dir 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_dir", "output_path")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "11_Thematic_Map", "default"
def _resolve_csv_paths(config: Dict[str, Any], work_dir: str) -> List[Path]:
@ -209,7 +219,7 @@ def execute_step11(config: Dict[str, Any]) -> Dict[str, Any]:
output_dir: str = config.get("output_dir") or ""
work_dir: str = config.get("work_dir") or "."
output_path = _resolve_output_dir(output_dir, work_dir)
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "make_thematic_map"
# ---------- 提前失败检查 ----------

View File

@ -48,11 +48,22 @@ from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_dir: str | None, work_dir: str) -> Path:
if output_dir:
return Path(output_dir)
return Path(work_dir) / "14_visualization"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算可视化输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
(step12 的 output_dir 本身就是一个目录),否则用 work_dir/14_visualization 默认。
注意:step12 与其他步骤不同——output_dir 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_dir", "output_path")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "14_visualization", "default"
def _resolve_models_dir(work_dir: str, models_dir: str | None) -> str:
@ -170,7 +181,7 @@ def execute_step12(config: Dict[str, Any]) -> Dict[str, Any]:
gen_glint = bool(config.get("generate_glint_previews", True))
gen_sampling = bool(config.get("generate_sampling_maps", True))
output_path = _resolve_output_dir(output_dir, work_dir)
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "viz_generate"
# ---------- 提前失败检查 ----------

View File

@ -53,12 +53,22 @@ import os
from pathlib import Path
from typing import Any, Dict, Optional
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
"""根据 output_dir / work_dir 计算 Word 报告输出目录"""
if output_dir:
return Path(output_dir)
return Path(work_dir) / "14_visualization"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算 Word 报告输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
(step13 的 output_dir 本身就是一个目录),否则用 work_dir/14_visualization 默认。
注意:step13 与其他步骤不同——output_dir 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_dir", "output_path")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "14_visualization", "default"
def _apply_ai_env(config: Dict[str, Any]) -> None:
@ -117,7 +127,7 @@ def execute_step13(config: Dict[str, Any]) -> Dict[str, Any]:
).strip()
enabled: bool = bool(config.get("enabled", True))
output_path = _resolve_output_dir(output_dir, work_dir)
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "generate_word_report"
# ---------- 提前失败检查 ----------

View File

@ -35,6 +35,7 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.water_mask_step import WaterMaskStep
from src.new.services._output_resolver import get_user_output_path, is_user_specified
def _resolve_mode(config: Dict[str, Any]) -> str:

View File

@ -39,13 +39,21 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.glint_detection_step import GlintDetectionStep
from src.new.services._output_resolver import (
copy_to_user_path,
get_user_output_path,
is_user_specified,
resolve_output_dir,
)
def _resolve_glint_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算耀斑检测输出目录"""
if output_path:
return Path(output_path).parent
return Path(work_dir) / "2_Glint_Detection"
def _resolve_glint_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算耀斑检测输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
否则用 work_dir/2_Glint_Detection 默认。
"""
return resolve_output_dir(config, work_dir, "2_Glint_Detection", "output_path", "output_dir")
def _clean_int_param(value: Any, default: int = 0) -> int | None:
@ -77,7 +85,7 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
glint_dir = _resolve_glint_dir(output_path, work_dir)
glint_dir, _source = _resolve_glint_dir(config, work_dir)
# ---------- 提前失败检查 ----------
if not enabled:
@ -103,13 +111,14 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
}
# ---------- 构建底层 kwargs(可选字段:None 一律不传,让底层走默认) ----------
# 注意:GlintDetectionStep.run 不接受 output_path 关键字——它只接收 glint_dir;
# 用户指定的文件名将通过下文的 copy_to_user_path 事后劫持拷贝。
kwargs: Dict[str, Any] = {
"img_path": img_path,
"glint_wave": glint_wave,
"method": method,
"water_mask_path": water_mask_path,
"glint_dir": glint_dir,
"output_path": output_path,
"callback": None, # 日志由 main_view 统一接管
}
@ -162,6 +171,15 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
"mode": method,
}
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
# 旧版 GlintDetectionStep.run 只接受 glint_dir 不接受确切文件名,
# 所以用户浏览时输入的 output_path 在算法内部被忽略;这里事后把
# 硬编码的 result_path 拷贝/重命名到 user_path。
user_path = config.get("output_path")
if user_path:
result_path = copy_to_user_path(result_path, user_path)
p = Path(result_path) # 同步刷新 p 给最后的 return 用
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),

View File

@ -43,14 +43,23 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.glint_removal_step import GlintRemovalStep
from src.new.services._output_resolver import (
copy_to_user_path,
get_user_output_path,
is_user_specified,
resolve_output_dir,
)
def _resolve_dirs(output_path: str | None, work_dir: str) -> tuple[Path, Path]:
"""根据 output_path / work_dir 计算 (deglint_dir, water_mask_dir)"""
if output_path:
deglint_dir = Path(output_path).parent
else:
deglint_dir = Path(work_dir) / "3_Deglint"
def _resolve_dirs(config: Dict[str, Any], work_dir: str) -> tuple[Path, Path]:
"""根据 output_path / work_dir 计算 (deglint_dir, water_mask_dir)
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
否则用 work_dir/3_Deglint 默认。
"""
deglint_dir, _source = resolve_output_dir(
config, work_dir, "3_Deglint", "output_path", "output_dir"
)
water_mask_dir = Path(work_dir) / "1_water_mask"
return deglint_dir, water_mask_dir
@ -124,7 +133,7 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
deglint_dir, water_mask_dir = _resolve_dirs(output_path, work_dir)
deglint_dir, water_mask_dir = _resolve_dirs(config, work_dir)
# ---------- 提前失败检查 ----------
if not enabled:
@ -150,6 +159,8 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
}
# ---------- 构建底层 kwargs ----------
# 注意:GlintRemovalStep.run 不接受 output_path 关键字——它只接收 deglint_dir;
# 用户指定的文件名将通过下文的 copy_to_user_path 事后劫持拷贝。
method_kwargs = _build_method_kwargs(method, config)
kwargs: Dict[str, Any] = {
"img_path": img_path,
@ -159,7 +170,6 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
"interpolation_method": interpolation_method,
"deglint_dir": deglint_dir,
"water_mask_dir": water_mask_dir,
"output_path": output_path,
"callback": None, # 日志由 main_view 统一接管
}
kwargs.update(method_kwargs)
@ -199,6 +209,16 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
"mode": method,
}
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
# 旧版 GlintRemovalStep.run 只接受 deglint_dir 不接受确切文件名;
# 用户浏览指定的 .bsq 文件名被底层忽略(同时 .hdr 头文件也按硬编码名生成)。
# 这里事后把 result_path 拷贝/重命名到 user_path,copy_to_user_path
# 会自动处理 .hdr / .HDR 伴随文件。
user_path = config.get("output_path")
if user_path:
result_path = copy_to_user_path(result_path, user_path)
p = Path(result_path)
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),

View File

@ -38,13 +38,21 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.prediction_step import PredictionStep
from src.new.services._output_resolver import (
copy_to_user_path,
get_user_output_path,
is_user_specified,
resolve_output_dir,
)
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算采样点输出目录"""
if output_path:
return Path(output_path).parent
return Path(work_dir) / "4_Sampling"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算采样点输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
否则用 work_dir/4_Sampling 默认。
"""
return resolve_output_dir(config, work_dir, "4_Sampling", "output_path", "output_dir")
def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
@ -67,7 +75,7 @@ def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_path, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "adaptive" if use_adaptive_sampling else "fixed"
# ---------- 提前失败检查 ----------
@ -138,6 +146,14 @@ def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
"mode": mode,
}
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
# PredictionStep.generate_sampling_points 内部按 sampling_spectra.csv 硬编码;
# 用户浏览指定的文件名在算法内部被忽略。
user_path = config.get("output_path")
if user_path:
result_path = copy_to_user_path(result_path, user_path)
p = Path(result_path)
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),

View File

@ -33,13 +33,21 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.data_preparation_step import DataPreparationStep
from src.new.services._output_resolver import (
copy_to_user_path,
get_user_output_path,
is_user_specified,
resolve_output_dir,
)
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算清洗后 CSV 输出目录"""
if output_path:
return Path(output_path).parent
return Path(work_dir) / "5_Data_Cleaning"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算清洗后 CSV 输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
否则用 work_dir/5_Data_Cleaning 默认。
"""
return resolve_output_dir(config, work_dir, "5_Data_Cleaning", "output_path", "output_dir")
def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
@ -57,7 +65,7 @@ def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_path, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "csv_clean"
# ---------- 提前失败检查 ----------
@ -128,6 +136,14 @@ def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
"mode": mode,
}
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
# process_csv 内部硬编码输出文件名 processed_data.csv,
# 用户浏览指定的文件名(无论是 .csv 还是别的格式)被底层忽略。
user_path = config.get("output_path")
if user_path:
result_path = copy_to_user_path(result_path, user_path)
p = Path(result_path)
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),

View File

@ -39,13 +39,23 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.data_preparation_step import DataPreparationStep
from src.new.services._output_resolver import (
copy_to_user_path,
get_user_output_path,
is_user_specified,
resolve_output_dir,
)
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算 training_spectra.csv 输出目录"""
if output_path:
return Path(output_path).parent
return Path(work_dir) / "6_Spectral_Feature_Extraction"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算 training_spectra.csv 输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
否则用 work_dir/6_Spectral_Feature_Extraction 默认。
"""
return resolve_output_dir(
config, work_dir, "6_Spectral_Feature_Extraction", "output_path", "output_dir"
)
def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
@ -69,7 +79,7 @@ def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_path, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "extract_spectra"
# ---------- 提前失败检查 ----------
@ -163,6 +173,14 @@ def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
"mode": mode,
}
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
# extract_training_spectra 内部硬编码输出文件名 training_spectra.csv,
# 用户浏览指定的文件名在算法内部被忽略。
user_path = config.get("output_path")
if user_path:
result_path = copy_to_user_path(result_path, user_path)
p = Path(result_path)
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),

View File

@ -35,13 +35,19 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from src.core.steps.data_preparation_step import DataPreparationStep
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_file: str | None, work_dir: str) -> Path:
"""根据 output_file / work_dir 计算 indices CSV 输出目录"""
if output_file:
return Path(output_file).parent
return Path(work_dir) / "7_Water_Quality_Indices"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_file / output_path / work_dir 计算 indices CSV 输出目录
使用共享解析器强制执行"用户优先"规则——step7 旧 panel 字段是 output_file,
新架构保留 output_path 兼容性,按优先级回退。
"""
return resolve_output_dir(
config, work_dir, "7_Water_Quality_Indices",
"output_file", "output_path", "output_dir",
)
def execute_step7(config: Dict[str, Any]) -> Dict[str, Any]:
@ -61,7 +67,7 @@ def execute_step7(config: Dict[str, Any]) -> Dict[str, Any]:
output_file: Optional[str] = config.get("output_file")
work_dir: str = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_file, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "calc_indices"
# ---------- 提前失败检查 ----------

View File

@ -38,13 +38,23 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from src.core.steps.modeling_step import ModelingStep
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算模型保存目录"""
if output_path:
return Path(output_path)
return Path(work_dir) / "8_Supervised_Model_Training"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算模型保存目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时直接用其值
(因为 step8 的 output_path 是一个目录,不是文件),
否则用 work_dir/8_Supervised_Model_Training 默认。
注意:step8 与其他步骤不同——output_path 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_path", "output_dir")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "8_Supervised_Model_Training", "default"
def execute_step8(config: Dict[str, Any]) -> Dict[str, Any]:
@ -67,7 +77,7 @@ def execute_step8(config: Dict[str, Any]) -> Dict[str, Any]:
output_path: Optional[str] = config.get("output_path")
work_dir: str = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_path, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "train_ml"
# ---------- 提前失败检查 ----------

View File

@ -41,14 +41,22 @@ from pathlib import Path
from typing import Any, Dict
from src.core.steps.prediction_step import PredictionStep
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
"""根据 output_path / work_dir 计算预测结果输出目录"""
if output_path:
return Path(output_path)
# 与旧 pipeline 一致:prediction_dir / 9_ML_Prediction
return Path(work_dir) / "prediction" / "9_ML_Prediction"
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_path / work_dir 计算预测结果输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时直接用其值
(step9 的 output_path 本身就是一个目录,非文件),否则用 work_dir 默认路径。
注意:step9 与其他步骤不同——output_path 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_path", "output_dir")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "prediction" / "9_ML_Prediction", "default"
def execute_step9(config: Dict[str, Any]) -> Dict[str, Any]:
@ -69,7 +77,7 @@ def execute_step9(config: Dict[str, Any]) -> Dict[str, Any]:
output_path: str = config.get("output_path")
work_dir: str = config.get("work_dir") or "."
output_dir = _resolve_output_dir(output_path, work_dir)
output_dir, _source = _resolve_output_dir(config, work_dir)
mode = "predict_ml"
# ---------- 提前失败检查 ----------