from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.orm import Session from app.ai.pose_analyzer import analyze_task from app.config import settings from app.db.models import AnalysisTask, Report, Video from app.mock_data import MOCK_MOVEMENT_REPORT, MOCK_REPORT from app.queue.analysis_queue import get_analysis_queue from app.schemas.models import AnalysisTaskCreateRequest, TaskStatus, TaskType from app.services import oss as oss_service from app.services.videos import verify_child_owner DISCLAIMER_SCREENING = "本报告用于健康管理建议,不构成医疗诊断。" DISCLAIMER_MOVEMENT = "本报告用于运动训练反馈,不构成医疗诊断。" def task_to_dict(task: AnalysisTask) -> dict: report_id = None if task.result and isinstance(task.result, dict): report_id = task.result.get("reportId") return { "id": task.id, "status": task.status, "progress": task.progress, "errorCode": task.error_code, "reportId": report_id, "retryCount": task.retry_count, } def build_screening_report_payload(child_id: int, task_id: int) -> dict: data = {**MOCK_REPORT, "childId": child_id, "taskId": task_id} return data def build_movement_report_payload(child_id: int, task_id: int) -> dict: return {**MOCK_MOVEMENT_REPORT, "childId": child_id, "taskId": task_id} def create_report_for_task(db: Session, task: AnalysisTask, analysis: dict) -> Report: report_data = analysis.get("report", {}) if task.task_type == TaskType.movement_scoring.value: payload = {**MOCK_MOVEMENT_REPORT, **report_data} report = Report( child_id=task.child_id, task_id=task.id, report_type="movement_scoring", risk_level="medium", summary="跟练打卡报告已生成", metrics=[payload], recommendations=["继续保持训练节奏", "28 天后安排复测"], disclaimer=DISCLAIMER_MOVEMENT, status="published", ) else: payload = {**MOCK_REPORT, **report_data, "childId": task.child_id, "taskId": task.id} risk_level = payload.get("riskLevel", "medium") report_status = "pending_review" if risk_level in ("high", "review_required") else "published" report = Report( child_id=task.child_id, task_id=task.id, report_type="posture_screening", risk_level=risk_level, summary=payload.get("summary", MOCK_REPORT["summary"]), metrics=payload.get("metrics", MOCK_REPORT["metrics"]), recommendations=payload.get("recommendations", MOCK_REPORT["recommendations"]), disclaimer=payload.get("disclaimer", DISCLAIMER_SCREENING), status=report_status, ) db.add(report) db.flush() return report def process_analysis_task(db: Session, task_id: int) -> AnalysisTask | None: task = db.get(AnalysisTask, task_id) if not task or task.status in (TaskStatus.SUCCEEDED.value, TaskStatus.CANCELLED.value): return task task.status = TaskStatus.PROCESSING.value task.progress = 35 task.started_at = datetime.now(timezone.utc) db.commit() video = db.get(Video, task.video_id) video_path = None if video: video_path = oss_service.resolve_local_path(video.object_key) analysis = analyze_task(task.task_type, video_path) task.model_version = analysis.get("modelVersion", "mock-v1") task.progress = 80 db.commit() report = create_report_for_task(db, task, analysis) task.status = TaskStatus.SUCCEEDED.value task.progress = 100 task.finished_at = datetime.now(timezone.utc) task.result = { "reportId": report.id, "engine": analysis.get("engine"), "confidence": analysis.get("confidence"), } db.commit() db.refresh(task) return task def enqueue_task(task_id: int, db: Session | None = None) -> None: queue = get_analysis_queue() queue.push(task_id) if not settings.analysis_inline_process: return if db is not None: process_analysis_task(db, task_id) return from app.db.session import SessionLocal local_db = SessionLocal() try: process_analysis_task(local_db, task_id) finally: local_db.close() def create_analysis_task( db: Session, user_id: int, body: AnalysisTaskCreateRequest, idempotency_key: str | None, ) -> tuple[AnalysisTask | None, str | None, AnalysisTask | None]: child = verify_child_owner(db, user_id, body.child_id) if not child: return None, "child_not_found", None video = db.get(Video, body.video_id) if not video or video.child_id != body.child_id: return None, "video_not_found", None if idempotency_key: existing = db.scalar( select(AnalysisTask).where( AnalysisTask.child_id == body.child_id, AnalysisTask.video_id == body.video_id, AnalysisTask.task_type == body.task_type.value, AnalysisTask.idempotency_key == idempotency_key, ) ) if existing: return existing, "duplicate", existing task = AnalysisTask( child_id=body.child_id, video_id=body.video_id, task_type=body.task_type.value, status=TaskStatus.QUEUED.value, progress=0, idempotency_key=idempotency_key, ) db.add(task) db.commit() db.refresh(task) enqueue_task(task.id, db=db) db.refresh(task) return task, None, None def get_task_for_user(db: Session, user_id: int, task_id: int) -> AnalysisTask | None: task = db.get(AnalysisTask, task_id) if not task: return None child = verify_child_owner(db, user_id, task.child_id) if not child: return None return task def cancel_task(db: Session, user_id: int, task_id: int) -> AnalysisTask | None: task = get_task_for_user(db, user_id, task_id) if not task: return None if task.status in (TaskStatus.SUCCEEDED.value, TaskStatus.CANCELLED.value): return task task.status = TaskStatus.CANCELLED.value task.finished_at = datetime.now(timezone.utc) db.commit() db.refresh(task) return task def handle_webhook(db: Session, task_id: int, status: str) -> None: task = db.get(AnalysisTask, task_id) if not task: return if status == TaskStatus.SUCCEEDED.value: process_analysis_task(db, task_id) elif status == TaskStatus.FAILED.value: task.status = TaskStatus.FAILED.value task.error_code = "worker_failed" task.finished_at = datetime.now(timezone.utc) db.commit()