#!/usr/bin/env python3 """ 批量报告生成脚本:为长宁区全部9所学校生成课程实施监测报告 数据加载和赋分/统计只做一次,LLM和渲染对每校独立执行 """ import sys import time import argparse import logging from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) from app.engines.data_engine import DataEngine from app.engines.scoring_engine import ScoringEngine from app.engines.stats_engine import StatsEngine from app.engines.llm_engine import LLMEngine from app.engines.report_renderer import ReportRenderer logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) logger = logging.getLogger(__name__) 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("--no-cache", action="store_true", help="不使用LLM缓存") parser.add_argument("--no-llm", action="store_true", help="跳过LLM生成(仅图表+数据)") parser.add_argument("--schools", nargs="*", help="指定学校列表(默认全部9所)") args = parser.parse_args() total_start = time.time() print("=" * 70) print("📊 课程实施监测报告 — 批量生成系统") print("=" * 70) # ===== Step 1: 加载数据(只做一次) ===== logger.info("[全局 1/3] 加载Excel数据...") data_engine = DataEngine() data_engine.load_all() # ===== Step 2: 全区赋分(只做一次) ===== logger.info("[全局 2/3] 全区赋分计算...") scoring_engine = ScoringEngine(data_engine) raw_scores = scoring_engine.score_all_schools() # ===== Step 3: 统计分析(只做一次) ===== logger.info("[全局 3/3] PCA合成 + 标准化...") stats_engine = StatsEngine() sub_scores = stats_engine.compute_dimension_scores(raw_scores) dim_scores = stats_engine.compute_dimension_aggregates(sub_scores) # 确定生成哪些学校 all_schools = list(data_engine.schools) if args.schools: schools = [] for s in args.schools: if s in all_schools: schools.append(s) else: print(f" ⚠️ 学校 '{s}' 不在数据中,跳过") if not schools: print("❌ 没有有效的学校,退出") return else: schools = all_schools print(f"\n🏫 将为以下 {len(schools)} 所学校生成报告:") for i, s in enumerate(schools, 1): from app.config import SCHOOL_TYPE_MAP info = SCHOOL_TYPE_MAP.get(s, {}) print(f" {i}. {s} ({info.get('type', '未知类型')})") # ===== 初始化引擎 ===== renderer = ReportRenderer() llm_engine = None if not args.no_llm: llm_engine = LLMEngine() llm_engine.set_progress_callback(progress_callback) output_dir = Path(__file__).parent.parent / "output" 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}") print(f"{'─' * 70}") try: # 统计分析(学校专属数据包) report_data = stats_engine.compute_school_report_data(school, sub_scores, dim_scores) # LLM生成 if args.no_llm: print(" ⏭️ 跳过LLM生成(--no-llm模式)") llm_sections = {} else: print(" 🤖 LLM并行生成报告文字...") llm_sections = llm_engine.generate_report_segments( report_data, use_cache=not args.no_cache, ) print() # 换行(progress bar之后) # 渲染HTML output_path = output_dir / f"{school}_报告.html" renderer.render_to_file(report_data, llm_sections, output_path) # 保存数据JSON import json def _json_safe(obj): """处理numpy类型""" import numpy as np if isinstance(obj, (np.integer,)): return int(obj) if isinstance(obj, (np.floating,)): return float(obj) if isinstance(obj, np.ndarray): return obj.tolist() raise TypeError(f"Object of type {type(obj)} is not JSON serializable") json_path = output_dir / f"{school}_report_data.json" with open(json_path, "w", encoding="utf-8") as f: json.dump(report_data, f, ensure_ascii=False, indent=2, default=_json_safe) elapsed = time.time() - school_start score = report_data["overall"]["score"] rank = report_data["overall"]["rank_in_district"] cluster = report_data["overall"]["cluster"] llm_count = len(llm_sections) results.append({ "school": school, "status": "✅", "score": score, "rank": rank, "cluster": cluster, "llm_segments": llm_count, "time": elapsed, "output": str(output_path), }) print(f" ✅ 完成! 得分={score}分 排名={rank}/9 类型={cluster} LLM={llm_count}段 耗时={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"{'学校':<10} {'状态':<4} {'得分':<8} {'排名':<8} {'类型':<8} {'LLM段':<8} {'耗时':<8}") print(f"{'─' * 62}") success_count = 0 for r in results: if r["status"] == "✅": success_count += 1 print(f"{r['school']:<10} {r['status']:<4} {r['score']:<8.2f} {r['rank']}/9{'':<5} {r['cluster']:<8} {r['llm_segments']:<8} {r['time']:.1f}s") else: print(f"{r['school']:<10} {r['status']:<4} {'失败: ' + r.get('error', '未知')}") print(f"{'─' * 62}") print(f"成功: {success_count}/{len(schools)} | 总耗时: {total_elapsed:.1f}s") print(f"输出目录: {output_dir}") print(f"{'=' * 70}") # 保存汇总JSON import json summary_path = output_dir / "batch_summary.json" with open(summary_path, "w", encoding="utf-8") as f: json.dump({ "generated_at": time.strftime("%Y-%m-%d %H:%M:%S"), "total_schools": len(schools), "success_count": success_count, "total_time": round(total_elapsed, 1), "results": results, }, f, ensure_ascii=False, indent=2) print(f"汇总文件: {summary_path}") if __name__ == "__main__": main()