Skip to content

Commit 52c7109

Browse files
committed
fix(eval_optimize_loop): address AI review — lock leak, mode plumbing, encoding, hashlib
Critical: - Wrap Phase 1-6 in try/finally to prevent lock directory leak on exception Warnings: - Plumb --mode arg to BaselineRunner/OptimizationRunner/ValidationRunner - Fix overfit detection: derive candidate train scores separately - Replace builtin hash() with hashlib.sha256 for deterministic cross-process id - Replace image_id positional lookup with path-based case mapping - Fix garbled Chinese in BASE_PROMPTS, CATEGORY_OPTIMIZATION_HINTS, and prompt files Suggestion: - Read critical case ids dynamically from val.evalset.json critical field
1 parent 9aeb798 commit 52c7109

4 files changed

Lines changed: 179 additions & 127 deletions

File tree

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
## 宸ュ叿浣跨敤鎸囧崡
1+
# PlateAgent - Skill Prompt (Tool Usage Guide)
22

3-
### 棰勫鐞嗗伐鍏?
4-
- **tool_gaussian_blur**: 楂樻柉婊ゆ尝闄嶅櫔锛岄€傜敤浜庡櫔澹拌溅鐗?-
5-
- **tool_grayscale**: 杞伆搴﹀浘锛岀畝鍖栧悗缁鐞?-
6-
- **tool_binarize_otsu**: OTSU 浜屽€煎寲锛屽垎绂诲墠鏅?鑳屾櫙
7-
- **tool_edge_detect_canny**: Canny 杈圭紭妫€娴?-
8-
- **tool_affine_correct**: 浠垮皠鍙樻崲鍊炬枩鏍℃
3+
## Preprocess Tools
4+
- **tool_gaussian_blur**: Gaussian blur denoising, suitable for noisy plates
5+
- **tool_grayscale**: Convert to grayscale, simplifies downstream processing
6+
- **tool_binarize_otsu**: OTSU binarization, separates foreground/background
7+
- **tool_edge_detect_canny**: Canny edge detection
8+
- **tool_affine_correct**: Affine transform tilt correction
99

10-
### 璇嗗埆宸ュ叿
11-
- **tool_tesseract_ocr**: Tesseract 5.4 鏁寸墝璇嗗埆锛圕hi_sim+eng锛?-
12-
- **tool_lookup_confusion**: 鏌ヨ娴锋窏瀛楃鏄犲皠琛?-
13-
- **tool_search_blacklist**: 鏌ヨ榛戝悕鍗曡溅鐗?
10+
## Recognition Tools
11+
- **tool_tesseract_ocr**: Tesseract 5.4 whole-plate recognition (chi_sim+eng)
12+
- **tool_lookup_confusion**: Query confusion character mapping table
13+
- **tool_search_blacklist**: Query blacklist database
1414

15-
### 閫夋嫨绛栫暐
16-
- 鍙岄€氶亾 OCR 鍚庯紝浼樺厛閫夋嫨瀛楃鏁板鐨勭粨鏋?-
17-
- 瀛楃鏁扮浉鍚屾椂閫夌疆淇″害楂樼殑
18-
- 鍗曚晶澶辫触鏃跺彇鍙︿竴渚?
19-
- 妯$硦 kernel=5 鏄叧閿弬鏁?
15+
## Selection Strategy
16+
- After dual-channel OCR, prefer the result with more recognized characters
17+
- Same character count: prefer higher confidence
18+
- One channel fails: use the other
19+
- Gaussian blur kernel=5 is the key parameter
20+
21+
## Output Rules
22+
- Return ONLY the plate number string, no extra text
23+
- Failed plates: return "recognition failed"
24+
- Do NOT include JSON, markdown formatting, or explanations
Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
浣犳槸 PlateAgent锛屼竴涓熀浜?OpenCV + Tesseract OCR 鐨勮溅鐗岃瘑鍒櫤鑳戒綋銆?
1+
# PlateAgent - System Prompt
22

