File size: 4,349 Bytes
52c1e6d
0ea25d9
 
 
 
 
8ee051c
52c1e6d
 
8ee051c
0ea25d9
 
8ee051c
52c1e6d
4ea39a1
0ea25d9
 
52c1e6d
8ee051c
 
52c1e6d
8ee051c
 
 
0ea25d9
8ee051c
0ea25d9
8ee051c
 
52c1e6d
0ea25d9
 
 
 
 
8ee051c
0ea25d9
 
 
 
 
 
 
 
 
 
 
 
 
8ee051c
0ea25d9
 
 
 
 
 
52c1e6d
0ea25d9
 
 
 
 
 
 
 
8ee051c
0ea25d9
 
 
 
 
52c1e6d
 
 
 
 
0ea25d9
 
 
 
6764e22
0ea25d9
 
 
 
6764e22
0ea25d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52c1e6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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 ----------
@app.get("/api/health")
def health():
    dist_exists = os.path.exists("dist/index.html")
    return {"ok": True, "repo": REPO_ID, "dist": dist_exists}

@app.post("/api/dataset/upsert")
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}


@app.get("/api/dataset/latest")
def get_latest():
    obj = download_json("datasets/latest.json")
    if obj is None:
        raise HTTPException(status_code=404, detail="No dataset yet.")
    return obj

@app.get("/api/annotation/get")
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

@app.post("/api/annotation/upsert")
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 或错误提示
@app.get("/")
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/*)
@app.get("/{full_path:path}")
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)