Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| from __future__ import annotations | |
| import re | |
| from collections import defaultdict | |
| from typing import Any | |
| from fastapi import HTTPException | |
| from sidebar_catalog import ( | |
| HF_SOURCE_CATEGORIES as _HF_SOURCE_CATEGORIES, | |
| HF_SOURCE_NAMES_ZH as _HF_SOURCE_NAMES_ZH, | |
| OCEAN_CATALOG as _OCEAN_CATALOG, | |
| ) | |
| def _project_package_plan(project: str) -> dict[str, Any]: | |
| """Build a reproducible project data plan from the user's project description. | |
| v3.5.1 keeps the planner deterministic/offline so a recommendation can always be | |
| reproduced, but upgrades it from a flat keyword list to a structured plan: | |
| project intent -> requirements -> priority -> source mapping -> coverage hints -> | |
| alternatives. Live file availability/size is still verified by the estimate/build | |
| endpoints before downloading anything. | |
| """ | |
| text = str(project or "").strip() | |
| q = text.lower() | |
| if len(text) < 4: | |
| raise HTTPException(400, "请至少用一句话描述项目目标。") | |
| ocean_scores: dict[str, int] = defaultdict(int) | |
| fish_scores: dict[str, int] = defaultdict(int) | |
| reasons: dict[str, list[str]] = defaultdict(list) | |
| requirement_tags: dict[str, set[str]] = defaultdict(set) | |
| def add_ocean(key: str, score: int, reason: str, requirement: str = ""): | |
| ocean_scores[key] += score | |
| reasons["ocean:" + key].append(reason) | |
| if requirement: | |
| requirement_tags["ocean:" + key].add(requirement) | |
| def add_fish(name: str, score: int, reason: str, requirement: str = ""): | |
| fish_scores[name] += score | |
| reasons["fish:" + name].append(reason) | |
| if requirement: | |
| requirement_tags["fish:" + name].add(requirement) | |
| # ---- 1) Interpret the project itself ------------------------------------------------- | |
| species = [] | |
| species_rules = [ | |
| (("柔鱼", "茎柔鱼", "鱿鱼", "squid"), "柔鱼/鱿鱼"), | |
| (("金枪鱼", "tuna"), "金枪鱼"), | |
| (("黄鳍", "yellowfin"), "黄鳍金枪鱼"), | |
| (("大眼", "bigeye"), "大眼金枪鱼"), | |
| (("长鳍", "albacore"), "长鳍金枪鱼"), | |
| (("蓝鳍", "bluefin"), "蓝鳍金枪鱼"), | |
| (("鲣", "skipjack"), "鲣"), | |
| ] | |
| for terms, label in species_rules: | |
| if any(t in q for t in terms) and label not in species: | |
| species.append(label) | |
| task_rules = [ | |
| (("中尺度涡旋", "涡旋", "eddy", "mesoscale eddy"), "物理海洋过程研究"), | |
| (("锋面", "海洋锋", "front"), "物理海洋过程研究"), | |
| (("上升流", "upwelling"), "物理海洋过程研究"), | |
| (("环流", "海流", "洋流", "circulation", "current"), "物理海洋过程研究"), | |
| (("生境", "适生区", "habitat", "species distribution", "sdm", "maxent", "分布预测"), "生境/物种分布预测"), | |
| (("资源评估", "种群评估", "stock assessment", "biomass", "资源量", "补充量"), "资源/种群评估"), | |
| (("cpue", "努力量", "effort", "catch rate"), "CPUE/捕捞努力分析"), | |
| (("渔场", "fishing ground", "捕捞热点", "hotspot"), "渔场/热点分析"), | |
| (("气候", "enso", "气候变化", "climate"), "气候影响分析"), | |
| (("趋势", "变化趋势", "时序", "time series"), "时间序列/趋势分析"), | |
| ] | |
| task_type = "综合海洋分析" | |
| for terms, label in task_rules: | |
| if any(t in q for t in terms): | |
| task_type = label | |
| break | |
| # Exact day, month, year and rough ranges. Keep exact date separately because Ocean | |
| # export currently needs a single date. | |
| date = "" | |
| time_range = "" | |
| m = re.search(r"(20\d{2})[-/年](1[0-2]|0?[1-9])[-/月](3[01]|[12]\d|0?[1-9])", text) | |
| if m: | |
| date = f"{int(m.group(1)):04d}-{int(m.group(2)):02d}-{int(m.group(3)):02d}" | |
| time_range = date | |
| else: | |
| compact = re.search(r"20\d{6}", text) | |
| if compact: | |
| raw = compact.group(0) | |
| date = f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}" | |
| time_range = date | |
| else: | |
| yr = re.search(r"(20\d{2})\s*(?:年)?\s*(?:[-–—至到~~]\s*(20\d{2})\s*年?)?", text) | |
| mo = re.search(r"(20\d{2})[-/年](1[0-2]|0?[1-9])(?:月)?", text) | |
| if mo: | |
| time_range = f"{int(mo.group(1)):04d}-{int(mo.group(2)):02d}" | |
| elif yr: | |
| time_range = yr.group(1) + ((" 至 " + yr.group(2)) if yr.group(2) else "") | |
| regions = [ | |
| (("南海",), [99.0, 124.0, 0.0, 25.0], "南海"), | |
| (("东海",), [118.0, 132.0, 23.0, 34.0], "东海"), | |
| (("黄海",), [117.0, 126.0, 30.0, 41.0], "黄海"), | |
| (("西北太平洋",), [120.0, 180.0, 10.0, 50.0], "西北太平洋"), | |
| (("北太平洋",), [120.0, -100.0, 0.0, 60.0], "北太平洋"), | |
| # “太平洋”跨越日期变更线,不能安全地用一个普通 west<east bbox 表示。 | |
| # 先识别语义区域,但要求用户在真正导出前补充子区域或经纬度范围。 | |
| (("太平洋", "pacific"), [], "太平洋"), | |
| (("印度洋",), [20.0, 120.0, -50.0, 30.0], "印度洋"), | |
| (("大西洋",), [-80.0, 20.0, -60.0, 60.0], "大西洋"), | |
| (("全球", "global"), [-180.0, 180.0, -90.0, 90.0], "全球"), | |
| ] | |
| bbox: list[float] = [] | |
| region_name = "" | |
| for terms, b, label in regions: | |
| if any(t in text.lower() for t in terms): | |
| bbox, region_name = b, label | |
| break | |
| # ---- 2) Translate research goals into data requirements ------------------------------- | |
| reqs: list[dict[str, Any]] = [] | |
| def requirement(name: str, priority: str, why: str, variables: list[str], domain: str): | |
| if any(x["name"] == name for x in reqs): | |
| return | |
| reqs.append({"name": name, "priority": priority, "why": why, "variables": variables, "domain": domain}) | |
| habitat_intent = any(t in q for t in ("生境", "适生区", "分布预测", "maxent", "物种分布", "环境驱动", "habitat", "sdm")) | |
| tuna_intent = any(t in q for t in ("tuna", "金枪鱼", "鲣", "黄鳍", "大眼", "长鳍", "蓝鳍")) | |
| squid_intent = any(t in q for t in ("squid", "鱿鱼", "柔鱼", "茎柔鱼", "赤鱿")) | |
| effort_intent = any(t in q for t in ("cpue", "努力量", "捕捞量", "catch", "effort", "渔获", "捕捞记录")) | |
| stock_intent = any(t in q for t in ("资源评估", "种群评估", "stock assessment", "biomass", "资源量", "补充量")) | |
| vessel_intent = any(t in q for t in ("渔船", "ais", "捕捞活动", "船舶活动", "夜光", "viirs")) | |
| eddy_intent = any(t in q for t in ("中尺度涡旋", "涡旋", "eddy", "mesoscale eddy")) | |
| physical_ocean_intent = eddy_intent or any(t in q for t in ("锋面", "上升流", "环流", "海流", "洋流", "front", "upwelling", "circulation")) | |
| fisheries_intent = bool(tuna_intent or squid_intent or effort_intent or stock_intent or vessel_intent or any(t in q for t in ("渔业", "渔场", "捕捞", "渔获", "fishery", "fisheries"))) | |
| keyword_ocean = [ | |
| (("sst", "海温", "水温", "温度"), "oisst", 9, "海表温度是项目明确提出的环境因子", "SST"), | |
| (("盐度", "salinity"), "cmems_physics", 8, "项目明确需要盐度", "盐度"), | |
| (("流速", "海流", "环流", "uo", "vo", "地转流"), "cmems_physics", 8, "项目明确需要海流/环流", "海流"), | |
| (("叶绿素", "chlorophyll", "chl"), "cmems_bgc", 9, "项目明确需要叶绿素/生产力指标", "叶绿素"), | |
| (("营养盐", "no3", "硝酸盐", "磷酸盐"), "cmems_bgc", 8, "项目明确需要营养盐", "营养盐"), | |
| (("溶解氧", "oxygen", " do "), "cmems_bgc", 9, "项目明确需要溶解氧", "溶解氧"), | |
| (("海色", "遥感叶绿素", "oc-cci", "occci"), "occci", 8, "项目明确需要海色遥感", "海色"), | |
| (("风", "风速", "风场", "气温", "气压", "era5", "气象"), "era5", 7, "项目明确需要大气驱动", "风场/气象"), | |
| (("降水", "蒸发", "辐射", "热通量"), "era5_accum", 7, "项目明确需要累积大气量/通量", "大气通量"), | |
| (("混合层", "mlotst"), "cmems_surface", 8, "项目明确需要混合层结构", "混合层"), | |
| (("海面高度", "ssh", "sla", "adt", "zos"), "cmems_surface", 8, "项目明确需要海面高度/高度异常", "SSH/SLA/ADT"), | |
| (("酸化", "ph", "co2", "碳酸盐", "spco2"), "cmems_carbonate", 8, "项目明确需要碳酸盐系统", "碳酸盐"), | |
| ] | |
| for terms, key, score, reason, req in keyword_ocean: | |
| if any(t in q for t in terms): | |
| add_ocean(key, score, reason, req) | |
| requirement(req, "必需", reason, [req], "Ocean") | |
| # Domain defaults are not merely keywords: they encode common analysis dependencies. | |
| # Physical-ocean process tasks must stay Ocean-only unless the user explicitly asks | |
| # for a fisheries/ecology response variable. | |
| if eddy_intent: | |
| requirement("SSH / SLA", "必需", "中尺度涡旋通常首先由海表高度或海表高度异常识别与追踪", ["SSH", "SLA", "zos"], "Ocean") | |
| requirement("ADT / 海表动力高度", "推荐", "用于描述涡旋绝对动力高度结构;具体可用变量以实时产品为准", ["ADT", "zos"], "Ocean") | |
| requirement("地转流 / 海流 U-V", "推荐", "用于判断旋转方向、流速和涡旋动力结构", ["uo", "vo"], "Ocean") | |
| requirement("SST", "推荐", "用于分析暖涡/冷涡对应的海表温度异常", ["SST", "anom"], "Ocean") | |
| requirement("叶绿素 / 海色", "可选", "只有研究涡旋生态或初级生产力响应时才需要", ["CHL", "chlor_a"], "Ocean") | |
| requirement("温盐三维剖面", "可选", "研究涡旋垂向结构、温跃层或水团特征时需要", ["thetao", "so"], "Ocean") | |
| add_ocean("cmems_surface", 10, "中尺度涡旋识别的核心海表高度/上层海洋场", "SSH / SLA") | |
| add_ocean("cmems_physics", 7, "涡旋动力结构和三维温盐流场", "地转流 / 海流 U-V") | |
| add_ocean("oisst", 6, "暖涡/冷涡海表温度异常分析", "SST") | |
| add_ocean("occci", 3, "仅用于涡旋生态响应或生产力研究", "叶绿素 / 海色") | |
| if habitat_intent: | |
| requirement("SST", "必需", "生境模型通常需要基础热环境", ["SST"], "Ocean") | |
| if fisheries_intent or species: | |
| requirement("渔业出现/捕捞记录", "必需", "生境模型需要物种出现或捕捞位置作为响应数据", ["occurrence", "catch", "effort"], "Fisheries") | |
| requirement("生产力/叶绿素", "推荐", "用于描述食物资源与生产力环境", ["CHL", "NPP"], "Ocean") | |
| requirement("上层海洋结构", "推荐", "SSH/混合层可补充水团与锋面信息", ["SSH", "MLD"], "Ocean") | |
| requirement("海流", "推荐", "海流影响洄游、输运和锋面结构", ["uo", "vo"], "Ocean") | |
| add_ocean("oisst", 7, "生境模型基础热环境", "SST") | |
| add_ocean("cmems_bgc", 5, "生境模型生产力与生态环境", "生产力/叶绿素") | |
| add_ocean("cmems_surface", 4, "生境模型上层海洋结构", "上层海洋结构") | |
| add_ocean("cmems_physics", 4, "生境模型海流与物理环境", "海流") | |
| if "气候" in task_type: | |
| requirement("大气驱动", "推荐", "用于解释年际/气候尺度变化", ["wind", "air temperature", "pressure"], "Ocean") | |
| add_ocean("era5", 5, "气候影响分析的大气驱动", "大气驱动") | |
| if tuna_intent: | |
| requirement("金枪鱼区域渔业记录", "必需", "研究对象为金枪鱼,需要区域性渔获/努力量记录", ["catch", "effort", "CPUE"], "Fisheries") | |
| for n in ("WCPFC", "IATTC", "ICCAT", "IOTC", "CCSBT"): | |
| score = 7 | |
| if region_name in ("西北太平洋", "北太平洋") and n == "WCPFC": score = 10 | |
| if region_name == "印度洋" and n == "IOTC": score = 10 | |
| if region_name == "大西洋" and n == "ICCAT": score = 10 | |
| add_fish(n, score, "金枪鱼区域渔业数据", "金枪鱼区域渔业记录") | |
| if squid_intent: | |
| requirement("柔鱼/鱿鱼区域渔业记录", "必需", "研究对象为柔鱼/鱿鱼,需要区域性捕捞/出现记录", ["catch", "effort", "CPUE", "occurrence"], "Fisheries") | |
| add_fish("SPRFMO", 9 if region_name in ("西北太平洋", "北太平洋") else 7, "柔鱼/鱿鱼区域渔业数据", "柔鱼/鱿鱼区域渔业记录") | |
| add_fish("NPFC", 10 if region_name in ("西北太平洋", "北太平洋") else 7, "北太平洋柔鱼/鱿鱼区域数据", "柔鱼/鱿鱼区域渔业记录") | |
| if effort_intent: | |
| requirement("Catch / Effort / CPUE", "必需", "项目明确涉及捕捞量、努力量或 CPUE", ["catch", "effort", "CPUE"], "Fisheries") | |
| add_fish("FAO", 5, "捕捞统计基线", "Catch / Effort / CPUE") | |
| add_fish("Sea Around Us", 5, "历史重建捕捞量", "Catch / Effort / CPUE") | |
| if stock_intent: | |
| requirement("种群评估指标", "必需", "资源评估需要生物量/补充量等种群指标", ["biomass", "recruitment", "F"], "Fisheries") | |
| add_fish("RAM Legacy", 10, "资源评估与种群指标", "种群评估指标") | |
| if vessel_intent: | |
| requirement("船舶活动/表观努力", "推荐", "用于刻画捕捞活动空间分布", ["AIS fishing hours", "VIIRS detections"], "Fisheries") | |
| add_fish("GFW", 9, "AIS 表观捕捞活动", "船舶活动/表观努力") | |
| add_fish("VIIRS", 7, "夜光船活动观测", "船舶活动/表观努力") | |
| if not ocean_scores and not fish_scores: | |
| requirement("基础海洋环境", "推荐", "项目描述较宽泛,默认先提供海洋环境背景,不自动引入无关渔业数据", ["SST", "SSH"], "Ocean") | |
| add_ocean("oisst", 5, "通用海洋环境背景", "基础海洋环境") | |
| add_ocean("cmems_surface", 5, "通用海表与上层海洋背景", "基础海洋环境") | |
| if fisheries_intent: | |
| requirement("全球渔业统计", "推荐", "项目明确涉及渔业,提供全球渔业统计基线", ["catch"], "Fisheries") | |
| add_fish("FAO", 4, "全球渔业统计基线", "全球渔业统计") | |
| def priority(score: int) -> str: | |
| if score >= 8: return "必需" | |
| if score >= 5: return "推荐" | |
| return "可选" | |
| alternatives = { | |
| "oisst": ["CMEMS Physics(若需要三维温度/更多物理量)"], | |
| "cmems_bgc": ["OC-CCI(若主要关注遥感叶绿素/海色)"], | |
| "occci": ["CMEMS BGC(若还需要营养盐、溶解氧等)"], | |
| "era5": ["ERA5 Accum(若重点是降水、辐射、热通量累计量)"], | |
| "cmems_surface": ["CMEMS Physics(若需要更完整三维物理场)"], | |
| } | |
| fish_alternatives = { | |
| "WCPFC": ["FAO", "Sea Around Us"], "IATTC": ["FAO", "Sea Around Us"], | |
| "ICCAT": ["FAO", "Sea Around Us"], "IOTC": ["FAO", "Sea Around Us"], | |
| "CCSBT": ["FAO", "Sea Around Us"], "SPRFMO": ["NPFC", "FAO"], | |
| "NPFC": ["SPRFMO", "FAO"], "GFW": ["VIIRS"], "VIIRS": ["GFW"], | |
| "RAM Legacy": ["FAO", "Sea Around Us"], | |
| } | |
| ocean_lookup = {x[0]: x for x in _OCEAN_CATALOG} | |
| ocean = [] | |
| for key, score in sorted(ocean_scores.items(), key=lambda x: (-x[1], x[0]))[:7]: | |
| entry = ocean_lookup.get(key) | |
| if not entry: continue | |
| _, name, name_zh, variables = entry | |
| ocean.append({ | |
| "database": "Ocean", "key": key, "name": name, "name_zh": name_zh, | |
| "variables": list(variables), "score": score, "priority": priority(score), | |
| "reason": ";".join(dict.fromkeys(reasons["ocean:" + key])), | |
| "requirements": sorted(requirement_tags["ocean:" + key]), | |
| "coverage": {"time": "打包前实时核验", "space": "打包前按所选区域核验", "resolution": "需结合模型尺度确认"}, | |
| "alternatives": alternatives.get(key, []), | |
| }) | |
| fisheries = [] | |
| for name, score in sorted(fish_scores.items(), key=lambda x: (-x[1], x[0]))[:10]: | |
| category = _HF_SOURCE_CATEGORIES.get(name, ("general", "综合渔业数据"))[0] | |
| db = "Tuna-Fisheries-Dataset" if category == "tuna" else "squid_dataset" if category == "squid" else "按实际 Hugging Face 仓库分类" | |
| fisheries.append({ | |
| "database": db, "name": name, "name_zh": _HF_SOURCE_NAMES_ZH.get(name, name), | |
| "category": category, "score": score, "priority": priority(score), | |
| "reason": ";".join(dict.fromkeys(reasons["fish:" + name])), | |
| "requirements": sorted(requirement_tags["fish:" + name]), | |
| "coverage": {"time": "打包前核验文件时间覆盖", "space": "依据来源组织/文件范围核验", "resolution": "以实际文件粒度为准"}, | |
| "alternatives": fish_alternatives.get(name, []), | |
| }) | |
| missing_conditions = [] | |
| if not region_name: | |
| missing_conditions.append("海域/空间范围") | |
| elif region_name == "太平洋" and not bbox: | |
| missing_conditions.append("更精确的太平洋子区域/经纬度范围") | |
| if not time_range: missing_conditions.append("时间范围") | |
| if not species and fisheries_intent: | |
| missing_conditions.append("目标物种") | |
| # Explicit variables plus inferred requirements give the user a compact project brief. | |
| requirements = sorted(reqs, key=lambda x: ({"必需": 0, "推荐": 1, "可选": 2}.get(x["priority"], 3), x["name"])) | |
| required_count = sum(1 for x in ocean + fisheries if x["priority"] == "必需") | |
| recommended_count = sum(1 for x in ocean + fisheries if x["priority"] == "推荐") | |
| optional_count = sum(1 for x in ocean + fisheries if x["priority"] == "可选") | |
| return { | |
| "planner_version": "3.5.1", | |
| "project": text, | |
| "project_profile": { | |
| "task_type": task_type, | |
| "research_domain": "物理海洋" if physical_ocean_intent and not fisheries_intent else ("渔业/生态" if fisheries_intent else "海洋综合"), | |
| "research_object": "中尺度涡旋" if eddy_intent else ("、".join(species) if species else ""), | |
| "species": species, | |
| "region": region_name, | |
| "bbox": bbox, | |
| "time_range": time_range, | |
| "exact_date": date, | |
| }, | |
| "requirements": requirements, | |
| "ocean": ocean, | |
| "fisheries": fisheries, | |
| "date": date, | |
| "time_range": time_range, | |
| "bbox": bbox, | |
| "region_name": region_name, | |
| "ocean_export_ready": bool(date and bbox), | |
| "missing_conditions": missing_conditions, | |
| "priority_summary": {"required": required_count, "recommended": recommended_count, "optional": optional_count}, | |
| "checks": [ | |
| "生成前通过实时 inventory 核验 Hugging Face 文件是否存在及大小。", | |
| "Ocean 原始格点导出会按日期与区域请求 Marine API;缺少精确日期时只生成可复现请求清单。", | |
| "时间覆盖、空间覆盖和分辨率属于数据源级条件,最终以实时文件/接口返回为准,不用规则结果冒充真实覆盖。", | |
| ], | |
| "note": "推荐按“必需 / 推荐 / 可选”分级;物理海洋任务默认只推荐 Ocean,只有出现物种、捕捞、CPUE、渔场等明确渔业意图时才加入 Fisheries。若主来源不可用,可参考替代来源。", | |
| } | |