修复错题一直显示处理中:超时、自动刷新与状态更新。
This commit is contained in:
@@ -19,6 +19,9 @@ class Settings(BaseSettings):
|
||||
FRONTEND_DIST: str = "../frontend/dist"
|
||||
ADMIN_DEFAULT_USERNAME: str = "admin"
|
||||
ADMIN_DEFAULT_PASSWORD: str = "admin123"
|
||||
OCR_TIMEOUT_SECONDS: int = 300
|
||||
AI_TIMEOUT_SECONDS: int = 600
|
||||
PROCESS_STALE_MINUTES: int = 20
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Query, UploadFile, status
|
||||
@@ -26,6 +28,28 @@ def _short_error(exc: BaseException, prefix: str = "") -> str:
|
||||
return f"{prefix}{msg}" if prefix else msg
|
||||
|
||||
|
||||
def _is_still_processing(wq: WrongQuestion) -> bool:
|
||||
if wq.status == WrongQuestionStatus.pending:
|
||||
return True
|
||||
if wq.status == WrongQuestionStatus.ocr_done and not wq.question_text and not wq.error_message:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _expire_stale_processing(wq: WrongQuestion, db: Session) -> None:
|
||||
if not _is_still_processing(wq):
|
||||
return
|
||||
created = wq.created_at
|
||||
if created.tzinfo is None:
|
||||
created = created.replace(tzinfo=timezone.utc)
|
||||
age = datetime.now(timezone.utc) - created
|
||||
if age <= timedelta(minutes=settings.PROCESS_STALE_MINUTES):
|
||||
return
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = f"处理超时(超过 {settings.PROCESS_STALE_MINUTES} 分钟),请点击「重新识别标注」重试"
|
||||
db.commit()
|
||||
|
||||
|
||||
def _parse_mark_regions(raw: str | None) -> list[dict] | None:
|
||||
if not raw:
|
||||
return None
|
||||
@@ -57,25 +81,41 @@ def _wq_to_out(wq: WrongQuestion) -> WrongQuestionOut:
|
||||
|
||||
|
||||
async def _run_ai_pipeline(wq: WrongQuestion, db: Session, ocr_lines: list[dict], ocr_text: str):
|
||||
import asyncio
|
||||
|
||||
subject_name = wq.subject.name if wq.subject else "综合"
|
||||
school_level = wq.student.school_level if wq.student else None
|
||||
olympiad = wq.category == WrongQuestionCategory.olympiad
|
||||
ai_cfg = llm_service.load_ai_config(db)
|
||||
image_full = str(Path(settings.UPLOAD_DIR) / wq.image_path)
|
||||
timeout = settings.AI_TIMEOUT_SECONDS
|
||||
|
||||
try:
|
||||
detect_resp = await asyncio.wait_for(
|
||||
llm_service.detect_wrong_line_ids(ai_cfg, subject_name, ocr_lines, school_level),
|
||||
timeout=min(90, timeout),
|
||||
)
|
||||
wrong_ids = annotation_service.parse_wrong_line_ids(detect_resp, ocr_lines)
|
||||
except Exception:
|
||||
wrong_ids = annotation_service.heuristic_wrong_line_ids(ocr_lines)
|
||||
|
||||
detect_resp = await llm_service.detect_wrong_line_ids(ai_cfg, subject_name, ocr_lines, school_level)
|
||||
wrong_ids = annotation_service.parse_wrong_line_ids(detect_resp, ocr_lines)
|
||||
regions = annotation_service.regions_from_lines(ocr_lines, wrong_ids)
|
||||
wq.mark_regions_json = json.dumps(regions, ensure_ascii=False)
|
||||
|
||||
ann_rel = ocr_service.annotated_rel_path(wq.image_path)
|
||||
wq.annotated_image_path = annotation_service.draw_annotated_image(
|
||||
image_full, ocr_lines, wrong_ids, ann_rel
|
||||
)
|
||||
db.commit()
|
||||
|
||||
question_text = await llm_service.format_question(ai_cfg, subject_name, ocr_text, school_level)
|
||||
solution_full = await llm_service.generate_solution(
|
||||
ai_cfg, subject_name, question_text, school_level, olympiad=olympiad
|
||||
question_text = await asyncio.wait_for(
|
||||
llm_service.format_question(ai_cfg, subject_name, ocr_text, school_level),
|
||||
timeout=timeout,
|
||||
)
|
||||
solution_full = await asyncio.wait_for(
|
||||
llm_service.generate_solution(
|
||||
ai_cfg, subject_name, question_text, school_level, olympiad=olympiad
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
approach, solution_body = annotation_service.split_solution_sections(solution_full)
|
||||
wq.question_text = question_text
|
||||
@@ -87,6 +127,7 @@ async def _run_ai_pipeline(wq: WrongQuestion, db: Session, ocr_lines: list[dict]
|
||||
|
||||
def _process_wrong_question(question_id: uuid.UUID):
|
||||
db = SessionLocal()
|
||||
wq = None
|
||||
try:
|
||||
wq = (
|
||||
db.query(WrongQuestion)
|
||||
@@ -100,7 +141,9 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
wq.error_message = None
|
||||
image_full = Path(settings.UPLOAD_DIR) / wq.image_path
|
||||
try:
|
||||
ocr_result = ocr_service.run_ocr_with_regions(str(image_full))
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
future = pool.submit(ocr_service.run_ocr_with_regions, str(image_full))
|
||||
ocr_result = future.result(timeout=settings.OCR_TIMEOUT_SECONDS)
|
||||
ocr_text = ocr_result["text"]
|
||||
ocr_lines = ocr_result["lines"]
|
||||
wq.ocr_raw_text = ocr_text or None
|
||||
@@ -111,6 +154,14 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
return
|
||||
wq.status = WrongQuestionStatus.ocr_done
|
||||
db.commit()
|
||||
except FuturesTimeout:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = (
|
||||
f"OCR 识别超时(>{settings.OCR_TIMEOUT_SECONDS}秒)。"
|
||||
" 首次加载模型较慢,请稍后点「重新识别标注」重试"
|
||||
)
|
||||
db.commit()
|
||||
return
|
||||
except Exception as exc:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
msg = _short_error(exc, "OCR 识别失败:")
|
||||
@@ -129,10 +180,18 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = _short_error(exc, "AI 处理失败:")
|
||||
detail = _short_error(exc, "AI 处理失败:")
|
||||
if "Timeout" in type(exc).__name__ or "timeout" in str(exc).lower():
|
||||
detail = "AI 处理超时,请检查 Ollama/OpenAI 是否可用后重试"
|
||||
wq.error_message = detail
|
||||
db.commit()
|
||||
finally:
|
||||
loop.close()
|
||||
except Exception as exc:
|
||||
if wq is not None:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = _short_error(exc, "处理失败:")
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -164,6 +223,8 @@ def list_wrong_questions(
|
||||
| (WrongQuestion.ocr_raw_text.ilike(pattern))
|
||||
)
|
||||
items = query.order_by(WrongQuestion.created_at.desc()).all()
|
||||
for w in items:
|
||||
_expire_stale_processing(w, db)
|
||||
return [_wq_to_out(w) for w in items]
|
||||
|
||||
|
||||
@@ -233,6 +294,7 @@ def get_wrong_question(
|
||||
)
|
||||
if wq is None or wq.student.user_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="错题不存在")
|
||||
_expire_stale_processing(wq, db)
|
||||
return _wq_to_out(wq)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user