"""Pipeline Step 17 — V4 (template-fit) 를 V3 Top-K 가 아니라 32 프레임 전체에 적용. 기존 V4 r2 는 V3 Top-5 만 평가 → 5 개 중 선택. 이 스크립트는 32 프레임 전체에 template-fit 적용 → confidence 기준 전체 랭킹. V1/V2/V3 처럼 "32 중 Top-3" 형태 산출. 출력: v4_full32_result.yaml """ import datetime import sys from pathlib import Path import yaml HERE = Path(__file__).parent sys.path.insert(0, str(HERE)) from phase_common import load_32_frames, load_frame_index from detect_mdx import detect_mdx_analysis from template_fit import ( load_templates_v1, collect_anchor_vocab, compute_template_fit, route, ) from embeddings import embed_texts, cosine from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR OUT_PATH = HERE / 'v4_full32_result.yaml' def extract_mdx_raw(sid): cfg = MDX_SECTIONS[sid] p = MDX_DIR / cfg['file'] lines = p.read_text(encoding='utf-8').split('\n') start = None for i, ln in enumerate(lines): if ln.strip() == cfg['start'].strip(): start = i break end = len(lines) if cfg.get('end_prefix'): for i in range(start + 1, len(lines)): if lines[i].strip().startswith(cfg['end_prefix']): end = i break section = lines[start:end] return section[0].lstrip('#').strip(), '\n'.join(section) def main(): v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8')) answer_map = v1['meta']['answer_map'] holdout = v1['meta']['holdout_sections'] templates = load_templates_v1() anchor_vocab = collect_anchor_vocab(templates) frames = load_32_frames() idx_data, frame_to_short = load_frame_index() fids = list(frames.keys()) frame_num_map = {fid: int(frame_to_short[fid]) for fid in fids} frame_contents = [frames[fid].get('content', '') for fid in fids] print('[V4 full-32] 32 frame content 임베딩 중...') frame_vecs = embed_texts(frame_contents) out_sections = {} for sid in v1['mdx_sections']: title, raw_text = extract_mdx_raw(sid) mdx_analysis = detect_mdx_analysis(raw_text, title, anchor_vocab=anchor_vocab) mdx_summary = mdx_analysis['summary'] mdx_vec = embed_texts([mdx_summary])[0] judgments = [] for i, fid in enumerate(fids): if fid not in templates: continue template = templates[fid] content_emb = max(0.0, min(1.0, float(cosine(mdx_vec, frame_vecs[i])))) fit = compute_template_fit(mdx_analysis, template, content_emb) label = route(fit['confidence'], fit['axes'], fit['adaptation'], fit['not_suits']) judgments.append({ 'frame_id': fid, 'frame_number': frame_num_map[fid], 'template_id': template.get('template_id'), 'confidence': round(float(fit['confidence']), 4), 'base': round(float(fit['base']), 4), 'penalty': round(float(fit['total_penalty']), 4), 'label': label, 'content_embedding': round(content_emb, 4), 'axes': { 'anchor': round(float(fit['axes']['anchor']['score']), 4), 'cardinality': round(float(fit['axes']['cardinality']), 4), 'relation': round(float(fit['axes']['relation']), 4), 'slot': round(float(fit['axes']['slot']), 4), 'content': round(float(fit['axes']['content']), 4), }, }) # confidence 내림차순 judgments.sort(key=lambda x: -x['confidence']) for new_rank, item in enumerate(judgments, start=1): item['v4_full_rank'] = new_rank ans = answer_map.get(sid) out_sections[sid] = { 'mdx_title': title, 'answer_frame_number': ans, 'is_holdout': sid in holdout, 'judgments_full32': judgments, 'usable_count': sum(1 for j in judgments if j['label'] != 'reject'), 'reject_count': sum(1 for j in judgments if j['label'] == 'reject'), } out = { 'meta': { 'pipeline_step': '8.v4.full32', 'description': 'V4 template-fit 을 V3 Top-K 가 아니라 32 프레임 전체에 적용', 'note': '기존 v4_template_fit_r2_result.yaml 는 V3 Top-5 만 평가. 이 파일은 32 전체.', 'generated_at': datetime.datetime.now().isoformat(timespec='seconds'), 'answer_map': answer_map, 'holdout_sections': holdout, }, 'mdx_sections': out_sections, } OUT_PATH.write_text( yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000), encoding='utf-8', ) print('=' * 70) print(f'V4 full-32 평가 완료: {OUT_PATH}') print('=' * 70) print(f'\n각 섹션별 사용 가능 프레임 (label != reject) 개수 + Top-3:') for sid, s in out_sections.items(): ans = s.get('answer_frame_number') ans_str = f'(정답 {ans})' if ans else '(holdout)' print(f'\n[{sid}] {ans_str} 사용가능 {s["usable_count"]}/{s["usable_count"]+s["reject_count"]}') usable = [j for j in s['judgments_full32'] if j['label'] != 'reject'] for j in usable[:3]: mark = ' 🎯' if ans and j['frame_number'] == ans else '' print(f' Frame {j["frame_number"]:>2}{mark} conf {j["confidence"]:.3f} {j["label"]}') if not usable: print(' (사용 가능 후보 없음 — 모두 reject)') if __name__ == '__main__': main()