| 234 | |
| 235 | |
| 236 | def score_semantic_quality( |
| 237 | text: str, |
| 238 | *, |
| 239 | source_text: str, |
| 240 | query_text: str = "", |
| 241 | policy: QualityFallbackPolicy | None = None, |
| 242 | ) -> tuple[bool, float, str]: |
| 243 | policy = policy or QualityFallbackPolicy() |
| 244 | candidate = text.strip() |
| 245 | if not candidate: |
| 246 | return False, 0.0, "empty" |
| 247 | |
| 248 | lowered = candidate.lower() |
| 249 | for marker in policy.reject_markers: |
| 250 | if marker in lowered: |
| 251 | return False, 0.0, f"reject_marker:{marker}" |
| 252 | |
| 253 | alpha_chars = sum(1 for ch in candidate if ch.isalpha()) |
| 254 | if len(candidate) < policy.min_chars: |
| 255 | return False, 0.15, "too_short" |
| 256 | if alpha_chars < policy.min_alpha_chars: |
| 257 | return False, 0.2, "too_sparse" |
| 258 | |
| 259 | source_tokens = max(1, estimate_tokens(source_text)) |
| 260 | candidate_tokens = estimate_tokens(candidate) |
| 261 | ratio = candidate_tokens / source_tokens |
| 262 | if ratio < policy.min_source_ratio: |
| 263 | return False, min(0.3, ratio / max(policy.min_source_ratio, 1e-6)), "ratio_too_small" |
| 264 | if ratio > policy.max_source_ratio: |
| 265 | return False, max(0.1, 1.0 - ratio), "ratio_too_large" |
| 266 | |
| 267 | overlap_required = policy.min_query_overlap_terms |
| 268 | overlap = _query_overlap_terms(candidate, query_text) |
| 269 | if overlap_required > 0 and overlap < overlap_required: |
| 270 | return False, 0.35, "low_query_overlap" |
| 271 | |
| 272 | ratio_score = min(1.0, ratio / max(policy.min_source_ratio, 1e-6)) |
| 273 | overlap_score = 1.0 if overlap_required == 0 else min(1.0, overlap / overlap_required) |
| 274 | quality = min(1.0, 0.5 + min(ratio_score, 1.0) * 0.25 + overlap_score * 0.25) |
| 275 | return True, quality, "ok" |
| 276 | |
| 277 | |
| 278 | def _query_overlap_terms(candidate: str, query_text: str) -> int: |