Files
conai/backend/app/api/vision.py
sinmb79 48f1027f08 feat: Phase 2 구현 — AI 에이전트 4인방, EVMS, Vision AI, Geofence
AI 에이전트 (Layer 2):
- GONGSA: 공사 담당 (공정 브리핑, 공기 지연 감지, 날씨 연동 작업 조정)
- PUMJIL: 품질 담당 (시공 전 체크리스트, Vision 보조 판독, 시험 기한 추적)
- ANJEON: 안전 담당 (위험 공정 경보, TBM 생성, 중대재해처벌법 Q&A)
- GUMU: 공무 담당 (인허가 능동 추적, 기성청구 제안, 보고서 초안)
- 에이전트 라우터 (키워드 기반 자동 분배), 아침 브리핑 엔드포인트

EVMS 기본:
- PV·EV·AC·SPI·CPI 산출 (WBS/Task 기반)
- EAC·ETC 예측, 스냅샷 이력 저장

Vision AI:
- Level 1: 현장 사진 분류 (Claude Vision), 작업일보 자동 첨부
- Level 2: 안전장비(안전모/조끼) 착용 감지

Geofence 위험구역:
- 구역 CRUD (굴착면, 크레인 반경, 밀폐공간 등)
- 진입 이벤트 웹훅 (익명 — 개인 이동 경로 비수집)

인허가 자동도출:
- 공종 입력 → AI가 필요 인허가 목록 자동 도출 + 체크리스트 생성

DB 마이그레이션 (002):
- agent_conversations, agent_messages, evms_snapshots, geofence_zones

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:49:44 +09:00

119 lines
4.1 KiB
Python

"""
Vision AI API — Level 1 (사진 분류) + Level 2 (안전장비 감지)
"""
import uuid
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
from fastapi.responses import JSONResponse
from sqlalchemy import select
from app.deps import CurrentUser, DB
from app.models.project import Project
from app.models.daily_report import DailyReport, DailyReportPhoto
from app.services.vision_service import classify_photo, analyze_safety
router = APIRouter(prefix="/projects/{project_id}/vision", tags=["Vision AI"])
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp", "image/heic"}
MAX_SIZE_MB = 10
async def _get_project_or_404(project_id: uuid.UUID, db: DB) -> Project:
r = await db.execute(select(Project).where(Project.id == project_id))
p = r.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
return p
@router.post("/classify")
async def classify_field_photo(
project_id: uuid.UUID,
db: DB,
current_user: CurrentUser,
file: UploadFile = File(...),
location_hint: str | None = Form(None),
daily_report_id: uuid.UUID | None = Form(None),
):
"""
현장 사진 분류 (Vision AI Level 1)
- 공종 자동 분류
- 안전장비 착용 여부 1차 확인
- 작업일보 캡션 자동 생성
- daily_report_id 제공 시 해당 일보에 사진 자동 첨부
"""
await _get_project_or_404(project_id, db)
# 파일 검증
content_type = file.content_type or "image/jpeg"
if content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=400,
detail=f"지원하지 않는 파일 형식입니다. 허용: {', '.join(ALLOWED_TYPES)}"
)
image_data = await file.read()
if len(image_data) > MAX_SIZE_MB * 1024 * 1024:
raise HTTPException(status_code=400, detail=f"파일 크기가 {MAX_SIZE_MB}MB를 초과합니다")
# Vision AI 분류
result = await classify_photo(
image_data=image_data,
media_type=content_type,
location_hint=location_hint,
)
# 작업일보에 사진 첨부 (daily_report_id 제공 시)
if daily_report_id:
dr = await db.execute(
select(DailyReport).where(
DailyReport.id == daily_report_id,
DailyReport.project_id == project_id,
)
)
report = dr.scalar_one_or_none()
if report:
# 사진 수 카운트
photos_q = await db.execute(
select(DailyReportPhoto).where(DailyReportPhoto.daily_report_id == daily_report_id)
)
existing = photos_q.scalars().all()
photo = DailyReportPhoto(
daily_report_id=daily_report_id,
s3_key=f"vision/{project_id}/{daily_report_id}/{file.filename or 'photo.jpg'}",
caption=result.get("caption", ""),
sort_order=len(existing),
)
db.add(photo)
await db.commit()
result["attached_to_report"] = str(daily_report_id)
return JSONResponse(content=result)
@router.post("/safety-check")
async def safety_check(
project_id: uuid.UUID,
db: DB,
current_user: CurrentUser,
file: UploadFile = File(...),
):
"""
안전장비 착용 감지 (Vision AI Level 2)
안전모/안전조끼 착용 여부를 분석하고 위반 사항을 반환합니다.
최종 판정은 현장 책임자가 합니다.
"""
await _get_project_or_404(project_id, db)
content_type = file.content_type or "image/jpeg"
if content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=400, detail="지원하지 않는 파일 형식입니다")
image_data = await file.read()
if len(image_data) > MAX_SIZE_MB * 1024 * 1024:
raise HTTPException(status_code=400, detail=f"파일 크기가 {MAX_SIZE_MB}MB를 초과합니다")
result = await analyze_safety(image_data=image_data, media_type=content_type)
result["disclaimer"] = "이 결과는 AI 1차 분석이며, 최종 판정은 현장 책임자가 합니다."
return JSONResponse(content=result)