Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
213 lines
8.4 KiB
Python
213 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
二期报告生成脚本框架
|
|
数据 → PCA赋分(SPSS对齐) → 统计 → [LLM并行生成] → HTML报告
|
|
|
|
⚠️ 默认 --no-llm 模式,不会调用 LLM(避免产生费用)
|
|
若需启用 LLM 生成,手动传 --enable-llm 参数
|
|
"""
|
|
import sys
|
|
import time
|
|
import argparse
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from data_engine_era2 import DataEngineEra2
|
|
from engines.pca_scoring_engine_era2 import PcaScoringEngineEra2
|
|
from engines.stats_engine_era2 import StatsEngineEra2 as StatsEngine
|
|
from config_era2 import DIMENSION_FRAMEWORK
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def clean_for_json(obj):
|
|
if isinstance(obj, dict):
|
|
return {k: clean_for_json(v) for k, v in obj.items()}
|
|
elif isinstance(obj, list):
|
|
return [clean_for_json(v) for v in obj]
|
|
elif isinstance(obj, (np.integer,)):
|
|
return int(obj)
|
|
elif isinstance(obj, (np.floating,)):
|
|
return round(float(obj), 4)
|
|
elif isinstance(obj, np.ndarray):
|
|
return obj.tolist()
|
|
elif isinstance(obj, float):
|
|
return round(obj, 4)
|
|
return obj
|
|
|
|
|
|
def progress_callback(completed, total, segment_id):
|
|
pct = completed / total * 100
|
|
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
|
|
print(f"\r [{bar}] {pct:.0f}% ({completed}/{total}) {segment_id:<40}", end="", flush=True)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="二期报告生成")
|
|
parser.add_argument("--school", type=str, required=True,
|
|
help="学校名称(简称或全称均可)")
|
|
parser.add_argument("--district", type=str, default="长宁区",
|
|
help="区域筛选,'all'表示全部,默认'长宁区'")
|
|
parser.add_argument("--enable-llm", action="store_true",
|
|
help="启用LLM生成(会产生API调用费用!)")
|
|
parser.add_argument("--no-cache", action="store_true",
|
|
help="不使用LLM缓存")
|
|
parser.add_argument("--no-scoring-cache", action="store_true",
|
|
help="不使用赋分缓存(强制重新计算全市赋分)")
|
|
parser.add_argument("--list-schools", action="store_true",
|
|
help="列出所有可用学校")
|
|
parser.add_argument("--enable-agent", action="store_true",
|
|
help="在报告中嵌入AI对话助手(右下角浮动按钮)")
|
|
parser.add_argument("--lang", type=str, default="zh", choices=["zh", "en", "both"],
|
|
help="报告语言:zh(中文,默认)/ en(英文)/ both(同时生成两份)")
|
|
args = parser.parse_args()
|
|
|
|
start_time = time.time()
|
|
|
|
print("=" * 70)
|
|
print("📊 二期课程实施监测报告生成系统")
|
|
if not args.enable_llm:
|
|
print("⚠️ LLM 已禁用(--no-llm 模式),仅生成数据+图表")
|
|
if args.enable_agent:
|
|
print("🤖 AI 对话助手已启用")
|
|
print("=" * 70)
|
|
|
|
# ===== Step 1: 加载数据 =====
|
|
district = None if args.district == "all" else args.district
|
|
logger.info(f"[1/5] 加载二期数据... (区域: {args.district}, 全市基准)")
|
|
# 加载全市数据,按区筛选报告范围
|
|
data_engine = DataEngineEra2(district_filter=district)
|
|
data_engine.load_all()
|
|
district_schools = data_engine.schools # 本区学校列表
|
|
|
|
# 全市数据引擎(用于赋分全市学校)
|
|
if data_engine.use_city_data and district:
|
|
city_engine = DataEngineEra2(district_filter=None) # 不筛选区
|
|
city_engine.load_all()
|
|
all_schools_for_scoring = city_engine
|
|
logger.info(f" 全市基准: {len(city_engine.schools)}校, 本区: {len(district_schools)}校")
|
|
else:
|
|
city_engine = None
|
|
all_schools_for_scoring = data_engine
|
|
|
|
if args.list_schools:
|
|
print(f"\n可用学校 ({len(district_schools)}所):")
|
|
for i, s in enumerate(district_schools, 1):
|
|
print(f" {i:3d}. {s}")
|
|
return
|
|
|
|
school = args.school
|
|
if school not in district_schools:
|
|
# 尝试从映射查找
|
|
from config_era2 import SCHOOL_NAME_SHORT_TO_FULL, SCHOOL_NAME_FULL_TO_SHORT
|
|
if school in SCHOOL_NAME_SHORT_TO_FULL:
|
|
pass
|
|
elif school in SCHOOL_NAME_FULL_TO_SHORT:
|
|
school = SCHOOL_NAME_FULL_TO_SHORT[school]
|
|
|
|
if school not in all_schools_for_scoring.schools:
|
|
print(f"\n❌ 学校 '{args.school}' 不在数据中。本区可用学校:")
|
|
for s in district_schools:
|
|
print(f" - {s}")
|
|
return
|
|
|
|
print(f"\n🏫 目标学校: {school}")
|
|
print(f"📍 数据范围: {args.district} ({len(district_schools)}所学校)")
|
|
if city_engine:
|
|
print(f"📊 标准化基准: 全市{len(city_engine.schools)}所学校")
|
|
|
|
# ===== Step 2: PCA赋分(SPSS对齐,全市所有学校) =====
|
|
logger.info(f"[2/5] PCA赋分计算 ({len(all_schools_for_scoring.schools)}校, SPSS对齐)...")
|
|
pca_engine = PcaScoringEngineEra2(all_schools_for_scoring)
|
|
pca_sub_scores = pca_engine.compute_all()
|
|
|
|
# ===== Step 3: 统计分析(全市基准标准化) =====
|
|
logger.info("[3/5] 全市基准标准化 + 统计分析...")
|
|
stats_engine = StatsEngine()
|
|
sub_scores = stats_engine.compute_dimension_scores_pca(pca_sub_scores)
|
|
dim_scores = stats_engine.compute_dimension_aggregates(sub_scores)
|
|
report_data = stats_engine.compute_school_report_data(
|
|
school, sub_scores, dim_scores,
|
|
district_schools=district_schools,
|
|
)
|
|
|
|
# 注入区域信息(模板需要)
|
|
report_data["district"] = args.district
|
|
report_data["total_schools_in_district"] = len(district_schools)
|
|
|
|
# 决定要生成的语言列表
|
|
langs = ["zh", "en"] if args.lang == "both" else [args.lang]
|
|
|
|
# ===== Step 4: LLM生成(按语言分别生成;缓存按 lang 隔离) =====
|
|
llm_sections_by_lang = {}
|
|
if args.enable_llm:
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "backend"))
|
|
from app.engines.llm_engine import LLMEngine
|
|
llm_engine = LLMEngine()
|
|
llm_engine.set_progress_callback(progress_callback)
|
|
for lg in langs:
|
|
logger.info(f"[4/5] LLM并行生成报告文字 (lang={lg}) ...")
|
|
llm_sections_by_lang[lg] = llm_engine.generate_report_segments(
|
|
report_data,
|
|
use_cache=not args.no_cache,
|
|
lang=lg,
|
|
)
|
|
print()
|
|
else:
|
|
logger.info("[4/5] 跳过LLM生成(--no-llm 模式)")
|
|
for lg in langs:
|
|
llm_sections_by_lang[lg] = {}
|
|
|
|
# ===== Step 5: 渲染HTML =====
|
|
from engines.report_renderer_era2 import ReportRendererEra2 as ReportRenderer
|
|
renderer = ReportRenderer()
|
|
|
|
output_dir = Path(__file__).parent.parent.parent / "output" / "era2" / args.district
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
output_paths = {}
|
|
for lg in langs:
|
|
logger.info(f"[5/5] 渲染HTML报告 (lang={lg}) ...")
|
|
suffix = "_report_en.html" if lg == "en" else "_报告.html"
|
|
output_path = output_dir / f"{school}{suffix}"
|
|
renderer.render_to_file(
|
|
report_data, llm_sections_by_lang[lg], output_path,
|
|
enable_agent=args.enable_agent, lang=lg,
|
|
)
|
|
output_paths[lg] = output_path
|
|
|
|
# 保存报告数据JSON(一份,与语言无关)
|
|
json_path = output_dir / f"{school}_report_data.json"
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump(clean_for_json(report_data), f, ensure_ascii=False, indent=2)
|
|
|
|
elapsed = time.time() - start_time
|
|
|
|
print(f"\n{'=' * 70}")
|
|
print(f"✅ 报告生成完成!")
|
|
print(f" 学校: {school}")
|
|
print(f" 区域: {args.district} ({len(data_engine.schools)}所学校)")
|
|
print(f" 总体得分: {report_data['overall']['score']}分 (第{report_data['overall']['rank_in_district']}名)")
|
|
for lg in langs:
|
|
n_sec = len(llm_sections_by_lang.get(lg, {}))
|
|
print(f" LLM段落({lg}): {n_sec}个 {'(已禁用)' if not args.enable_llm else ''}")
|
|
print(f" AI助手: {'✅ 已嵌入' if args.enable_agent else '❌ 未启用'}")
|
|
print(f" 耗时: {elapsed:.1f}秒")
|
|
for lg, p in output_paths.items():
|
|
print(f" HTML({lg}): {p}")
|
|
print(f" JSON: {json_path}")
|
|
print(f"{'=' * 70}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|