"""Source-only short-summary consensus; never accepts reference summaries.""" from __future__ import annotations import re from collections import Counter SHORT_SUMMARY_PROMPT = ( "다음 전기 문단의 중심 사건 또는 주제를 한국어 한 문장, 50자 이내로 요약하세요. " "핵심 인물과 행동 또는 원인·결과를 보존하고 원문의 표현을 가능한 한 유지하세요. " "배경 설명과 수식은 줄이고 원문에 없는 사실은 쓰지 마세요. 제목·목록 없이 요약만 출력하세요." ) def character_f1(a: str, b: str) -> float: def grams(text): text = re.sub(r"\s+", "", text) return Counter(text[i:i + 2] for i in range(len(text) - 1)) x, y = grams(a), grams(b) total = sum(x.values()) + sum(y.values()) return 2 * sum((x & y).values()) / total if total else 0.0 def word_f1(a: str, b: str) -> float: def grams(text): words = text.split() return Counter(zip(words, words[1:])) x, y = grams(a), grams(b) total = sum(x.values()) + sum(y.values()) return 2 * sum((x & y).values()) / total if total else 0.0 def select_consensus(summaries: list[str], word_weight: float = 0.0) -> int: """Select a medoid among independently generated candidates, under 50 chars. Consensus measures stability, not truth: systematic model errors can survive. Return -1 if all candidates are empty/overlength instead of silently truncating. """ if not 0 <= word_weight <= 1: raise ValueError("word_weight must be between 0 and 1") eligible = [i for i, s in enumerate(summaries) if s.strip() and len(s.strip()) <= 50] if not eligible: return -1 return max(eligible, key=lambda i: ( sum((1 - word_weight) * character_f1(summaries[i], summaries[j]) + word_weight * word_f1(summaries[i], summaries[j]) for j in eligible if j != i), -i)) def generate_candidates(client, text: str, count: int = 5) -> list[dict]: if not text.strip(): raise ValueError("Source text must not be empty") if not 1 <= count <= 5: raise ValueError("Candidate count must be between 1 and 5") response = client.chat.completions.create( model="gpt-4o", temperature=0.3, max_tokens=100, n=count, messages=[{"role": "system", "content": "당신은 텍스트 요약 전문가입니다."}, {"role": "user", "content": SHORT_SUMMARY_PROMPT + "\n\n" + text}], ) return [{"summary": (c.message.content or "").strip() if c.finish_reason == "stop" else "", "finish_reason": c.finish_reason, "model": response.model, "response_id": response.id} for c in response.choices] def summarize_short(client, text: str, word_weight: float = 0.0) -> dict: """Opt-in 50-character summary; five completions incur additional API cost. Return an explicit invalid result if no complete length-compliant summary exists. Callers should inspect valid, not silently present an empty result as success. """ candidates = generate_candidates(client, text) chosen = select_consensus([c["summary"] for c in candidates], word_weight=word_weight) return {"summary": candidates[chosen]["summary"] if chosen >= 0 else "", "valid": chosen >= 0, "selected_index": chosen, "strategy": "source_only_consensus5", "word_weight": word_weight, "candidates": candidates}