Skip to content

Commit 915d170

Browse files
Make static Spaces work with Buckets and also allow conversion from Gradio SDK to Static Spaces (#469)
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
1 parent c279def commit 915d170

12 files changed

Lines changed: 227 additions & 133 deletions

.changeset/tall-dryers-take.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trackio": minor
3+
---
4+
5+
feat:Make static Spaces work with Buckets and also allow conversion from Gradio SDK to Static Spaces
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
Example: deploy a Gradio Space during training, then convert it to a static Space once done.
3+
4+
This demonstrates the Gradio -> Static conversion flow:
5+
1. Start training with a live Gradio dashboard (real-time updates)
6+
2. After training finishes, convert the Space to static (no server needed, cheaper)
7+
8+
Usage:
9+
python examples/convert-gradio-to-static.py
10+
"""
11+
12+
import math
13+
import random
14+
import time
15+
16+
import trackio
17+
18+
PROJECT = f"gradio-to-static-{random.randint(100000, 999999)}"
19+
SPACE_ID = f"convert-demo-{random.randint(100000, 999999)}"
20+
EPOCHS = 10
21+
22+
for run in range(2):
23+
trackio.init(
24+
project=PROJECT,
25+
name=f"run-{run}",
26+
config={"epochs": EPOCHS, "lr": 0.001 * (run + 1), "batch_size": 32},
27+
space_id=SPACE_ID,
28+
auto_log_gpu=False,
29+
)
30+
31+
for epoch in range(EPOCHS):
32+
progress = epoch / EPOCHS
33+
loss = 2.0 * math.exp(-3 * progress) + 0.1 + random.gauss(0, 0.05)
34+
acc = 0.95 / (1 + math.exp(-6 * (progress - 0.5))) + random.gauss(0, 0.02)
35+
36+
trackio.log(
37+
{
38+
"train/loss": round(max(0.01, loss), 4),
39+
"train/accuracy": round(min(0.99, max(0, acc)), 4),
40+
},
41+
step=epoch,
42+
)
43+
44+
trackio.log_system(
45+
{
46+
"cpu_percent": round(40 + epoch * 3 + random.uniform(-2, 2), 1),
47+
"memory_gb": round(4.0 + epoch * 0.1 + random.uniform(-0.05, 0.05), 2),
48+
}
49+
)
50+
51+
time.sleep(0.3)
52+
53+
trackio.finish()
54+
55+
print("\nTraining complete. Converting Gradio Space to static...")
56+
space_id = trackio.sync(project=PROJECT, space_id=SPACE_ID, sdk="static")
57+
print(f"Static dashboard: https://huggingface.co/spaces/{space_id}")

examples/sync-static-space-everything.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def main():
105105
trackio.log({"reports/summary": trackio.Markdown(report_md)})
106106
trackio.finish()
107107

108-
space_id = trackio.sync(project=PROJECT)
108+
space_id = trackio.sync(project=PROJECT, sdk="static")
109109
print(f"Dashboard: https://huggingface.co/spaces/{space_id}")
110110

111111

examples/sync-static-space.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ def fake_accuracy(epoch, max_epochs):
4949
)
5050
trackio.finish()
5151

52-
space_id = trackio.sync(project=PROJECT)
52+
space_id = trackio.sync(project=PROJECT, sdk="static")
5353
print(f"Dashboard: https://huggingface.co/spaces/{space_id}")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
requires-python = ">=3.10"
1515
dependencies = [
1616
"pandas<3.0.0",
17-
"huggingface-hub<2.0.0",
17+
"huggingface-hub>=1.9.0,<2",
1818
"gradio[oauth]>=6.10.0,<7.0.0",
1919
"numpy<3.0.0",
2020
"pillow<12.0.0",

tests/unit/test_sqlite_storage.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,29 @@ def test_rename_run_with_system_metrics(temp_dir):
397397

398398
new_system_logs = SQLiteStorage.get_system_logs(project, new_name)
399399
assert new_system_logs[0]["gpu_usage"] == 80.5
400+
401+
402+
def test_bucket_upload_paths_match_mount_layout(temp_dir):
403+
project = "proj_bucket"
404+
SQLiteStorage.log(project=project, run="run1", metrics={"loss": 0.5})
405+
406+
media_dir = Path(temp_dir) / "media" / project
407+
media_dir.mkdir(parents=True)
408+
(media_dir / "img.png").write_bytes(b"fake")
409+
410+
db_path = SQLiteStorage.get_project_db_path(project)
411+
files_to_add = [(str(db_path), f"trackio/{db_path.name}")]
412+
for media_file in media_dir.rglob("*"):
413+
if media_file.is_file():
414+
rel = media_file.relative_to(Path(temp_dir))
415+
files_to_add.append((str(media_file), f"trackio/{rel}"))
416+
417+
mount_point = Path("/data")
418+
trackio_dir = mount_point / "trackio"
419+
for local_path, remote_path in files_to_add:
420+
mounted = mount_point / remote_path
421+
assert str(mounted).startswith(str(trackio_dir)), (
422+
f"Bucket path {remote_path!r} would mount at {mounted}, "
423+
f"outside TRACKIO_DIR={trackio_dir}"
424+
)
425+
assert Path(local_path).exists()

trackio/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,6 @@ def init(
259259
space_url = deploy.SPACE_HOST_URL.format(
260260
user_name=user_name, space_name=space_name
261261
)
262-
print(
263-
f"* View dashboard by going to: {deploy._BOLD_ORANGE}{space_url}{deploy._RESET}"
264-
)
265262
if utils.is_in_notebook() and embed:
266263
utils.embed_url_in_notebook(space_url)
267264
context_vars.current_project.set(project)

trackio/bucket_storage.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import shutil
12
import sqlite3
3+
import tempfile
4+
from pathlib import Path
25

36
import huggingface_hub
47
from huggingface_hub import sync_bucket
@@ -15,7 +18,7 @@ def download_bucket_to_trackio_dir(bucket_id: str) -> None:
1518
TRACKIO_DIR.mkdir(parents=True, exist_ok=True)
1619
sync_bucket(
1720
source=f"hf://buckets/{bucket_id}",
18-
dest=str(TRACKIO_DIR),
21+
dest=str(TRACKIO_DIR.parent),
1922
quiet=True,
2023
)
2124

@@ -28,13 +31,63 @@ def upload_project_to_bucket(project: str, bucket_id: str) -> None:
2831
with sqlite3.connect(str(db_path), timeout=30.0) as conn:
2932
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
3033

31-
files_to_add = [(str(db_path), db_path.name)]
34+
files_to_add = [(str(db_path), f"trackio/{db_path.name}")]
3235

3336
media_dir = MEDIA_DIR / project
3437
if media_dir.exists():
3538
for media_file in media_dir.rglob("*"):
3639
if media_file.is_file():
3740
rel = media_file.relative_to(TRACKIO_DIR)
38-
files_to_add.append((str(media_file), str(rel)))
41+
files_to_add.append((str(media_file), f"trackio/{rel}"))
3942

4043
huggingface_hub.batch_bucket_files(bucket_id, add=files_to_add)
44+
45+
46+
def _download_db_from_bucket(project: str, bucket_id: str) -> bool:
47+
db_filename = SQLiteStorage.get_project_db_filename(project)
48+
remote_path = f"trackio/{db_filename}"
49+
local_path = SQLiteStorage.get_project_db_path(project)
50+
local_path.parent.mkdir(parents=True, exist_ok=True)
51+
try:
52+
huggingface_hub.download_bucket_files(
53+
bucket_id,
54+
files=[(remote_path, str(local_path))],
55+
)
56+
return local_path.exists()
57+
except Exception:
58+
return False
59+
60+
61+
def _local_db_has_data(project: str) -> bool:
62+
db_path = SQLiteStorage.get_project_db_path(project)
63+
if not db_path.exists() or db_path.stat().st_size == 0:
64+
return False
65+
conn = sqlite3.connect(str(db_path), timeout=5.0)
66+
try:
67+
count = conn.execute("SELECT COUNT(*) FROM metrics").fetchone()[0]
68+
return count > 0
69+
except Exception:
70+
return False
71+
finally:
72+
conn.close()
73+
74+
75+
def upload_project_to_bucket_for_static(project: str, bucket_id: str) -> None:
76+
if not _local_db_has_data(project):
77+
_download_db_from_bucket(project, bucket_id)
78+
79+
with tempfile.TemporaryDirectory() as tmp_dir:
80+
output_dir = Path(tmp_dir)
81+
SQLiteStorage.export_for_static_space(project, output_dir)
82+
83+
media_dir = MEDIA_DIR / project
84+
if media_dir.exists():
85+
shutil.copytree(media_dir, output_dir / "media")
86+
87+
files_to_add = []
88+
for f in output_dir.rglob("*"):
89+
if f.is_file():
90+
rel = f.relative_to(output_dir)
91+
files_to_add.append((str(f), str(rel)))
92+
93+
huggingface_hub.batch_bucket_files(bucket_id, add=files_to_add)

trackio/deploy.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import time
1010
from importlib.resources import files
1111
from pathlib import Path
12+
from typing import Literal
1213

1314
if sys.version_info >= (3, 11):
1415
import tomllib
@@ -20,12 +21,14 @@
2021
import huggingface_hub
2122
from gradio_client import Client, handle_file
2223
from httpx import ReadTimeout
24+
from huggingface_hub import Volume
2325
from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError
2426

2527
import trackio
26-
from trackio.bucket_storage import create_bucket_if_not_exists, upload_project_to_bucket
27-
from trackio.space_volumes import (
28-
attach_bucket_volume,
28+
from trackio.bucket_storage import (
29+
create_bucket_if_not_exists,
30+
upload_project_to_bucket,
31+
upload_project_to_bucket_for_static,
2932
)
3033
from trackio.sqlite_storage import SQLiteStorage
3134
from trackio.utils import (
@@ -233,14 +236,25 @@ def deploy_as_space(
233236
if hf_token := huggingface_hub.utils.get_token():
234237
huggingface_hub.add_space_secret(space_id, "HF_TOKEN", hf_token)
235238
if bucket_id is not None:
236-
changed = attach_bucket_volume(
237-
space_id,
238-
bucket_id,
239-
mount_path="/data",
239+
runtime = hf_api.get_space_runtime(space_id)
240+
existing = list(runtime.volumes) if runtime.volumes else []
241+
already_mounted = any(
242+
v.type == "bucket" and v.source == bucket_id and v.mount_path == "/data"
243+
for v in existing
240244
)
241-
huggingface_hub.add_space_variable(space_id, "TRACKIO_DIR", "/data/trackio")
242-
if changed:
245+
if not already_mounted:
246+
non_bucket = [
247+
v
248+
for v in existing
249+
if not (v.type == "bucket" and v.source == bucket_id)
250+
]
251+
hf_api.set_space_volumes(
252+
space_id,
253+
non_bucket
254+
+ [Volume(type="bucket", source=bucket_id, mount_path="/data")],
255+
)
243256
print(f"* Attached bucket {bucket_id} at '/data'")
257+
huggingface_hub.add_space_variable(space_id, "TRACKIO_DIR", "/data/trackio")
244258
elif dataset_id is not None:
245259
huggingface_hub.add_space_variable(space_id, "TRACKIO_DATASET_ID", dataset_id)
246260
if logo_light_url := os.environ.get("TRACKIO_LOGO_LIGHT_URL"):
@@ -687,7 +701,7 @@ def sync(
687701
private: bool | None = None,
688702
force: bool = False,
689703
run_in_background: bool = False,
690-
sdk: str = "gradio",
704+
sdk: Literal["gradio", "static"] = "gradio",
691705
dataset_id: str | None = None,
692706
bucket_id: str | None = None,
693707
) -> str:
@@ -712,7 +726,7 @@ def sync(
712726
sdk (`str`, *optional*, defaults to `"gradio"`):
713727
The type of Space to deploy. `"gradio"` deploys a Gradio Space with a live
714728
server. `"static"` deploys a static Space that reads from an HF Dataset
715-
(no server needed).
729+
or HF Bucket (no server needed).
716730
dataset_id (`str`, *optional*):
717731
The ID of the HF Dataset to sync to. When provided, uses the legacy
718732
Dataset backend instead of Buckets.
@@ -734,6 +748,20 @@ def sync(
734748

735749
def _do_sync():
736750
if sdk == "static":
751+
try:
752+
info = huggingface_hub.HfApi().space_info(space_id)
753+
if info.sdk == "gradio":
754+
if not force:
755+
answer = input(
756+
f"Space '{space_id}' is currently a Gradio Space. "
757+
f"Convert to static? [y/N] "
758+
)
759+
if answer.lower() not in ("y", "yes"):
760+
print("Aborted.")
761+
return
762+
except RepositoryNotFoundError:
763+
pass
764+
737765
if dataset_id is not None:
738766
upload_dataset_for_static(project, dataset_id, private=private)
739767
hf_token = huggingface_hub.utils.get_token() if private else None
@@ -746,7 +774,7 @@ def _do_sync():
746774
)
747775
elif bucket_id is not None:
748776
create_bucket_if_not_exists(bucket_id, private=private)
749-
upload_project_to_bucket(project, bucket_id)
777+
upload_project_to_bucket_for_static(project, bucket_id)
750778
print(
751779
f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}"
752780
)

0 commit comments

Comments
 (0)