Initial commit
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
71db82393a
@@ -0,0 +1,211 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user