ffasr / storage.py
Shivam
eval script in about, fix for qwen,
2f5908d
Raw
History Blame Contribute Delete
11.7 kB
"""HF Hub bucket (or S3) access: download files and upload results.
Storage configuration is read from environment variables so the Space can be moved
between users / organisations without editing the code:
FFASR_BUCKET_ID HF dataset repo acting as the read/write bucket.
Default: ``whojavumusic/FFASR_Leaderboard-storage``.
FFASR_RESULTS_PATH Path inside the bucket for the leaderboard CSV.
Default: ``results/leaderboard.csv``.
FFASR_JOBS_PATH Path inside the bucket for the job-state CSV.
Default: ``results/jobs_state.csv``.
HF_TOKEN Hub token with **write** access to ``FFASR_BUCKET_ID``.
``ds_token`` is also accepted as a legacy fallback.
token_for_ffasr_jobs (or ``FFASR_JOBS_TOKEN``) Hub token used **only** to submit
and poll Hugging Face **Jobs** (billing account with Job credits).
``HF_TOKEN`` is still passed into the job for bucket artifact upload.
For a private Space + private bucket, set ``HF_TOKEN`` as a Space repository secret
belonging to (or granted access to) the organisation that owns the bucket.
Set ``token_for_ffasr_jobs`` to the account/org that pays for Hub Jobs (not the Space
runtime default token).
"""
import io
import os
import shutil
import subprocess
import tempfile
from contextlib import nullcontext
from pathlib import Path
# --- Active: HuggingFace Bucket storage ---
STORAGE_BACKEND = "hf_bucket"
HF_BUCKET_ID = os.environ.get("FFASR_BUCKET_ID", "treble-technologies/FFASR_Leaderboard-storage")
DATASET_PREFIX = ""
RESULTS_PATH = os.environ.get("FFASR_RESULTS_PATH", "results/leaderboard.csv")
JOBS_STATE_PATH = os.environ.get("FFASR_JOBS_PATH", "results/jobs_state.csv")
# Append-only leaderboard history (one row per changed model per version). Used to
# reconstruct and display past leaderboard states from the version dropdown.
HISTORY_PATH = os.environ.get("FFASR_HISTORY_PATH", "results/leaderboard_history.csv")
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("ds_token")
# Env names tried in order (HF Space secrets are often lowercase like ``token_for_ffasr_jobs``).
_JOBS_TOKEN_ENV_KEYS = (
"token_for_ffasr_jobs",
"TOKEN_FOR_FFASR_JOBS",
"FFASR_JOBS_TOKEN",
"HF_JOBS_TOKEN",
)
def token_for_ffasr_jobs() -> str | None:
"""Hub token for submitting/polling FFASR remote Jobs (billing account)."""
for key in _JOBS_TOKEN_ENV_KEYS:
tok = os.environ.get(key, "").strip()
if tok:
return tok
return None
def require_token_for_ffasr_jobs() -> str:
"""Jobs billing token; never falls back to ``HF_TOKEN`` (avoids wrong namespace / 402)."""
tok = token_for_ffasr_jobs()
if tok:
return tok
keys = ", ".join(f"`{k}`" for k in _JOBS_TOKEN_ENV_KEYS)
raise RuntimeError(
f"Remote Hub Jobs require a dedicated jobs token: set one of {keys} as a Space secret "
"(Hub token with Job credits and permission to run jobs). "
"HF_TOKEN is only used for dataset bucket read/write on the Space and inside the job."
)
try:
from huggingface_hub import (
HfApi,
list_bucket_tree,
download_bucket_files,
batch_bucket_files,
)
hf_api = HfApi(token=HF_TOKEN)
except Exception:
hf_api = None
download_bucket_files = None
batch_bucket_files = None
list_bucket_tree = None
# --- Optional: AWS S3 (uncomment in init / wire here if needed) ---
# STORAGE_BACKEND = "s3"
# import boto3
# s3 = boto3.client("s3")
# DATASET_BUCKET = os.environ.get("DATASET_S3_BUCKET", "")
# RESULTS_BUCKET = os.environ.get("RESULTS_S3_BUCKET", DATASET_BUCKET)
# RESULTS_KEY = os.environ.get("RESULTS_S3_KEY", "results/leaderboard.csv")
def _ensure_xet_progress_reporter_compatible() -> None:
"""
Some container images mix an older ``XetProgressReporter`` with a newer ``hf_api`` that
calls ``notify_upload_complete`` between chunked uploads.
"""
try:
from huggingface_hub.utils._xet_progress_reporting import XetProgressReporter
except ImportError:
return
if hasattr(XetProgressReporter, "notify_upload_complete"):
return
def notify_upload_complete(self) -> None:
try:
self._total_bytes_offset = getattr(self.data_processing_bar, "total", None) or 0
self._total_transfer_bytes_offset = getattr(self.upload_bar, "total", None) or 0
except Exception:
pass
XetProgressReporter.notify_upload_complete = notify_upload_complete # type: ignore[method-assign]
def upload_to_bucket(
bucket_id: str,
*,
add: list[tuple[str | Path | bytes, str]] | None = None,
copy: list[tuple[str, str, str, str]] | None = None,
delete: list[str] | None = None,
token: str | bool | None = None,
) -> None:
"""
Upload/copy/delete bucket files with progress bars disabled and Xet reporter compatibility.
"""
if batch_bucket_files is None:
# In-process hub lacks the bucket API (e.g. qwen-asr stack pins hub<1.0).
# Fall back to an isolated uv env that has it.
if not _bucket_uv_available():
raise RuntimeError("huggingface_hub bucket support is not available")
_upload_to_bucket_via_uv(
bucket_id, add=add, copy=copy, delete=delete, token=token
)
return
_ensure_xet_progress_reporter_compatible()
try:
from huggingface_hub.utils import disable_progress_bars
except ImportError:
ctx = nullcontext()
else:
ctx = disable_progress_bars()
with ctx:
batch_bucket_files(
bucket_id,
add=add,
copy=copy,
delete=delete,
token=token,
)
# huggingface_hub version that ships the bucket API, used by the isolated-subprocess
# fallback when the in-process hub is pinned <1.0 (e.g. the qwen-asr stack, whose
# transformers==4.57.x pin forces huggingface-hub<1.0 and so lacks bucket support).
_BUCKET_FALLBACK_HUB_SPEC = os.environ.get(
"FFASR_BUCKET_FALLBACK_HUB_SPEC", "huggingface-hub>=1.14.0"
)
_BUCKET_DOWNLOAD_SNIPPET = """\
import os, sys
from huggingface_hub import download_bucket_files
bucket_id, remote_path, local_path = sys.argv[1], sys.argv[2], sys.argv[3]
download_bucket_files(
bucket_id,
files=[(remote_path, local_path)],
token=os.environ.get("FFASR_BUCKET_DL_TOKEN") or None,
)
"""
_BUCKET_UPLOAD_SNIPPET = """\
import json, os, sys
from huggingface_hub import batch_bucket_files
try:
from huggingface_hub.utils import disable_progress_bars
disable_progress_bars()
except Exception:
pass
manifest_path, bucket_id = sys.argv[1], sys.argv[2]
with open(manifest_path, encoding="utf-8") as f:
manifest = json.load(f)
add = [(src, dst) for src, dst in manifest.get("add", [])] or None
copy = [tuple(c) for c in manifest.get("copy", [])] or None
delete = list(manifest.get("delete", [])) or None
batch_bucket_files(
bucket_id,
add=add,
copy=copy,
delete=delete,
token=os.environ.get("FFASR_BUCKET_DL_TOKEN") or None,
)
"""
def _bucket_uv_available() -> bool:
return shutil.which("uv") is not None
def _run_bucket_uv_snippet(
snippet: str, args: list[str], *, token: str | None = None
) -> subprocess.CompletedProcess:
"""Run ``snippet`` in an isolated ``uv`` env that has the hub bucket API.
Used when this process' ``huggingface_hub`` is pinned <1.0 (no bucket support).
``uv`` resolves an ephemeral, cached environment with a recent ``huggingface_hub``
(plus ``hf_xet`` for Xet-backed buckets), independent of the job's pinned stack.
The Hub token is passed via ``FFASR_BUCKET_DL_TOKEN`` (never on argv).
"""
uv = shutil.which("uv") or "uv"
cmd = [
uv,
"run",
"--no-project",
"--python",
"3.12",
"--with",
_BUCKET_FALLBACK_HUB_SPEC,
"--with",
"hf_xet",
"python",
"-c",
snippet,
*args,
]
env = dict(os.environ)
tok = token or HF_TOKEN
if tok:
env["FFASR_BUCKET_DL_TOKEN"] = tok
env.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
try:
return subprocess.run(
cmd,
env=env,
check=False,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise RuntimeError(
"huggingface_hub bucket support is not available in this environment and the "
"`uv` fallback could not be launched (uv not found on PATH)."
) from exc
def _download_bucket_file_via_uv(path: str, local_path: str) -> None:
proc = _run_bucket_uv_snippet(
_BUCKET_DOWNLOAD_SNIPPET, [HF_BUCKET_ID, path, local_path]
)
if proc.returncode != 0 or not os.path.exists(local_path):
tail = (proc.stderr or proc.stdout or "").strip()[-2000:]
raise RuntimeError(
"Bucket download via isolated uv environment failed "
f"(exit {proc.returncode}) for '{path}'.\n{tail}"
)
def _upload_to_bucket_via_uv(
bucket_id: str,
*,
add: list[tuple[str | Path | bytes, str]] | None,
copy: list[tuple[str, str, str, str]] | None,
delete: list[str] | None,
token: str | bool | None = None,
) -> None:
"""Upload/copy/delete bucket files via the isolated ``uv`` env (hub <1.0 fallback).
``bytes`` sources in ``add`` are materialized to temp files so the subprocess can
pass plain file paths to ``batch_bucket_files``.
"""
tmpdir = tempfile.mkdtemp(prefix="ffasr_bucket_up_")
try:
add_items: list[tuple[str, str]] = []
for i, (src, dst) in enumerate(add or []):
if isinstance(src, (bytes, bytearray)):
src_path = os.path.join(tmpdir, f"add_{i}_{os.path.basename(dst) or 'blob'}")
with open(src_path, "wb") as fh:
fh.write(src)
else:
src_path = os.fspath(src)
add_items.append((src_path, dst))
manifest = {
"add": add_items,
"copy": [list(c) for c in (copy or [])],
"delete": list(delete or []),
}
import json as _json
manifest_path = os.path.join(tmpdir, "manifest.json")
with open(manifest_path, "w", encoding="utf-8") as fh:
_json.dump(manifest, fh)
snippet_token = token if isinstance(token, str) else None
proc = _run_bucket_uv_snippet(
_BUCKET_UPLOAD_SNIPPET, [manifest_path, bucket_id], token=snippet_token
)
if proc.returncode != 0:
tail = (proc.stderr or proc.stdout or "").strip()[-2000:]
raise RuntimeError(
"Bucket upload via isolated uv environment failed "
f"(exit {proc.returncode}).\n{tail}"
)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def download_bucket_file(path: str) -> str:
"""Download a single file from HF Bucket and return the local path.
Uses the in-process bucket API when available; otherwise falls back to an isolated
``uv`` environment that has a hub version with bucket support (the qwen-asr stack
pins huggingface-hub<1.0, which lacks the bucket API).
"""
local_dir = tempfile.mkdtemp()
local_path = os.path.join(local_dir, os.path.basename(path))
if download_bucket_files is not None:
download_bucket_files(
HF_BUCKET_ID,
files=[(path, local_path)],
token=HF_TOKEN,
)
else:
_download_bucket_file_via_uv(path, local_path)
return local_path