Files
Aislo/backend/app/main.py
T
2026-07-05 14:05:22 +09:00

222 lines
7.4 KiB
Python

from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi import Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from .analyzer import (
LAS_PREVIEW_PATH,
PREVIEW_PATH,
ROOT_DIR,
SAMPLE_DIR,
analyze_project,
analyze_sample_project,
collect_project_files,
confirm_sample_release,
create_ground_filter_cache,
find_las_in_dir,
get_project_source_dir,
get_project_storage_dir,
load_analysis,
load_analysis_for_project,
load_or_create_las_points_for_project,
load_or_create_las_points_sample,
)
from .classifier import ClassifyParams, run_classification
app = FastAPI(title="Forest Road Phase 1 Analyzer", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
FRONTEND_DIST = ROOT_DIR / "frontend" / "dist"
FRONTEND_ASSETS = FRONTEND_DIST / "assets"
if FRONTEND_ASSETS.exists():
app.mount("/assets", StaticFiles(directory=FRONTEND_ASSETS), name="frontend-assets")
# ---- Health / Root ----
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/", response_model=None)
def root() -> FileResponse | dict[str, object]:
index_path = FRONTEND_DIST / "index.html"
if index_path.exists():
return FileResponse(index_path)
return {
"name": "Forest Road Phase 1 Analyzer",
"status": "running",
"frontend_dev": "http://localhost:5173",
}
@app.get("/favicon.ico", include_in_schema=False)
def favicon() -> Response:
return Response(status_code=204)
# ---- Upload & Sample Check ----
class CheckSampleRequest(BaseModel):
filenames: list[str]
@app.post("/api/check-sample")
def check_sample(body: CheckSampleRequest) -> dict[str, object]:
sample_filenames = {f.name for f in SAMPLE_DIR.iterdir() if f.is_file()}
matched = all(fn in sample_filenames for fn in body.filenames)
return {"matched": matched, "project_id": "sample" if matched else None}
@app.post("/api/upload")
async def upload_files(
las_file: UploadFile = File(...),
prj_file: UploadFile = File(...),
tfw_file: UploadFile = File(...),
tif_file: UploadFile | None = File(None),
) -> dict[str, object]:
project_id = f"upload_{datetime.now().strftime('%Y%m%d%H%M%S')}"
raw_dir = ROOT_DIR / "storage" / "projects" / project_id / "raw"
raw_dir.mkdir(parents=True, exist_ok=True)
for upload in [las_file, prj_file, tfw_file] + ([tif_file] if tif_file else []):
if upload and upload.filename:
dest = raw_dir / upload.filename
content = await upload.read()
dest.write_bytes(content)
result = analyze_project(project_id)
return {"project_id": project_id, "status": result.get("status", "ready")}
# ---- Dynamic project endpoints ----
@app.get("/api/projects/{project_id}/analysis")
def get_project_analysis(project_id: str) -> dict[str, object]:
analysis = load_analysis_for_project(project_id)
if analysis is None:
if project_id == "sample":
return analyze_sample_project()
return analyze_project(project_id)
return analysis
@app.get("/api/projects/{project_id}/las-points-sample")
def get_project_las_points(project_id: str) -> dict[str, object]:
return load_or_create_las_points_for_project(project_id)
@app.post("/api/projects/{project_id}/analyze-ground")
def run_ground_analysis(project_id: str) -> dict[str, object]:
storage_dir = get_project_storage_dir(project_id)
stats_path = storage_dir / "processed" / "ground-stats.json"
if stats_path.exists():
return json.loads(stats_path.read_text(encoding="utf-8"))
result = create_ground_filter_cache(project_id)
stats_path.parent.mkdir(parents=True, exist_ok=True)
stats_path.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8")
return result
class ClassifyRequest(BaseModel):
rgb_exg_threshold: float = 0.05
ground_height_threshold: float = 1.5
@app.post("/api/projects/{project_id}/classify")
def classify_points(project_id: str, body: ClassifyRequest) -> dict[str, object]:
source_dir = get_project_source_dir(project_id)
las_path = find_las_in_dir(source_dir)
if not las_path.exists():
raise HTTPException(status_code=404, detail="LAS 파일을 찾을 수 없습니다.")
cache_path = get_project_storage_dir(project_id) / "processed" / "features.npz"
params = ClassifyParams(
rgb_exg_threshold=body.rgb_exg_threshold,
ground_height_threshold=body.ground_height_threshold,
)
return run_classification(las_path, cache_path, params)
@app.get("/api/projects/{project_id}/all-points")
def get_all_points(project_id: str) -> dict[str, object]:
path = get_project_storage_dir(project_id) / "processed" / "all-points.json"
if not path.exists():
raise HTTPException(status_code=404, detail="전체 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.")
return json.loads(path.read_text(encoding="utf-8"))
@app.get("/api/projects/{project_id}/ground-points")
def get_ground_points(project_id: str) -> dict[str, object]:
path = get_project_storage_dir(project_id) / "processed" / "ground-points.json"
if not path.exists():
raise HTTPException(status_code=404, detail="지면 포인트 캐시가 없습니다. analyze-ground를 먼저 실행하세요.")
return json.loads(path.read_text(encoding="utf-8"))
@app.get("/api/projects/{project_id}/preview")
def get_project_preview(project_id: str) -> FileResponse:
preview_path = get_project_storage_dir(project_id) / "processed" / "preview.png"
if not preview_path.exists():
if project_id == "sample":
if not PREVIEW_PATH.exists():
analyze_sample_project()
preview_path = PREVIEW_PATH
else:
raise HTTPException(status_code=404, detail="미리보기 이미지가 없습니다.")
if not preview_path.exists():
raise HTTPException(status_code=404, detail="미리보기 이미지를 생성하지 못했습니다.")
return FileResponse(preview_path, media_type="image/png")
# ---- Legacy sample endpoints (backward compat) ----
@app.get("/api/projects/sample")
def get_sample_project() -> dict[str, str]:
return {
"id": "sample",
"name": "개발용 샘플 프로젝트",
"description": "samples/step1_scan 기반 Phase 1 분석 대상",
}
@app.get("/api/projects/sample/files")
def get_sample_files() -> dict[str, object]:
return {"project_id": "sample", "files": collect_project_files()}
@app.post("/api/projects/sample/analyze")
def run_sample_analysis() -> dict[str, object]:
return analyze_sample_project()
@app.get("/api/projects/sample/las-preview")
def get_sample_las_preview() -> FileResponse:
if not LAS_PREVIEW_PATH.exists():
analyze_sample_project()
if not LAS_PREVIEW_PATH.exists():
raise HTTPException(status_code=404, detail="LAS preview image is not available.")
return FileResponse(Path(LAS_PREVIEW_PATH), media_type="image/png")
@app.post("/api/projects/sample/confirm")
def confirm_sample() -> dict[str, object]:
return confirm_sample_release()