Compare commits
5
Commits
7d1876b799
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e48dd73fdf | ||
|
|
547836e04b | ||
|
|
df128508a3 | ||
|
|
81bf44c6cc | ||
|
|
544d858f62 |
@@ -40,6 +40,19 @@ def _get(base: str, slug: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
return r.json()
|
||||
|
||||
|
||||
def get_replica(slug: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""The digital-replica markdown for a paper (P4): {slug, title, n_questions, total_marks, markdown,
|
||||
questions:[{label, marks, markdown}]}. Raises ExtractError if the paper has no replica yet."""
|
||||
base = service_url()
|
||||
if not base:
|
||||
raise ExtractError("EXAM_EXTRACT_URL not configured")
|
||||
r = requests.get(f"{base}/api/replica/{slug}", timeout=timeout)
|
||||
if r.status_code == 404:
|
||||
raise ExtractError(f"no digital replica for {slug}")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def extract_suggestions(slug: str, pdf_bytes: bytes, *, force: bool = False,
|
||||
poll_timeout: int = 1500, poll_interval: int = 5) -> Dict[str, Any]:
|
||||
"""POST the paper to the service and poll until the analyse contract is ready.
|
||||
|
||||
@@ -80,6 +80,9 @@ class ResponseAreaPayload(BaseModel):
|
||||
] = None
|
||||
# Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage).
|
||||
context_type: Optional[str] = None
|
||||
# Rich recognition payload (75-exam-marker-region-meta.sql): figure name/description, OMR geometry, unit…
|
||||
# Carried on canvas save so a named context figure survives a round-trip.
|
||||
meta: Optional[Dict[str, Any]] = None
|
||||
source: Literal["manual", "ai"] = "manual"
|
||||
confirmed: bool = True
|
||||
confidence: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
|
||||
@@ -745,6 +745,9 @@ def _map_service_contract_to_rows(template_id: str, contract: Dict[str, Any],
|
||||
"bounds": _frac_box_to_canvas(q.get("bounds"), q.get("page") or 1, pages),
|
||||
"page": q.get("page"), "source": "ai", "confirmed": False,
|
||||
"confidence": _safe_confidence(q.get("confidence")), "derivation": "extract-service",
|
||||
# analyse contract v2 (migration 78): command verb + stem prose per part
|
||||
"command_word": (q.get("command_word") or None) if not q.get("is_container") else None,
|
||||
"preamble": q.get("preamble") or None,
|
||||
})
|
||||
for q in questions: # FK safety: de-parent a dangling parent_id
|
||||
if q["parent_id"] and q["parent_id"] not in q_ids:
|
||||
@@ -793,9 +796,21 @@ def _run_service_extract_merge(ctx: ExamContext, template_id: str, pdf_bytes: by
|
||||
contract = exam_extract.extract_suggestions(slug, pdf_bytes)
|
||||
rows = _map_service_contract_to_rows(template_id, contract, pdf_bytes)
|
||||
_refresh_ai_rows(ctx, template_id, rows)
|
||||
n_pages = (contract.get("meta") or {}).get("n_pages") or (contract.get("meta") or {}).get("pages")
|
||||
meta = contract.get("meta") or {}
|
||||
# P3: record provenance + the audit cover-reconciliation gate so the setup UI can flag under-reads,
|
||||
# and store the slug so the digital-text view can resolve this paper's replica.
|
||||
updates: Dict[str, Any] = {"extraction_meta": {
|
||||
"engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {},
|
||||
"counts": {k: len(v) for k, v in rows.items()},
|
||||
# question-layout signals for the setup UI: paper sections (e.g. A-level Section A/B) + EITHER/OR choices
|
||||
"sections": meta.get("sections") or [],
|
||||
"choice_groups": meta.get("choice_groups") or [],
|
||||
"marks_confidence": meta.get("marks_confidence"),
|
||||
}}
|
||||
n_pages = meta.get("n_pages") or meta.get("pages")
|
||||
if n_pages:
|
||||
ctx.supabase.table("exam_templates").update({"page_count": n_pages}).eq("id", template_id).execute()
|
||||
updates["page_count"] = n_pages
|
||||
ctx.supabase.table("exam_templates").update(updates).eq("id", template_id).execute()
|
||||
return rows
|
||||
|
||||
|
||||
@@ -1034,6 +1049,24 @@ async def auto_map_status(
|
||||
return body
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}/digital-text")
|
||||
async def template_digital_text(
|
||||
template_id: str,
|
||||
ctx: ExamContext = Depends(get_exam_context),
|
||||
) -> Dict[str, Any]:
|
||||
"""P4: the digital-replica markdown for this template's paper (stem text, parts, marks, answer-space
|
||||
placeholders, spec refs). Resolves the paper via the slug the extraction used."""
|
||||
template = _fetch_template_or_404(ctx, template_id)
|
||||
_require_source_visibility_or_404(ctx, template)
|
||||
if not exam_extract.is_enabled():
|
||||
raise HTTPException(status_code=503, detail="Extraction service not configured")
|
||||
slug = (template.get("extraction_meta") or {}).get("slug") or _extract_slug(ctx, template)
|
||||
try:
|
||||
return exam_extract.get_replica(slug)
|
||||
except exam_extract.ExtractError as exc:
|
||||
raise HTTPException(status_code=404, detail=f"No digital text yet — run auto-map first ({exc})")
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}")
|
||||
async def replace_template(
|
||||
template_id: str,
|
||||
@@ -1114,6 +1147,7 @@ async def replace_template(
|
||||
"kind": ra.kind,
|
||||
"response_form": ra.response_form,
|
||||
"context_type": ra.context_type, # 73: optional Context differentiation
|
||||
"meta": ra.meta, # 75: rich recognition payload (name/description/OMR/…)
|
||||
"source": ra.source,
|
||||
"confirmed": ra.confirmed,
|
||||
"confidence": ra.confidence,
|
||||
|
||||
Reference in New Issue
Block a user