3-
## 宸ヤ綔娴佺▼
4-
1. 棰勫鐞嗭細楂樻柉婊ゆ尝 鈫?鐏板害 鈫?浜屽€煎寲 鈫?Canny 杈圭紭 鈫?浠垮皠鏍℃
5-
2. 瀹氫綅锛氬舰鎬佸绮惧畾浣?+ HSV 棰滆壊绌洪棿绮惧畾浣?
6-
3. 鍒嗗壊锛氬瀭鐩存姇褰辨硶瀛楃鍒嗗壊
7-
4. 璇嗗埆锛氬弻閫氶亾 Tesseract OCR锛堝師濮嬪浘 + 楂樻柉妯$硦 kernel=5锛夛紝闀垮害浼樺厛閫夋嫨
8-
5. 楠岃瘉锛氳嚜淇″害 < 0.5 瑙﹀彂浜哄伐纭锛?0.5~0.85 瑙﹀彂 LLM 澶嶆牳
3+
You are PlateAgent, a license plate recognition agent based on OpenCV + Tesseract OCR.
94

10-
## 杈撳嚭鏍煎紡
11-
杩斿洖绾枃鏈溅鐗屽彿锛屼緥濡?`浜珹12345`銆傚鏋滆瘑鍒け璐ワ紝杩斿洖 `recognition failed`銆?
5+
## Workflow
6+
1. Preprocess: Gaussian blur -> Grayscale -> Binarize (OTSU) -> Canny edges -> Affine correction
7+
2. Locate: Morphology coarse + HSV color-space fine localization
8+
3. Segment: Vertical projection character segmentation
9+
4. Recognize: Dual-channel Tesseract OCR (original + GaussianBlur kernel=5), length-priority selection
10+
5. Verify: confidence < 0.5 triggers human review; 0.5-0.85 triggers LLM re-check
1211

13-
## 娉ㄦ剰浜嬮」
14-
- 浼樺厛杩斿洖 7 瀛楃瀹屾暣杞︾墝锛堝惈鐪佷唤锛?
15-
- 娣锋穯瀛楃瀵圭収锛?B/8, 0/O, 2/Z, 5/S, 1/I, C/G, E/F
16-
- 鎻愬彇 Tesseract 杈撳嚭涓殑鏈夋晥瀛楃锛屾护闄ょ┖鏍煎拰鏍囩偣
12+
## Output Format
13+
Return the plain-text plate number, e.g. "京A12345".
14+
If recognition fails, return "recognition failed".
15+
16+
## Notes
17+
- Prefer 7-character complete plates (with province prefix)
18+
- Confusion character mapping: B/8, 0/O, 2/Z, 5/S, 1/I, C/G, E/F
19+
- Filter valid characters from Tesseract output; strip spaces and punctuation

examples/optimization/eval_optimize_loop/run_pipeline.py

Lines changed: 135 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"""Eval-Optimize Loop CLI entry point.
33
44
Usage:
5-
python run_pipeline.py # fake mode
5+
python run_pipeline.py # fake mode (fast smoke test)
66
python run_pipeline.py --mode real # real mode (needs PlateAgent)
77
python run_pipeline.py --max-iter 3 # max optimization iterations
88
"""
99

10-
import argparse, asyncio, json, sys, time
10+
import argparse, asyncio, json, os as _os, sys, time
1111
from pathlib import Path
1212
from datetime import datetime, timezone
1313

@@ -28,6 +28,16 @@ def load_config():
2828
return json.load(f)
2929

3030

31+
def _read_critical_case_ids(val_path: Path) -> list[str]:
32+
"""Dynamically read critical case ids from evalset (fixed: was hardcoded)."""
33+
try:
34+
with open(val_path, "r", encoding="utf-8") as f:
35+
data = json.load(f)
36+
return [c["case_id"] for c in data.get("cases", []) if c.get("critical", False)]
37+
except Exception:
38+
return ["val_001"] # fallback
39+
40+
3141
async def main():
3242
parser = argparse.ArgumentParser(description="Eval-Optimize Loop Pipeline")
3343
parser.add_argument("--mode", default="fake", choices=["fake", "real", "real-agent", "trace"])
@@ -44,105 +54,135 @@ async def main():
4454
val_path = Path(args.val) if args.val else BASE_DIR / "config" / "val.evalset.json"
4555
output_dir = Path(args.output) if args.output else BASE_DIR / "output"
4656
started_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
57+
run_mode = args.mode
4758

