Initial commit

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-13 15:38:41 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 71db82393a
180 changed files with 170640 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
#!/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()