Spaces:
Running
Running
Private Hugging Face dataset storage for recordings; health reports database and storage
Browse files- README.md +3 -1
- app.py +8 -1
- platform_app/core.py +44 -0
README.md
CHANGED
|
@@ -77,7 +77,7 @@ FastAPI (this Space, Docker)
|
|
| 77 |
│ ReportLab PDF (English / right-to-left Arabic)
|
| 78 |
└─ /demo standalone analyzer for a full recorded interview + single-question practice
|
| 79 |
│
|
| 80 |
-
Supabase Postgres (data) + Supabase Storage (recordings, PDFs)
|
| 81 |
```
|
| 82 |
|
| 83 |
### Configuration (Space secrets)
|
|
@@ -91,6 +91,8 @@ Supabase Postgres (data) + Supabase Storage (recordings, PDFs)
|
|
| 91 |
| `SUPABASE_SERVICE_ROLE_KEY` | Server-side storage access |
|
| 92 |
| `PINECONE_API_KEY` | Optional; a built-in vector index is used otherwise |
|
| 93 |
|
|
|
|
|
|
|
| 94 |
Optional: `GEMINI_MODEL` (default `gemini-3.5-flash-lite`, with automatic fallback to other Flash models), `STORAGE_BUCKET`.
|
| 95 |
|
| 96 |
### Run locally
|
|
|
|
| 77 |
│ ReportLab PDF (English / right-to-left Arabic)
|
| 78 |
└─ /demo standalone analyzer for a full recorded interview + single-question practice
|
| 79 |
│
|
| 80 |
+
Supabase Postgres (data) + Supabase Storage or a private HF dataset (recordings, PDFs)
|
| 81 |
```
|
| 82 |
|
| 83 |
### Configuration (Space secrets)
|
|
|
|
| 91 |
| `SUPABASE_SERVICE_ROLE_KEY` | Server-side storage access |
|
| 92 |
| `PINECONE_API_KEY` | Optional; a built-in vector index is used otherwise |
|
| 93 |
|
| 94 |
+
Large file storage (optional): set the variable `HF_STORAGE_REPO` (a private dataset, e.g. `EvalBot/evalbot-storage`) and the secret `HF_TOKEN` (a fine-grained token with write access to that dataset) to keep recordings and PDFs on Hugging Face instead of Supabase Storage. `/health` reports the active storage backend and database status; a scheduled ping to it keeps the Space and the free Supabase project awake.
|
| 95 |
+
|
| 96 |
Optional: `GEMINI_MODEL` (default `gemini-3.5-flash-lite`, with automatic fallback to other Flash models), `STORAGE_BUCKET`.
|
| 97 |
|
| 98 |
### Run locally
|
app.py
CHANGED
|
@@ -152,7 +152,14 @@ def _run(local_path: str, user_id: str, progress=None, lang: str = "en") -> Dict
|
|
| 152 |
|
| 153 |
@app.get("/health")
|
| 154 |
def health():
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
|
| 158 |
@app.post("/process-audio", response_model=ProcessResponse)
|
|
|
|
| 152 |
|
| 153 |
@app.get("/health")
|
| 154 |
def health():
|
| 155 |
+
core = platform_core
|
| 156 |
+
platform = {"database": False, "storage": core.storage_backend()}
|
| 157 |
+
if core.enabled():
|
| 158 |
+
try: # a real query, so the scheduled ping also keeps the free Supabase project active
|
| 159 |
+
platform["database"] = core.q1("select 1 as ok")["ok"] == 1
|
| 160 |
+
except Exception as e:
|
| 161 |
+
platform["database_error"] = type(e).__name__
|
| 162 |
+
return {"status": "ok", "services": pipeline.validate_env_vars(), "platform": platform}
|
| 163 |
|
| 164 |
|
| 165 |
@app.post("/process-audio", response_model=ProcessResponse)
|
platform_app/core.py
CHANGED
|
@@ -145,7 +145,32 @@ def _use_supabase_storage() -> bool:
|
|
| 145 |
return bool(SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY)
|
| 146 |
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
def ensure_bucket() -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
if not _use_supabase_storage():
|
| 150 |
os.makedirs(LOCAL_STORAGE_DIR, exist_ok=True)
|
| 151 |
return
|
|
@@ -157,6 +182,14 @@ def ensure_bucket() -> None:
|
|
| 157 |
|
| 158 |
|
| 159 |
def put_file(path: str, data: bytes, content_type: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
if not _use_supabase_storage():
|
| 161 |
full = os.path.join(LOCAL_STORAGE_DIR, path)
|
| 162 |
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
@@ -171,6 +204,17 @@ def put_file(path: str, data: bytes, content_type: str) -> str:
|
|
| 171 |
|
| 172 |
|
| 173 |
def get_file(path: str) -> bytes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
if not _use_supabase_storage():
|
| 175 |
with open(os.path.join(LOCAL_STORAGE_DIR, path), "rb") as f:
|
| 176 |
return f.read()
|
|
|
|
| 145 |
return bool(SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY)
|
| 146 |
|
| 147 |
|
| 148 |
+
# Optional: keep recordings and PDFs in a private Hugging Face dataset (100 GB+ free) instead of
|
| 149 |
+
# Supabase Storage (1 GB free). Enabled when HF_STORAGE_REPO and HF_TOKEN are both set.
|
| 150 |
+
HF_STORAGE_REPO = os.getenv("HF_STORAGE_REPO", "")
|
| 151 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 152 |
+
_hf_cache_dir = os.path.join(LOCAL_STORAGE_DIR, ".hf_cache")
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _use_hf_storage() -> bool:
|
| 156 |
+
return bool(HF_STORAGE_REPO and HF_TOKEN)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def storage_backend() -> str:
|
| 160 |
+
return "huggingface" if _use_hf_storage() else "supabase" if _use_supabase_storage() else "local"
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _hf_api():
|
| 164 |
+
from huggingface_hub import HfApi
|
| 165 |
+
return HfApi(token=HF_TOKEN)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
def ensure_bucket() -> None:
|
| 169 |
+
if _use_hf_storage():
|
| 170 |
+
_hf_api().create_repo(HF_STORAGE_REPO, repo_type="dataset", private=True, exist_ok=True)
|
| 171 |
+
os.makedirs(_hf_cache_dir, exist_ok=True)
|
| 172 |
+
logger.info("File storage: private HF dataset %s", HF_STORAGE_REPO)
|
| 173 |
+
return
|
| 174 |
if not _use_supabase_storage():
|
| 175 |
os.makedirs(LOCAL_STORAGE_DIR, exist_ok=True)
|
| 176 |
return
|
|
|
|
| 182 |
|
| 183 |
|
| 184 |
def put_file(path: str, data: bytes, content_type: str) -> str:
|
| 185 |
+
if _use_hf_storage():
|
| 186 |
+
_hf_api().upload_file(path_or_fileobj=data, path_in_repo=path, repo_id=HF_STORAGE_REPO,
|
| 187 |
+
repo_type="dataset", commit_message=f"add {path}")
|
| 188 |
+
full = os.path.join(_hf_cache_dir, path) # keep a local copy so reads are instant
|
| 189 |
+
os.makedirs(os.path.dirname(full), exist_ok=True)
|
| 190 |
+
with open(full, "wb") as f:
|
| 191 |
+
f.write(data)
|
| 192 |
+
return path
|
| 193 |
if not _use_supabase_storage():
|
| 194 |
full = os.path.join(LOCAL_STORAGE_DIR, path)
|
| 195 |
os.makedirs(os.path.dirname(full), exist_ok=True)
|
|
|
|
| 204 |
|
| 205 |
|
| 206 |
def get_file(path: str) -> bytes:
|
| 207 |
+
if _use_hf_storage():
|
| 208 |
+
full = os.path.join(_hf_cache_dir, path)
|
| 209 |
+
if not os.path.exists(full):
|
| 210 |
+
from huggingface_hub import hf_hub_download
|
| 211 |
+
try:
|
| 212 |
+
return open(hf_hub_download(HF_STORAGE_REPO, path, repo_type="dataset", token=HF_TOKEN,
|
| 213 |
+
local_dir=_hf_cache_dir), "rb").read()
|
| 214 |
+
except Exception as e:
|
| 215 |
+
raise RuntimeError(f"Storage download failed: {e}")
|
| 216 |
+
with open(full, "rb") as f:
|
| 217 |
+
return f.read()
|
| 218 |
if not _use_supabase_storage():
|
| 219 |
with open(os.path.join(LOCAL_STORAGE_DIR, path), "rb") as f:
|
| 220 |
return f.read()
|