48-
49-
# ---- 并发锁:防止多个 pipeline 实例同时写入 ----
50-
import os as _os
59+
# ---- concurrent lock (mkdir atomic) ----
5160
LOCK_DIR = _os.path.join(str(BASE_DIR), "output", ".pipeline.lock")
5261
try:
5362
_os.makedirs(LOCK_DIR, exist_ok=False)
5463
except FileExistsError:
55-
print("另一个 pipeline 实例正在运行,停止本次任务", file=sys.stderr)
64+
print("another pipeline instance is running, aborting", file=sys.stderr)
5665
sys.exit(75)
57-
# ---------------------------------------------------
58-
59-
if not args.quiet:
60-
print(f"Eval-Optimize Loop | mode={args.mode} seed={args.seed}")
61-
print()
62-
63-
# Phase 1: Baseline
64-
if not args.quiet: print("[1/6] Baseline...")
65-
br = BaselineRunner(mode="fake")
66-
baseline = await br.run(train_path, val_path)
67-
train_bl, val_bl = baseline["train"], baseline["val"]
68-
if not args.quiet:
69-
print(f" train: {train_bl.summary.pass_rate:.1%} val: {val_bl.summary.pass_rate:.1%}")
70-
71-
# Phase 2: Attribution
72-
if not args.quiet: print("[2/6] Attribution...")
73-
ar = AttributionRunner()
74-
attr = ar.run(train_bl, val_bl)
75-
if not args.quiet:
76-
p = attr.primary_failure_category
77-
print(f" failures: {attr.total_failures} primary: {p.category if p else 'none'}")
78-
79-
# Phase 3: Optimization
80-
if not args.quiet: print("[3/6] Optimization...")
81-
opt_runner = OptimizationRunner(mode="fake", config=config.get("pipeline", {}))
82-
opt_result = opt_runner.run(attr)
83-
if not args.quiet: print(f" candidates: {opt_result.total_iterations}")
84-
85-
# Phase 4: Validation
86-
if not args.quiet: print("[4/6] Validation...")
87-
vr = ValidationRunner(mode="fake")
88-
val_result = vr.run(val_bl, opt_result)
89-
if not args.quiet: print(f" delta: {val_result.summary.avg_score_delta:+.3f}")
90-
91-
# Phase 5: Gate
92-
if not args.quiet: print("[5/6] Gate...")
93-
gate = AcceptanceGate(config.get("gate", {}))
94-
decision = gate.decide(
95-
baseline_scores=val_bl.score_map,
96-
candidate_scores=val_result.score_map,
97-
baseline_train_scores=train_bl.score_map,
98-
candidate_train_scores=train_bl.score_map,
99-
baseline_cost=val_bl.summary.avg_cost * val_bl.summary.total,
100-
candidate_cost=val_result.summary.total_cost_candidate,
101-
critical_case_ids=["val_001"],
102-
)
103-
gate_dict = {
104-
"accepted": decision.accepted,
105-
"reason": decision.reason,
106-
"checks": [{"name": c.name, "passed": c.passed, "detail": c.detail} for c in decision.checks],
107-
}
108-
if not args.quiet: print(f" decision: {'ACCEPTED' if decision.accepted else 'REJECTED'}")
109-
110-
# Phase 6: Audit
111-
if not args.quiet: print("[6/6] Audit...")
112-
auditor = Auditor(output_dir=output_dir)
113-
trail = auditor.build_trail(
114-
pipeline_name="PlateAgent Eval-Optimize Loop",
115-
mode=args.mode, random_seed=args.seed,
116-
optimization=opt_result, baseline_val=val_bl,
117-
validation=val_result, gate_decision=gate_dict,
118-
started_at=started_at,
119-
)
120-
audit_path = auditor.save(
121-
audit_trail=trail, baseline=baseline, attribution=attr,
122-
optimization=opt_result, validation=val_result, gate_decision=gate_dict,
123-
)
124-
125-
# Standalone reports
126-
report_dir = output_dir / "reports"
127-
report_dir.mkdir(parents=True, exist_ok=True)
128-
generate_json_report(train_bl, val_bl, attr, opt_result, val_result, gate_dict,
129-
report_dir / "optimization_report.json")
130-
generate_markdown_report(train_bl, val_bl, attr, opt_result, val_result, gate_dict,
131-
report_dir / "optimization_report.md")
132-
133-
if not args.quiet:
134-
print(f" audit: {audit_path}")
135-
print(f" reports: {report_dir}")
136-
print("Done. 6 phases completed.")
137-
138-
139-
140-
141-
# 释放并发锁
66+
67+
# ---- try/finally: ensure lock release even on exception (fix: Critical) ----
14268
try:
143-
_os.rmdir(LOCK_DIR)
144-
except Exception:
145-
pass
69+
if not args.quiet:
70+
print(f"Eval-Optimize Loop | mode={run_mode} seed={args.seed}")
71+
print()
72+
73+
# Phase 1: Baseline
74+
if not args.quiet: print("[1/6] Baseline...")
75+
br = BaselineRunner(mode=run_mode if run_mode in ("real",) else "fake",
76+
plate_agent_root=str(BASE_DIR.parent.parent.parent / "plate-agent"))
77+
baseline = await br.run(train_path, val_path)
78+
train_bl, val_bl = baseline["train"], baseline["val"]
79+
if not args.quiet:
80+
print(f" train: {train_bl.summary.pass_rate:.1%} val: {val_bl.summary.pass_rate:.1%}")
81+
82+
# Phase 2: Attribution
83+
if not args.quiet: print("[2/6] Attribution...")
84+
ar = AttributionRunner()
85+
attr = ar.run(train_bl, val_bl)
86+
if not args.quiet:
87+
p = attr.primary_failure_category
88+
print(f" failures: {attr.total_failures} primary: {p.category if p else 'none'}")
89+
90+
# Phase 3: Optimization
91+
if not args.quiet: print("[3/6] Optimization...")
92+
use_real_agent = run_mode == "real-agent"
93+
if use_real_agent:
94+
sdk_train = BASE_DIR / "config" / "train.sdk.evalset.json"
95+
sdk_val = BASE_DIR / "config" / "val.sdk.evalset.json"
96+
sdk_opt = BASE_DIR / "config" / "optimizer.sdk.json"
97+
prompt_dir = BASE_DIR / "config" / "prompts"
98+
from src.call_agent import echo_call_agent
99+
opt_runner = OptimizationRunner(
100+
mode="real",
101+
config=config.get("pipeline", {}),
102+
call_agent=echo_call_agent,
103+
train_dataset=str(sdk_train),
104+
validation_dataset=str(sdk_val),
105+
sdk_config_path=str(sdk_opt),
106+
prompt_dir=str(prompt_dir),
107+
output_dir=str(output_dir / "optimizer"),
108+
)
109+
else:
110+
opt_runner = OptimizationRunner(mode="fake", config=config.get("pipeline", {}))
111+
opt_result = opt_runner.run(attr)
112+
if not args.quiet: print(f" candidates: {opt_result.total_iterations}")
113+
114+
# Phase 4: Validation
115+
if not args.quiet: print("[4/6] Validation...")
116+
vr = ValidationRunner(mode=run_mode if run_mode in ("real",) else "fake")
117+
val_result = vr.run(val_bl, opt_result)
118+
if not args.quiet: print(f" delta: {val_result.summary.avg_score_delta:+.3f}")
119+
120+
# Phase 5: Gate
121+
if not args.quiet: print("[5/6] Gate...")
122+
gate = AcceptanceGate(config.get("gate", {}))
123+
124+
# fix: overfit detection — run candidate on train set for real delta
125+
if run_mode == "real":
126+
candidate_train = await br.run_split(train_path, "train_candidate")
127+
candidate_train_scores = candidate_train.score_map
128+
else:
129+
# fake mode: derive candidate train scores from baseline + simulated delta
130+
candidate_train_scores = {
131+
cid: min(1.0, score + 0.05) for cid, score in train_bl.score_map.items()
132+
}
133+
134+
critical_case_ids = _read_critical_case_ids(val_path)
135+
136+
decision = gate.decide(
137+
baseline_scores=val_bl.score_map,
138+
candidate_scores=val_result.score_map,
139+
baseline_train_scores=train_bl.score_map,
140+
candidate_train_scores=candidate_train_scores,
141+
baseline_cost=val_bl.summary.avg_cost * val_bl.summary.total,
142+
candidate_cost=val_result.summary.total_cost_candidate,
143+
critical_case_ids=critical_case_ids,
144+
)
145+
gate_dict = {
146+
"accepted": decision.accepted,
147+
"reason": decision.reason,
148+
"checks": [{"name": c.name, "passed": c.passed, "detail": c.detail} for c in decision.checks],
149+
}
150+
if not args.quiet: print(f" decision: {'ACCEPTED' if decision.accepted else 'REJECTED'}")
151+
152+
# Phase 6: Audit
153+
if not args.quiet: print("[6/6] Audit...")
154+
auditor = Auditor(output_dir=output_dir)
155+
trail = auditor.build_trail(
156+
pipeline_name="PlateAgent Eval-Optimize Loop",
157+
mode=run_mode, random_seed=args.seed,
158+
optimization=opt_result, baseline_val=val_bl,
159+
validation=val_result, gate_decision=gate_dict,
160+
started_at=started_at,
161+
)
162+
audit_path = auditor.save(
163+
audit_trail=trail, baseline=baseline, attribution=attr,
164+
optimization=opt_result, validation=val_result, gate_decision=gate_dict,
165+
)
166+
167+
report_dir = output_dir / "reports"
168+
report_dir.mkdir(parents=True, exist_ok=True)
169+
generate_json_report(train_bl, val_bl, attr, opt_result, val_result, gate_dict,
170+
report_dir / "optimization_report.json")
171+
generate_markdown_report(train_bl, val_bl, attr, opt_result, val_result, gate_dict,
172+
report_dir / "optimization_report.md")
173+
174+
if not args.quiet:
175+
print(f" audit: {audit_path}")
176+
print(f" reports: {report_dir}")
177+
print("Done. 6 phases completed.")
178+
179+
finally:
180+
# release lock — always runs, even on exception
181+
try:
182+
_os.rmdir(LOCK_DIR)
183+
except Exception:
184+
pass
185+
146186

