Import Human Voice Rewrite Demo (PRD v1.1) + Dockerfile for Coolify
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
"""Evidence validation for Diagnosis (PRD §31.2.1 / §35.2).
|
||||
|
||||
Evidence must appear in the corresponding Original Text. Unmatched
|
||||
evidence is discarded so the frontend never paints fictional anchors.
|
||||
Patterns without any verified evidence are kept in the diagnosis table
|
||||
but lose their evidence list (no fake highlights).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from schemas import Annotation, DiagnoseResponse, Pattern
|
||||
|
||||
_WS = re.compile(r"\s+")
|
||||
|
||||
PATTERN_CATEGORY: dict[str, str] = {
|
||||
"P01": "growth",
|
||||
"P02": "growth",
|
||||
"P03": "rhetoric",
|
||||
"P04": "repeat",
|
||||
"P05": "rhetoric",
|
||||
"P06": "repeat",
|
||||
"P07": "growth",
|
||||
"P08": "structure",
|
||||
"P09": "growth",
|
||||
"P10": "growth",
|
||||
"P11": "repeat",
|
||||
"P12": "rhetoric",
|
||||
}
|
||||
|
||||
KIND_LABEL: dict[str, str] = {
|
||||
"rhetoric": "修辞包装",
|
||||
"repeat": "重复 / 工整",
|
||||
"growth": "抽象 / 总结",
|
||||
"structure": "结构 / 路标",
|
||||
}
|
||||
|
||||
VALID_CATEGORIES = set(KIND_LABEL)
|
||||
|
||||
|
||||
def normalize(s: str) -> str:
|
||||
return _WS.sub(" ", s).strip().lower()
|
||||
|
||||
|
||||
def evidence_in_text(evidence: str, text: str) -> bool:
|
||||
ev = normalize(evidence)
|
||||
if not ev:
|
||||
return False
|
||||
return ev in normalize(text)
|
||||
|
||||
|
||||
def verified_evidence(items: list[str], texts: list[str]) -> list[str]:
|
||||
haystacks = [normalize(t) for t in texts]
|
||||
hits: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in items:
|
||||
ev = normalize(raw)
|
||||
if not ev or ev in seen:
|
||||
continue
|
||||
if any(ev in h for h in haystacks):
|
||||
hits.append(raw)
|
||||
seen.add(ev)
|
||||
return hits
|
||||
|
||||
|
||||
def category_of(pattern_id: str, fallback: str = "") -> str:
|
||||
cat = (fallback or "").strip().lower()
|
||||
if cat in VALID_CATEGORIES:
|
||||
return cat
|
||||
return PATTERN_CATEGORY.get((pattern_id or "").upper(), "rhetoric")
|
||||
|
||||
|
||||
def sanitize_patterns(patterns: list[Pattern], paragraphs: list[str]) -> list[Pattern]:
|
||||
"""Keep patterns, but drop evidence that cannot be found in the original."""
|
||||
out: list[Pattern] = []
|
||||
for pat in patterns:
|
||||
hits = verified_evidence(pat.evidence, paragraphs) if pat.evidence else []
|
||||
cat = category_of(pat.pattern_id, pat.category)
|
||||
out.append(pat.model_copy(update={"evidence": hits, "category": cat}))
|
||||
return out
|
||||
|
||||
|
||||
def sanitize_annotation(note: Annotation, paragraph_text: str, index: int) -> Annotation | None:
|
||||
hits = verified_evidence(note.evidence, [paragraph_text]) if note.evidence else []
|
||||
if not hits:
|
||||
return None
|
||||
cat = category_of(note.pattern_id, note.category)
|
||||
return note.model_copy(
|
||||
update={
|
||||
"annotation_id": note.annotation_id or f"a{index + 1}",
|
||||
"evidence": hits,
|
||||
"category": cat,
|
||||
"kind_label": note.kind_label or KIND_LABEL.get(cat, "修辞包装"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def sanitize_diagnosis(resp: DiagnoseResponse, paragraphs: list[str]) -> DiagnoseResponse:
|
||||
"""Filter fictional evidence and cap per-paragraph annotations at 3."""
|
||||
patterns = sanitize_patterns(resp.patterns, paragraphs)
|
||||
briefs = []
|
||||
for brief in resp.paragraph_briefs:
|
||||
idx = _paragraph_index(brief.paragraph_id)
|
||||
text = paragraphs[idx] if 0 <= idx < len(paragraphs) else ""
|
||||
notes: list[Annotation] = []
|
||||
for i, note in enumerate(brief.annotations):
|
||||
cleaned = sanitize_annotation(note, text, i)
|
||||
if cleaned:
|
||||
notes.append(cleaned)
|
||||
if len(notes) >= 3:
|
||||
break
|
||||
briefs.append(brief.model_copy(update={"annotations": notes}))
|
||||
return resp.model_copy(update={"patterns": patterns, "paragraph_briefs": briefs})
|
||||
|
||||
|
||||
def _paragraph_index(pid: str) -> int:
|
||||
digits = "".join(ch for ch in (pid or "") if ch.isdigit())
|
||||
if not digits:
|
||||
return -1
|
||||
return int(digits) - 1
|
||||
Reference in New Issue
Block a user