|
|
| from fastapi import FastAPI |
| from fastapi import HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import StreamingResponse |
| from fastapi.staticfiles import StaticFiles |
| from huggingface_hub import HfApi |
| from huggingface_hub import hf_hub_download |
| from pydantic import BaseModel |
| from typing import Optional |
| import csv |
| from datetime import datetime |
| from datetime import timezone |
| import io |
| import json |
| import os |
| from pathlib import Path |
| import re |
| from uuid import uuid4 |
|
|
| app = FastAPI() |
|
|
| frontend_origins = os.getenv("FRONTEND_ORIGINS", "*") |
| allow_origins = ( |
| ["*"] |
| if frontend_origins == "*" |
| else [origin.strip() for origin in frontend_origins.split(",") if origin.strip()] |
| ) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=allow_origins, |
| allow_credentials=False, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| HF_DATASET_REPO_ID = os.getenv( |
| "HF_ANNOTATIONS_DATASET", |
| "adinayak/t2av_eval_annotations", |
| ) |
| HF_TOKEN = os.getenv("HF_TOKEN") |
| HF_DATASET_PRIVATE = os.getenv("HF_ANNOTATIONS_DATASET_PRIVATE", "true").lower() in { |
| "1", |
| "true", |
| "yes", |
| } |
| ANNOTATIONS_DIR = "annotations" |
| api = HfApi() |
|
|
|
|
| class AnnotationPayload(BaseModel): |
| video_id: str |
| annotations: dict |
| user: Optional[str] = "anonymous" |
|
|
|
|
| @app.on_event("startup") |
| def startup(): |
| ensure_dataset_configured() |
|
|
|
|
| @app.get("/health") |
| def root(): |
| return { |
| "status": "running", |
| "annotation_store": "huggingface_dataset", |
| "dataset_repo": HF_DATASET_REPO_ID, |
| } |
|
|
|
|
| def ensure_dataset_configured(): |
| if not HF_DATASET_REPO_ID: |
| raise HTTPException( |
| status_code=500, |
| detail="HF_ANNOTATIONS_DATASET is not configured.", |
| ) |
|
|
|
|
| def ensure_write_token(): |
| if not HF_TOKEN: |
| raise HTTPException( |
| status_code=500, |
| detail="HF_TOKEN is not configured. Add it as a Hugging Face Space secret.", |
| ) |
|
|
|
|
| def ensure_dataset_exists(): |
| ensure_dataset_configured() |
| ensure_write_token() |
| api.create_repo( |
| repo_id=HF_DATASET_REPO_ID, |
| repo_type="dataset", |
| private=HF_DATASET_PRIVATE, |
| token=HF_TOKEN, |
| exist_ok=True, |
| ) |
|
|
|
|
| def slugify(value): |
| slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-") |
| return slug or "unknown" |
|
|
|
|
| def annotation_path(annotation): |
| created_at = datetime.fromisoformat(annotation["created_at"].replace("Z", "+00:00")) |
| timestamp = created_at.strftime("%Y%m%dT%H%M%S%fZ") |
| video_id = slugify(annotation["video_id"]) |
| user = slugify(annotation["user"]) |
| return f"{ANNOTATIONS_DIR}/{timestamp}_{video_id}_{user}_{annotation['id']}.json" |
|
|
|
|
| def read_annotation_file(path): |
| try: |
| local_path = hf_hub_download( |
| repo_id=HF_DATASET_REPO_ID, |
| filename=path, |
| repo_type="dataset", |
| token=HF_TOKEN, |
| ) |
| except Exception as exc: |
| raise HTTPException(status_code=502, detail="Failed to read annotation file") from exc |
|
|
| with open(local_path, encoding="utf-8") as annotation_file: |
| return json.load(annotation_file) |
|
|
|
|
| def list_annotation_paths(): |
| ensure_dataset_configured() |
| try: |
| files = api.list_repo_files( |
| repo_id=HF_DATASET_REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN, |
| ) |
| except Exception as exc: |
| raise HTTPException(status_code=502, detail="Failed to list annotation dataset") from exc |
|
|
| return sorted( |
| [ |
| path |
| for path in files |
| if path.startswith(f"{ANNOTATIONS_DIR}/") and path.endswith(".json") |
| ], |
| reverse=True, |
| ) |
|
|
|
|
| def list_annotation_records(): |
| annotations = [read_annotation_file(path) for path in list_annotation_paths()] |
| return sorted( |
| annotations, |
| key=lambda item: item.get("created_at", ""), |
| reverse=True, |
| ) |
|
|
|
|
| @app.post("/save_annotation") |
| def save_annotation(payload: AnnotationPayload): |
| ensure_dataset_exists() |
| created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") |
| annotation = { |
| "id": uuid4().hex, |
| "video_id": payload.video_id, |
| "user": payload.user or "anonymous", |
| "annotations": payload.annotations, |
| "created_at": created_at, |
| } |
| path_in_repo = annotation_path(annotation) |
| annotation_bytes = json.dumps(annotation, indent=2).encode("utf-8") |
|
|
| try: |
| api.upload_file( |
| path_or_fileobj=io.BytesIO(annotation_bytes), |
| path_in_repo=path_in_repo, |
| repo_id=HF_DATASET_REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN, |
| commit_message=f"Add annotation {annotation['id']}", |
| ) |
| except Exception as exc: |
| raise HTTPException(status_code=500, detail="Failed to save annotation") from exc |
|
|
| return {"message": "saved", "annotation": annotation, "path": path_in_repo} |
|
|
|
|
| @app.get("/annotations") |
| def list_annotations(): |
| return list_annotation_records() |
|
|
|
|
| @app.get("/annotations.csv") |
| def download_annotations_csv(): |
| annotations = list_annotation_records() |
| output = io.StringIO() |
| writer = csv.DictWriter( |
| output, |
| fieldnames=["id", "video_id", "user", "created_at", "annotations"], |
| ) |
| writer.writeheader() |
| for annotation in annotations: |
| writer.writerow( |
| { |
| "id": annotation["id"], |
| "video_id": annotation["video_id"], |
| "user": annotation["user"], |
| "created_at": annotation["created_at"], |
| "annotations": json.dumps(annotation["annotations"]), |
| } |
| ) |
| output.seek(0) |
| return StreamingResponse( |
| iter([output.getvalue()]), |
| media_type="text/csv", |
| headers={"Content-Disposition": "attachment; filename=annotations.csv"}, |
| ) |
|
|
|
|
| @app.get("/annotations/{annotation_id}") |
| def get_annotation(annotation_id: str): |
| for annotation in list_annotation_records(): |
| if annotation["id"] == annotation_id: |
| return annotation |
| raise HTTPException(status_code=404, detail="Annotation not found") |
|
|
|
|
| static_dir = Path(__file__).resolve().parent.parent / "frontend" / "dist" |
|
|
| if static_dir.exists(): |
| app.mount("/", StaticFiles(directory=static_dir, html=True), name="frontend") |
|
|