147187
if __name__ == "__main__":
148-
asyncio.run(main())
188+
asyncio.run(main())

examples/optimization/eval_optimize_loop/src/baseline.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Phase 1: Baseline 评测引擎。
1+
"""Phase 1: Baseline 评测引擎。
22
33
对训练集和验证集进行 baseline 评测,记录每条的 metric 分、pass/fail、
44
失败原因和关键轨迹,作为后续优化流水线的基准线。
@@ -18,6 +18,7 @@
1818
import json
1919
import time
2020
from dataclasses import dataclass, field
21+
import hashlib
2122
from pathlib import Path
2223
from typing import Optional
2324

@@ -331,7 +332,7 @@ async def _run_real_split(
331332
gt_items = []
332333
for case in cases_data:
333334
gt_items.append({
334-
"id": hash(case["case_id"]) % 10000,
335+
"id": int(hashlib.sha256(case["case_id"].encode()).hexdigest()[:8], 16) % 10000 + 1,
335336
"image": f"eval/dataset/test_plates/{case['image']}",
336337
"plate_number": case["ground_truth"],
337338
"conditions": case.get("conditions", {}),
@@ -351,9 +352,12 @@ async def _run_real_split(
351352
report = await evaluator.run(verbose=False)
352353

353354
# 转换为 BaselineCaseResult 列表
355+
# fix: use path-based mapping instead of image_id positional lookup
356+
image_to_case = {c.get("image", ""): c["case_id"] for c in cases_data}
354357
case_results: list[BaselineCaseResult] = []
355358
for r in report.details:
356-
case_id = cases_data[r.image_id - 1]["case_id"] if r.image_id <= len(cases_data) else f"case_{r.image_id}"
359+
image_key = Path(r.image_path).name if r.image_path else ""
360+
case_id = image_to_case.get(image_key, image_to_case.get(r.image_path, f"case_{getattr(r, 'image_id', '?')}" ))
357361
case_result = BaselineCaseResult(
358362
case_id=case_id,
359363
image=r.image_path,

0 commit comments

Comments
 (0)