#!/usr/bin/env python3 """ 二期批量报告生成脚本 数据加载/PCA赋分(SPSS对齐)/统计只做一次,LLM和渲染对每校独立执行 ⚠️ 默认 --no-llm 模式,不调用 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 main(): parser = argparse.ArgumentParser(description="二期批量报告生成") parser.add_argument("--district", type=str, default="长宁区", help="区域筛选,'all'表示全部") parser.add_argument("--schools", nargs="*", help="指定学校列表(默认该区全部)") parser.add_argument("--enable-llm", action="store_true", help="启用LLM(会产生API费用!)") parser.add_argument("--no-cache", action="store_true", help="不使用LLM缓存") parser.add_argument("--enable-agent", action="store_true", help="在报告中嵌入AI对话助手") args = parser.parse_args() total_start = time.time() print("=" * 70) print("📊 二期课程实施监测报告 — 批量生成") if not args.enable_llm: print("⚠️ LLM 已禁用,仅生成数据+图表报告") if args.enable_agent: print("🤖 AI 对话助手已启用") print("=" * 70) # ===== 全局步骤(只做一次) ===== district = None if args.district == "all" else args.district logger.info(f"[全局 1/3] 加载二期数据... (区域: {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 logger.info(f"[全局 2/3] PCA赋分 ({len(all_schools_for_scoring.schools)}所学校, SPSS对齐)...") pca_engine = PcaScoringEngineEra2(all_schools_for_scoring) pca_sub_scores = pca_engine.compute_all() logger.info("[全局 3/3] 全市基准标准化...") stats_engine = StatsEngine() sub_scores = stats_engine.compute_dimension_scores_pca(pca_sub_scores) dim_scores = stats_engine.compute_dimension_aggregates(sub_scores) # 确定学校列表(本区) all_schools = district_schools if args.schools: schools = [s for s in args.schools if s in all_schools] skipped = [s for s in args.schools if s not in all_schools] for s in skipped: print(f" ⚠️ 学校 '{s}' 不在数据中,跳过") else: schools = all_schools print(f"\n🏫 将为 {len(schools)} 所学校生成报告:") for i, s in enumerate(schools, 1): print(f" {i}. {s}") # 初始化引擎 from engines.report_renderer_era2 import ReportRendererEra2 as ReportRenderer renderer = ReportRenderer() llm_engine = None 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() output_dir = Path(__file__).parent.parent.parent / "output" / "era2" / args.district output_dir.mkdir(parents=True, exist_ok=True) # ===== 逐校生成 ===== results = [] for idx, school in enumerate(schools, 1): school_start = time.time() print(f"\n{'─' * 70}") print(f"🔄 [{idx}/{len(schools)}] {school}") try: 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) if args.enable_llm and llm_engine: llm_sections = llm_engine.generate_report_segments( report_data, use_cache=not args.no_cache) else: llm_sections = {} # 渲染 html_path = output_dir / f"{school}_报告.html" renderer.render_to_file(report_data, llm_sections, html_path, enable_agent=args.enable_agent) # 保存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() - school_start score = report_data["overall"]["score"] rank = report_data["overall"]["rank_in_district"] total = report_data["overall"]["total_schools"] results.append({ "school": school, "status": "✅", "score": score, "rank": rank, "total": total, "time": elapsed, }) print(f" ✅ 得分={score} 排名={rank}/{total} 耗时={elapsed:.1f}s") except Exception as e: elapsed = time.time() - school_start logger.error(f" ❌ 失败: {e}", exc_info=True) results.append({"school": school, "status": "❌", "error": str(e), "time": elapsed}) # ===== 汇总 ===== total_elapsed = time.time() - total_start print(f"\n{'=' * 70}") print(f"📋 批量生成汇总") print(f"{'=' * 70}") print(f"{'学校':<15} {'状态':<4} {'得分':<8} {'排名':<10} {'耗时':<8}") print(f"{'─' * 50}") success = 0 for r in results: if r["status"] == "✅": success += 1 print(f"{r['school']:<15} {r['status']:<4} {r['score']:<8.2f} {r['rank']}/{r['total']:<7} {r['time']:.1f}s") else: print(f"{r['school']:<15} {r['status']:<4} 失败: {r.get('error', '')[:30]}") print(f"{'─' * 50}") print(f"成功: {success}/{len(schools)} | 总耗时: {total_elapsed:.1f}s") print(f"输出目录: {output_dir}") # 保存汇总 summary = { "generated_at": time.strftime("%Y-%m-%d %H:%M:%S"), "era": 2, "district": args.district, "total_schools": len(schools), "success": success, "total_time": round(total_elapsed, 1), "llm_enabled": args.enable_llm, "results": results, } with open(output_dir / "batch_summary.json", "w", encoding="utf-8") as f: json.dump(summary, f, ensure_ascii=False, indent=2) if __name__ == "__main__": main()