yash-b18 commited on
Commit
5978ef2
Β·
1 Parent(s): d07db3a

feat: add HuggingFace Spaces deployment (Dockerfile, artifact scripts, CORS fix)

Browse files
.dockerignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Version control
2
+ .git
3
+ .gitignore
4
+
5
+ # Python virtual env (large, not needed in container)
6
+ venv/
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ .Python
12
+
13
+ # Frontend (served separately via Vercel)
14
+ frontend/
15
+
16
+ # Raw data (large, not needed at inference)
17
+ data/raw/
18
+
19
+ # Notebooks
20
+ notebooks/
21
+
22
+ # Docs
23
+ docs/
24
+
25
+ # Dev artifacts
26
+ .pytest_cache/
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+ *.egg-info/
30
+
31
+ # OS files
32
+ .DS_Store
33
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # DeepReads β€” FastAPI backend for HuggingFace Spaces (Docker)
3
+ # Port: 7860 (HF Spaces requirement)
4
+ # ============================================================
5
+
6
+ FROM python:3.11-slim
7
+
8
+ # System dependencies
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ gcc g++ libgomp1 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ WORKDIR /app
14
+
15
+ # Install Python dependencies first (layer-cached)
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy source code
20
+ COPY src/ src/
21
+ COPY scripts/ scripts/
22
+
23
+ # Create required directories
24
+ RUN mkdir -p models data/processed data/outputs/figures
25
+
26
+ # HuggingFace Spaces injects these at runtime via Space secrets:
27
+ # HF_REPO_ID β€” your model artifact repo, e.g. "yourname/deepreads-artifacts"
28
+ # HF_TOKEN β€” (optional) if the artifact repo is private
29
+ # FRONTEND_URL β€” your Vercel frontend URL (for CORS), e.g. "https://deepreads.vercel.app"
30
+
31
+ # Download artifacts and start the API server
32
+ # The download script is a no-op if files already exist (fast restarts)
33
+ CMD python scripts/download_artifacts.py && \
34
+ uvicorn src.api.app:app \
35
+ --host 0.0.0.0 \
36
+ --port 7860 \
37
+ --workers 1 \
38
+ --timeout-keep-alive 75
scripts/download_artifacts.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Download model and data artifacts from HuggingFace Hub at startup.
3
+
4
+ Run once before starting the API server. Files are cached locally so
5
+ subsequent restarts skip the download entirely.
6
+
7
+ Usage:
8
+ python scripts/download_artifacts.py
9
+
10
+ Environment variables:
11
+ HF_REPO_ID β€” HuggingFace model repo, e.g. "yashb18/deepreads-artifacts"
12
+ HF_TOKEN β€” Optional HF token (only needed if repo is private)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Artifact manifest
23
+ # Each entry: (repo_filename, local_path)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ ROOT = Path(__file__).resolve().parent.parent
27
+
28
+ ARTIFACT_MAP: list[tuple[str, Path]] = [
29
+ # ── Models ──────────────────────────────────────────────────────────────
30
+ ("models/two_tower.pt", ROOT / "models" / "two_tower.pt"),
31
+ ("models/item_embeddings.npy", ROOT / "models" / "item_embeddings.npy"),
32
+ ("models/item_index.pkl", ROOT / "models" / "item_index.pkl"),
33
+ ("models/naive_baseline.pkl", ROOT / "models" / "naive_baseline.pkl"),
34
+ ("models/naive_category.pkl", ROOT / "models" / "naive_category.pkl"),
35
+ ("models/lgbm_model.pkl", ROOT / "models" / "lgbm_model.pkl"),
36
+ # ── Processed data ──────────────────────────────────────────────────────
37
+ ("data/processed/item_features.parquet", ROOT / "data" / "processed" / "item_features.parquet"),
38
+ ("data/processed/user_features.parquet", ROOT / "data" / "processed" / "user_features.parquet"),
39
+ ("data/processed/train.parquet", ROOT / "data" / "processed" / "train.parquet"),
40
+ ("data/processed/feature_artifacts.pkl", ROOT / "data" / "processed" / "feature_artifacts.pkl"),
41
+ ("data/processed/item_encoder.pkl", ROOT / "data" / "processed" / "item_encoder.pkl"),
42
+ ("data/processed/user_encoder.pkl", ROOT / "data" / "processed" / "user_encoder.pkl"),
43
+ ]
44
+
45
+
46
+ def main() -> None:
47
+ repo_id = os.environ.get("HF_REPO_ID", "")
48
+ if not repo_id:
49
+ print("ERROR: HF_REPO_ID environment variable is not set.", file=sys.stderr)
50
+ print(" Set it to your HuggingFace model repo, e.g.:", file=sys.stderr)
51
+ print(" export HF_REPO_ID=yourname/deepreads-artifacts", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+ token = os.environ.get("HF_TOKEN") or None
55
+
56
+ try:
57
+ from huggingface_hub import hf_hub_download
58
+ except ImportError:
59
+ print("ERROR: huggingface_hub is not installed. Run: pip install huggingface-hub", file=sys.stderr)
60
+ sys.exit(1)
61
+
62
+ missing = [(repo_fn, local) for repo_fn, local in ARTIFACT_MAP if not local.exists()]
63
+
64
+ if not missing:
65
+ print("All artifacts already present β€” skipping download.")
66
+ return
67
+
68
+ print(f"Downloading {len(missing)} artifact(s) from {repo_id} …")
69
+
70
+ for repo_filename, local_path in missing:
71
+ local_path.parent.mkdir(parents=True, exist_ok=True)
72
+ print(f" ↓ {repo_filename}")
73
+ try:
74
+ downloaded = hf_hub_download(
75
+ repo_id=repo_id,
76
+ filename=repo_filename,
77
+ repo_type="model",
78
+ token=token,
79
+ local_dir=ROOT,
80
+ local_dir_use_symlinks=False,
81
+ )
82
+ # hf_hub_download places files under local_dir/<filename>
83
+ # which matches local_path exactly, so nothing to move.
84
+ _ = downloaded
85
+ except Exception as exc:
86
+ print(f"ERROR: Failed to download {repo_filename}: {exc}", file=sys.stderr)
87
+ sys.exit(1)
88
+
89
+ print("Download complete.")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
scripts/upload_artifacts.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Upload model and data artifacts to a HuggingFace Hub model repository.
3
+
4
+ Run this ONCE from your local machine before deploying the Space.
5
+
6
+ Usage:
7
+ HF_REPO_ID=yourname/deepreads-artifacts python scripts/upload_artifacts.py
8
+
9
+ Environment variables:
10
+ HF_REPO_ID β€” target HF model repo (will be created if it doesn't exist)
11
+ HF_TOKEN β€” your HF write token (from https://huggingface.co/settings/tokens)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ ROOT = Path(__file__).resolve().parent.parent
21
+
22
+ ARTIFACT_MAP: list[tuple[Path, str]] = [
23
+ # (local_path, repo_filename)
24
+ # ── Models ──────────────────────────────────────────────────────────────
25
+ (ROOT / "models" / "two_tower.pt", "models/two_tower.pt"),
26
+ (ROOT / "models" / "item_embeddings.npy", "models/item_embeddings.npy"),
27
+ (ROOT / "models" / "item_index.pkl", "models/item_index.pkl"),
28
+ (ROOT / "models" / "naive_baseline.pkl", "models/naive_baseline.pkl"),
29
+ (ROOT / "models" / "naive_category.pkl", "models/naive_category.pkl"),
30
+ (ROOT / "models" / "lgbm_model.pkl", "models/lgbm_model.pkl"),
31
+ # ── Processed data ──────────────────────────────────────────────────────
32
+ (ROOT / "data" / "processed" / "item_features.parquet", "data/processed/item_features.parquet"),
33
+ (ROOT / "data" / "processed" / "user_features.parquet", "data/processed/user_features.parquet"),
34
+ (ROOT / "data" / "processed" / "train.parquet", "data/processed/train.parquet"),
35
+ (ROOT / "data" / "processed" / "feature_artifacts.pkl", "data/processed/feature_artifacts.pkl"),
36
+ (ROOT / "data" / "processed" / "item_encoder.pkl", "data/processed/item_encoder.pkl"),
37
+ (ROOT / "data" / "processed" / "user_encoder.pkl", "data/processed/user_encoder.pkl"),
38
+ ]
39
+
40
+
41
+ def main() -> None:
42
+ repo_id = os.environ.get("HF_REPO_ID", "")
43
+ if not repo_id:
44
+ print("ERROR: Set HF_REPO_ID, e.g.:")
45
+ print(" export HF_REPO_ID=yourname/deepreads-artifacts")
46
+ sys.exit(1)
47
+
48
+ token = os.environ.get("HF_TOKEN") or None
49
+ if not token:
50
+ print("WARNING: HF_TOKEN not set. Will try using cached credentials.")
51
+ print(" Get a write token at https://huggingface.co/settings/tokens")
52
+
53
+ try:
54
+ from huggingface_hub import HfApi
55
+ except ImportError:
56
+ print("ERROR: pip install huggingface-hub")
57
+ sys.exit(1)
58
+
59
+ api = HfApi(token=token)
60
+
61
+ # Create repo if it doesn't exist
62
+ try:
63
+ api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True, private=False)
64
+ print(f"Repository ready: https://huggingface.co/{repo_id}")
65
+ except Exception as e:
66
+ print(f"ERROR creating repo: {e}")
67
+ sys.exit(1)
68
+
69
+ missing = [(local, repo_fn) for local, repo_fn in ARTIFACT_MAP if not local.exists()]
70
+ if missing:
71
+ print(f"WARNING: {len(missing)} local file(s) not found and will be skipped:")
72
+ for local, _ in missing:
73
+ print(f" {local}")
74
+
75
+ to_upload = [(local, repo_fn) for local, repo_fn in ARTIFACT_MAP if local.exists()]
76
+ total = len(to_upload)
77
+ print(f"\nUploading {total} file(s) to {repo_id} …\n")
78
+
79
+ for i, (local_path, repo_filename) in enumerate(to_upload, 1):
80
+ size_mb = local_path.stat().st_size / 1_048_576
81
+ print(f" [{i}/{total}] {repo_filename} ({size_mb:.1f} MB)")
82
+ try:
83
+ api.upload_file(
84
+ path_or_fileobj=str(local_path),
85
+ path_in_repo=repo_filename,
86
+ repo_id=repo_id,
87
+ repo_type="model",
88
+ )
89
+ except Exception as e:
90
+ print(f" ERROR: {e}")
91
+ sys.exit(1)
92
+
93
+ print(f"\nDone! All artifacts uploaded to:")
94
+ print(f" https://huggingface.co/{repo_id}")
95
+ print(f"\nNext: set HF_REPO_ID={repo_id} as a Space secret.")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
src/api/app.py CHANGED
@@ -86,7 +86,6 @@ app = FastAPI(
86
  cors_origins = [
87
  "http://localhost:3000",
88
  "http://localhost:3001",
89
- "https://*.vercel.app",
90
  ]
91
  env_origin = os.getenv("FRONTEND_URL")
92
  if env_origin:
@@ -95,6 +94,8 @@ if env_origin:
95
  app.add_middleware(
96
  CORSMiddleware,
97
  allow_origins=cors_origins,
 
 
98
  allow_credentials=True,
99
  allow_methods=["*"],
100
  allow_headers=["*"],
 
86
  cors_origins = [
87
  "http://localhost:3000",
88
  "http://localhost:3001",
 
89
  ]
90
  env_origin = os.getenv("FRONTEND_URL")
91
  if env_origin:
 
94
  app.add_middleware(
95
  CORSMiddleware,
96
  allow_origins=cors_origins,
97
+ # Wildcard for all Vercel preview/production deployments
98
+ allow_origin_regex=r"https://.*\.vercel\.app",
99
  allow_credentials=True,
100
  allow_methods=["*"],
101
  allow_headers=["*"],