Spaces:
Running
Running
| # server.py | |
| import os | |
| import io | |
| import json | |
| from typing import Any, Dict, List, Optional | |
| from fastapi import FastAPI, Query, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, FileResponse, PlainTextResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| from datetime import datetime, timezone | |
| from huggingface_hub import HfApi, upload_file, hf_hub_download, create_repo | |
| # === Config === | |
| REPO_ID = os.environ.get("STORE_REPO", "TheFinAI/asset-annotator-store") | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| api = HfApi(token=HF_TOKEN) | |
| app = FastAPI() | |
| # CORS(同域访问其实不需要,但保留更宽松) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ---------- Hub helpers ---------- | |
| def ensure_repo(): | |
| try: | |
| create_repo(REPO_ID, repo_type="dataset", exist_ok=True, token=HF_TOKEN) | |
| except Exception: | |
| pass | |
| def upload_json(path_in_repo: str, obj: Any, message: str): | |
| ensure_repo() | |
| buf = io.BytesIO(json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8")) | |
| upload_file( | |
| path_or_fileobj=buf, | |
| path_in_repo=path_in_repo, | |
| repo_id=REPO_ID, | |
| repo_type="dataset", | |
| token=HF_TOKEN, | |
| commit_message=message, | |
| ) | |
| def download_json(path_in_repo: str) -> Optional[Any]: | |
| try: | |
| local = hf_hub_download(repo_id=REPO_ID, repo_type="dataset", filename=path_in_repo, token=HF_TOKEN) | |
| with open(local, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception: | |
| return None | |
| # ---------- API schemas ---------- | |
| class Dataset(BaseModel): | |
| dataset_id: str | |
| name: str | |
| data: Dict[str, List[Dict[str, Any]]] | |
| assets: List[str] | |
| dates: List[str] | |
| class Annotation(BaseModel): | |
| dataset_id: str | |
| user_id: str | |
| selections: List[Dict[str, Any]] | |
| step: int | |
| window_len: int | |
| # ---------- API routes ---------- | |
| def health(): | |
| dist_exists = os.path.exists("dist/index.html") | |
| return {"ok": True, "repo": REPO_ID, "dist": dist_exists} | |
| def upsert_dataset(ds: Dataset): | |
| payload = ds.dict() | |
| payload["id"] = ds.dataset_id # 兼容前端 | |
| payload["saved_at"] = datetime.now(timezone.utc).isoformat() | |
| upload_json("datasets/latest.json", payload, f"upsert dataset {ds.dataset_id}") | |
| return {"ok": True} | |
| def get_latest(): | |
| obj = download_json("datasets/latest.json") | |
| if obj is None: | |
| raise HTTPException(status_code=404, detail="No dataset yet.") | |
| return obj | |
| def get_annotation(dataset_id: str = Query(...), user_id: str = Query(...)): | |
| obj = download_json(f"annotations/{dataset_id}/{user_id}.json") | |
| if obj is None: | |
| return {"user_id": user_id, "dataset_id": dataset_id, "selections": [], "step": 1, "window_len": 7} | |
| return obj | |
| def upsert_annotation(ann: Annotation): | |
| payload = ann.dict() | |
| payload["updated_at"] = datetime.now(timezone.utc).isoformat() | |
| upload_json(f"annotations/{ann.dataset_id}/{ann.user_id}.json", payload, f"upsert annotation {ann.dataset_id}/{ann.user_id}") | |
| return {"ok": True} | |
| # ---------- Static mount + SPA fallback ---------- | |
| # 1) 如果 dist 存在,挂载静态资源 | |
| if os.path.isdir("dist"): | |
| app.mount("/assets", StaticFiles(directory="dist/assets", html=False), name="assets") | |
| # 2) 根路径:返回 index.html 或错误提示 | |
| def index(): | |
| if os.path.exists("dist/index.html"): | |
| return FileResponse("dist/index.html") | |
| return PlainTextResponse( | |
| "Frontend not built or not found. Missing dist/index.html.\n" | |
| "Check Dockerfile build stage and ensure '@vitejs/plugin-react' is installed.", | |
| status_code=500, | |
| ) | |
| # 3) 其它所有路径 → SPA fallback 到 index.html(不覆盖 /api/*) | |
| def spa(full_path: str): | |
| if full_path.startswith("api/"): | |
| raise HTTPException(status_code=404, detail="Not found") | |
| if os.path.exists("dist/index.html"): | |
| return FileResponse("dist/index.html") | |
| return PlainTextResponse("Frontend not built.", status_code=500) | |