suvradeepp commited on
Commit
f7e8732
·
verified ·
1 Parent(s): 35d483e

Publish Tiny Hinglish Turn Detector development preview

Browse files

release inventory sha256: fa73d6c3defbd256e431aa99c8b4fae4ea6e74cbb2fb736f57eb43fbbe65f41a

MODEL_CARD.md CHANGED
@@ -303,7 +303,7 @@ failure aggregates, silence perturbation report, exact environment files, CPU
303
  benchmarks, and guarded release tooling. Per-example validation predictions and
304
  the text-bearing split manifest remain in the private local workspace; the
305
  public package contains aggregate reports and hashes, not those sensitive rows.
306
- The quality gate completed with Ruff 0.15.16 passing and 127/127 automated tests
307
  passing via Python `unittest` discovery. The configured Hugging Face owner is
308
  `suvradeepp`.
309
 
 
303
  benchmarks, and guarded release tooling. Per-example validation predictions and
304
  the text-bearing split manifest remain in the private local workspace; the
305
  public package contains aggregate reports and hashes, not those sensitive rows.
306
+ The quality gate completed with Ruff 0.15.16 passing and 130/130 automated tests
307
  passing via Python `unittest` discovery. The configured Hugging Face owner is
308
  `suvradeepp`.
309
 
README.md CHANGED
@@ -303,7 +303,7 @@ failure aggregates, silence perturbation report, exact environment files, CPU
303
  benchmarks, and guarded release tooling. Per-example validation predictions and
304
  the text-bearing split manifest remain in the private local workspace; the
305
  public package contains aggregate reports and hashes, not those sensitive rows.
306
- The quality gate completed with Ruff 0.15.16 passing and 127/127 automated tests
307
  passing via Python `unittest` discovery. The configured Hugging Face owner is
308
  `suvradeepp`.
309
 
 
303
  benchmarks, and guarded release tooling. Per-example validation predictions and
304
  the text-bearing split manifest remain in the private local workspace; the
305
  public package contains aggregate reports and hashes, not those sensitive rows.
306
+ The quality gate completed with Ruff 0.15.16 passing and 130/130 automated tests
307
  passing via Python `unittest` discovery. The configured Hugging Face owner is
308
  `suvradeepp`.
309
 
REPORT.md CHANGED
@@ -525,8 +525,8 @@ calendar days or the missing external data collection occurred.
525
  | 13 | Integrate controller and demo | replay, three-state controller, Gradio, Hinglish recording prompts | Preview model and synthetic controller integration complete; real sequence replay pending |
526
  | 14 | Freeze test, package, publish | sealed-test guard, atomic release builder, exact-mirror publisher | Development packaging path ready; official-test/final gates not satisfied |
527
 
528
- The repository quality gate passed Ruff 0.15.16 and all 127 automated tests
529
- (127 passed via Python `unittest` discovery). That validates the implemented behavior under test; it does not fill
530
  the missing empirical datasets or rights review.
531
 
532
  ## 9. Decision log and next experiments
 
525
  | 13 | Integrate controller and demo | replay, three-state controller, Gradio, Hinglish recording prompts | Preview model and synthetic controller integration complete; real sequence replay pending |
526
  | 14 | Freeze test, package, publish | sealed-test guard, atomic release builder, exact-mirror publisher | Development packaging path ready; official-test/final gates not satisfied |
527
 
528
+ The repository quality gate passed Ruff 0.15.16 and all 130 automated tests
529
+ (130 passed via Python `unittest` discovery). That validates the implemented behavior under test; it does not fill
530
  the missing empirical datasets or rights review.
531
 
532
  ## 9. Decision log and next experiments
SOURCE_README.md CHANGED
@@ -26,7 +26,7 @@ engineering evidence, not a production-readiness claim.
26
  | Domain-shift stress | Measured, confounded | Source-held-out AP 0.5500/0.5368 for TinyTCN/baseline; both near chance in ranking, with wide nine-source bootstrap intervals |
27
  | Runtime/export | Measured | 611 KiB FP32 ONNX; exact PyTorch/ONNX parity; ONNX p95 0.956 ms neural-only and 1.629 ms waveform-to-probability |
28
  | Controller integration | Synthetic only | Metadata-bound 8-checkpoint/3-turn fixture emits 3 response edges with 0 duplicates; not model or product-quality evidence |
29
- | Code quality gate | Passed | Ruff 0.15.16; 127/127 automated tests passed via `unittest` discovery |
30
  | Accuracy/generalization | **Not established** | No full-corpus run, collected Hinglish recordings, real conversation replay, or official-test evaluation |
31
  | Hugging Face publication | Not claimed here | Local guarded release/publisher path exists; verify any remote publication separately |
32
 
@@ -321,6 +321,13 @@ uv run python scripts/publish_hf.py --username suvradeepp
321
  uv run python scripts/publish_hf.py --username suvradeepp --execute
322
  ```
323
 
 
 
 
 
 
 
 
324
  The release builder refuses smoke checkpoints or a release without measured
325
  test metrics unless `--allow-development-artifact` is passed explicitly. That
326
  override must remain visibly development-only. For a reviewable partial-data
 
26
  | Domain-shift stress | Measured, confounded | Source-held-out AP 0.5500/0.5368 for TinyTCN/baseline; both near chance in ranking, with wide nine-source bootstrap intervals |
27
  | Runtime/export | Measured | 611 KiB FP32 ONNX; exact PyTorch/ONNX parity; ONNX p95 0.956 ms neural-only and 1.629 ms waveform-to-probability |
28
  | Controller integration | Synthetic only | Metadata-bound 8-checkpoint/3-turn fixture emits 3 response edges with 0 duplicates; not model or product-quality evidence |
29
+ | Code quality gate | Passed | Ruff 0.15.16; 130/130 automated tests passed via `unittest` discovery |
30
  | Accuracy/generalization | **Not established** | No full-corpus run, collected Hinglish recordings, real conversation replay, or official-test evaluation |
31
  | Hugging Face publication | Not claimed here | Local guarded release/publisher path exists; verify any remote publication separately |
32
 
 
321
  uv run python scripts/publish_hf.py --username suvradeepp --execute
322
  ```
323
 
324
+ For a personal account without Hugging Face PRO, the publisher requests the
325
+ free-account-compatible ZeroGPU tier (`--space-hardware zero-a10g`) only when it
326
+ must create a new Space. Hugging Face currently gates new personal Gradio CPU
327
+ Spaces behind PRO even when CPU Basic has no hourly charge. ZeroGPU eligibility
328
+ still requires a verified account in good standing and an available hosted
329
+ Space slot. Paid accounts may override this with `--space-hardware cpu-basic`.
330
+
331
  The release builder refuses smoke checkpoints or a release without measured
332
  test metrics unless `--allow-development-artifact` is passed explicitly. That
333
  override must remain visibly development-only. For a reviewable partial-data
export_manifest.json CHANGED
@@ -171,9 +171,9 @@
171
  "sha256": "89167b269b2103ad0bd94918f2ec4f7b605cf001d391e570683ceef39066b6c2"
172
  },
173
  {
174
- "bytes": 42576,
175
  "path": "scripts/publish_hf.py",
176
- "sha256": "97b861d22e6fa0f6b6f998ca1026151f9720cfe3607670b3c5735120a5312760"
177
  },
178
  {
179
  "bytes": 3752,
@@ -371,7 +371,7 @@
371
  "sha256": "da0eac829fe43615f7bb149998c2f2762f148790fb0c14d4ba6db4d6b25298f4"
372
  }
373
  ],
374
- "source_inventory_sha256": "786e0982f03db47ca1ce264cea005f6d379fce76f72870bfe5c95b76502877eb",
375
  "task": "audio-turn-end-detection",
376
  "threshold": 0.741000771522522,
377
  "training_data": {
 
171
  "sha256": "89167b269b2103ad0bd94918f2ec4f7b605cf001d391e570683ceef39066b6c2"
172
  },
173
  {
174
+ "bytes": 43530,
175
  "path": "scripts/publish_hf.py",
176
+ "sha256": "36311a2457bb0e93cbd6fd0ed444e57a2ec2c44ab9bd17ed06132e9640a9ddc3"
177
  },
178
  {
179
  "bytes": 3752,
 
371
  "sha256": "da0eac829fe43615f7bb149998c2f2762f148790fb0c14d4ba6db4d6b25298f4"
372
  }
373
  ],
374
+ "source_inventory_sha256": "259931c509dc4abd54a3c5c72ca12948449bd19d3aed2b2c2ea0fac88a6a705b",
375
  "task": "audio-turn-end-detection",
376
  "threshold": 0.741000771522522,
377
  "training_data": {
release_provenance.json CHANGED
@@ -16,7 +16,7 @@
16
  },
17
  "MODEL_CARD.md": {
18
  "bytes": 14805,
19
- "sha256": "bf5ca1a9f856ebf39de50094b784936cb0751d103bf585dd9523f100837d336e"
20
  },
21
  "Makefile": {
22
  "bytes": 777,
@@ -28,15 +28,15 @@
28
  },
29
  "README.md": {
30
  "bytes": 14805,
31
- "sha256": "bf5ca1a9f856ebf39de50094b784936cb0751d103bf585dd9523f100837d336e"
32
  },
33
  "REPORT.md": {
34
  "bytes": 32272,
35
- "sha256": "fd5ec03ec01af960c043d5828ab5d1204eed2ced1c6f74223f1222b2f6721d82"
36
  },
37
  "SOURCE_README.md": {
38
- "bytes": 20065,
39
- "sha256": "ca0f08b2c18fc5b3c16b10c36922add96ef24048dea67ddfb120cf5bc51f5c49"
40
  },
41
  "app.py": {
42
  "bytes": 10381,
@@ -108,7 +108,7 @@
108
  },
109
  "export_manifest.json": {
110
  "bytes": 12430,
111
- "sha256": "48a7c3c711c23ad66e9df35c88153544a2107be41b0c9c6613f2b7fbdb0d235d"
112
  },
113
  "model.onnx": {
114
  "bytes": 625431,
@@ -283,8 +283,8 @@
283
  "sha256": "89167b269b2103ad0bd94918f2ec4f7b605cf001d391e570683ceef39066b6c2"
284
  },
285
  "scripts/publish_hf.py": {
286
- "bytes": 42576,
287
- "sha256": "97b861d22e6fa0f6b6f998ca1026151f9720cfe3607670b3c5735120a5312760"
288
  },
289
  "scripts/replay_stream.py": {
290
  "bytes": 3752,
@@ -527,8 +527,8 @@
527
  "sha256": "12a9a0edda07265de293076d12073a707b10e6fb9ebbf2e7c70ba9a219289bdc"
528
  },
529
  "tests/test_publish_hf.py": {
530
- "bytes": 22529,
531
- "sha256": "91f825c0ad9c5a6a25079ff4d956c27564b254f4db46ddb494770425d20ebd2f"
532
  },
533
  "tests/test_release_guardrails.py": {
534
  "bytes": 17399,
@@ -566,7 +566,7 @@
566
  },
567
  "MODEL_CARD.md": {
568
  "bytes": 14805,
569
- "sha256": "bf5ca1a9f856ebf39de50094b784936cb0751d103bf585dd9523f100837d336e"
570
  },
571
  "NOTICE": {
572
  "bytes": 563,
@@ -626,5 +626,5 @@
626
  "metrics_file": "development_metrics.json",
627
  "metrics_scope": "development",
628
  "model_file": "model.onnx",
629
- "source_and_artifact_inventory_sha256": "5d8ac3bec5bdc21d874024019012fefebe1c6e53cd002bc5a2aa8dd4d9e83ef3"
630
  }
 
16
  },
17
  "MODEL_CARD.md": {
18
  "bytes": 14805,
19
+ "sha256": "6add0853cdc0f09ff1abb594149c59e9d53f29ab4dba5de52bed7278cfb13c1b"
20
  },
21
  "Makefile": {
22
  "bytes": 777,
 
28
  },
29
  "README.md": {
30
  "bytes": 14805,
31
+ "sha256": "6add0853cdc0f09ff1abb594149c59e9d53f29ab4dba5de52bed7278cfb13c1b"
32
  },
33
  "REPORT.md": {
34
  "bytes": 32272,
35
+ "sha256": "9205011a4c547b8cf537d5b3538d25f1b8a9545c9bb020694f6d94deb26683f7"
36
  },
37
  "SOURCE_README.md": {
38
+ "bytes": 20535,
39
+ "sha256": "64539b78efae390a42ca19a718b8e7354327826fff81dc6c1a9c7841b11132e0"
40
  },
41
  "app.py": {
42
  "bytes": 10381,
 
108
  },
109
  "export_manifest.json": {
110
  "bytes": 12430,
111
+ "sha256": "1f70d8ec79c9c2430c9d3a16942f51254202279c984048e02a781a33313f242b"
112
  },
113
  "model.onnx": {
114
  "bytes": 625431,
 
283
  "sha256": "89167b269b2103ad0bd94918f2ec4f7b605cf001d391e570683ceef39066b6c2"
284
  },
285
  "scripts/publish_hf.py": {
286
+ "bytes": 43530,
287
+ "sha256": "36311a2457bb0e93cbd6fd0ed444e57a2ec2c44ab9bd17ed06132e9640a9ddc3"
288
  },
289
  "scripts/replay_stream.py": {
290
  "bytes": 3752,
 
527
  "sha256": "12a9a0edda07265de293076d12073a707b10e6fb9ebbf2e7c70ba9a219289bdc"
528
  },
529
  "tests/test_publish_hf.py": {
530
+ "bytes": 25161,
531
+ "sha256": "5af81ad7995b6bfd3816b7d97ee1226027659dc5b8a7cb4fe4376fa850dcd14e"
532
  },
533
  "tests/test_release_guardrails.py": {
534
  "bytes": 17399,
 
566
  },
567
  "MODEL_CARD.md": {
568
  "bytes": 14805,
569
+ "sha256": "6add0853cdc0f09ff1abb594149c59e9d53f29ab4dba5de52bed7278cfb13c1b"
570
  },
571
  "NOTICE": {
572
  "bytes": 563,
 
626
  "metrics_file": "development_metrics.json",
627
  "metrics_scope": "development",
628
  "model_file": "model.onnx",
629
+ "source_and_artifact_inventory_sha256": "4c327046a4f44513d65a2a920a67566c646bf9c82e1f770a7f1ee6614006a47e"
630
  }
scripts/publish_hf.py CHANGED
@@ -306,6 +306,12 @@ def _failure_kind(exc: BaseException) -> str:
306
  status = _http_status(exc)
307
  if status in {401, 403}:
308
  return f"authentication/authorization rejection (HTTP {status})"
 
 
 
 
 
 
309
  if status is not None:
310
  return f"HTTP {status}"
311
  chain = _exception_chain(exc)
@@ -495,6 +501,7 @@ def _ensure_repo(
495
  repo_id: str,
496
  repo_type: str,
497
  private: bool,
 
498
  policy: RetryPolicy,
499
  ) -> Any:
500
  info = _repo_info_or_none(api, repo_id=repo_id, repo_type=repo_type, policy=policy)
@@ -507,6 +514,8 @@ def _ensure_repo(
507
  }
508
  if repo_type == "space":
509
  create_kwargs["space_sdk"] = "gradio"
 
 
510
  _call_with_retry(
511
  f"create {repo_type} repository",
512
  lambda: api.create_repo(**create_kwargs),
@@ -604,12 +613,14 @@ def _publish_component(
604
  release_kind: str,
605
  inventory_digest: str,
606
  policy: RetryPolicy,
 
607
  ) -> ComponentResult:
608
  _ensure_repo(
609
  api,
610
  repo_id=repo_id,
611
  repo_type=repo_type,
612
  private=private,
 
613
  policy=policy,
614
  )
615
  info, initial = _current_verification(
@@ -840,6 +851,10 @@ def _execute_publish(
840
  "model": f"{args.username}/{args.model_name}",
841
  "space": f"{args.username}/{args.space_name}",
842
  },
 
 
 
 
843
  "status": "starting",
844
  "updated_at": _utc_now(),
845
  }
@@ -914,6 +929,7 @@ def _execute_publish(
914
  release_kind=release_kind,
915
  inventory_digest=manifest["release_inventory_sha256"],
916
  policy=policy,
 
917
  )
918
  receipt["space"] = space.as_dict()
919
  receipt["status"] = "space_content_verified"
@@ -1018,6 +1034,14 @@ def parse_args() -> argparse.Namespace:
1018
  parser.add_argument("--username", default="suvradeepp")
1019
  parser.add_argument("--model-name", default="tiny-hinglish-turn-detector")
1020
  parser.add_argument("--space-name", default="tiny-hinglish-turn-detector")
 
 
 
 
 
 
 
 
1021
  parser.add_argument("--release-dir", default="release")
1022
  parser.add_argument("--receipt", default="reports/hf_publish_receipt.json")
1023
  parser.add_argument("--private", action="store_true")
 
306
  status = _http_status(exc)
307
  if status in {401, 403}:
308
  return f"authentication/authorization rejection (HTTP {status})"
309
+ if status == 402:
310
+ return (
311
+ "Hugging Face plan/hardware entitlement required (HTTP 402); "
312
+ "verify free ZeroGPU eligibility and an available slot for "
313
+ "--space-hardware zero-a10g, otherwise upgrade the account plan"
314
+ )
315
  if status is not None:
316
  return f"HTTP {status}"
317
  chain = _exception_chain(exc)
 
501
  repo_id: str,
502
  repo_type: str,
503
  private: bool,
504
+ space_hardware: str | None,
505
  policy: RetryPolicy,
506
  ) -> Any:
507
  info = _repo_info_or_none(api, repo_id=repo_id, repo_type=repo_type, policy=policy)
 
514
  }
515
  if repo_type == "space":
516
  create_kwargs["space_sdk"] = "gradio"
517
+ if space_hardware is not None:
518
+ create_kwargs["space_hardware"] = space_hardware
519
  _call_with_retry(
520
  f"create {repo_type} repository",
521
  lambda: api.create_repo(**create_kwargs),
 
613
  release_kind: str,
614
  inventory_digest: str,
615
  policy: RetryPolicy,
616
+ space_hardware: str | None = None,
617
  ) -> ComponentResult:
618
  _ensure_repo(
619
  api,
620
  repo_id=repo_id,
621
  repo_type=repo_type,
622
  private=private,
623
+ space_hardware=space_hardware if repo_type == "space" else None,
624
  policy=policy,
625
  )
626
  info, initial = _current_verification(
 
851
  "model": f"{args.username}/{args.model_name}",
852
  "space": f"{args.username}/{args.space_name}",
853
  },
854
+ "space_creation": {
855
+ "sdk": "gradio",
856
+ "hardware_if_created": args.space_hardware,
857
+ },
858
  "status": "starting",
859
  "updated_at": _utc_now(),
860
  }
 
929
  release_kind=release_kind,
930
  inventory_digest=manifest["release_inventory_sha256"],
931
  policy=policy,
932
+ space_hardware=args.space_hardware,
933
  )
934
  receipt["space"] = space.as_dict()
935
  receipt["status"] = "space_content_verified"
 
1034
  parser.add_argument("--username", default="suvradeepp")
1035
  parser.add_argument("--model-name", default="tiny-hinglish-turn-detector")
1036
  parser.add_argument("--space-name", default="tiny-hinglish-turn-detector")
1037
+ parser.add_argument(
1038
+ "--space-hardware",
1039
+ default="zero-a10g",
1040
+ help=(
1041
+ "Hardware requested only when creating a new Space (default: zero-a10g, "
1042
+ "the free-account-compatible ZeroGPU tier)"
1043
+ ),
1044
+ )
1045
  parser.add_argument("--release-dir", default="release")
1046
  parser.add_argument("--receipt", default="reports/hf_publish_receipt.json")
1047
  parser.add_argument("--private", action="store_true")
tests/test_publish_hf.py CHANGED
@@ -10,6 +10,7 @@ from io import StringIO
10
  from pathlib import Path
11
  from types import SimpleNamespace
12
  from typing import Any
 
13
 
14
  from scripts.publish_hf import (
15
  PublishFailure,
@@ -24,6 +25,7 @@ from scripts.publish_hf import (
24
  _publish_component,
25
  _validate_inventory,
26
  _wait_for_space,
 
27
  )
28
 
29
 
@@ -37,6 +39,7 @@ class FakeHubApi:
37
  def __init__(self) -> None:
38
  self.repos: dict[tuple[str, str], dict[str, Any]] = {}
39
  self.history: dict[tuple[str, str, str], dict[str, bytes]] = {}
 
40
  self.upload_calls: list[dict[str, Any]] = []
41
  self.runtime_stages = ["BUILDING", "RUNNING"]
42
  self.upload_mode = "normal"
@@ -94,6 +97,7 @@ class FakeHubApi:
94
  )
95
 
96
  def create_repo(self, **kwargs: Any) -> None:
 
97
  key = (kwargs["repo_type"], kwargs["repo_id"])
98
  if key not in self.repos:
99
  self.add_repo(
@@ -183,6 +187,7 @@ class PublishHfTest(unittest.TestCase):
183
  *,
184
  repo_type: str = "model",
185
  replace_existing: bool = False,
 
186
  ):
187
  return _publish_component(
188
  api,
@@ -197,8 +202,63 @@ class PublishHfTest(unittest.TestCase):
197
  release_kind="development preview",
198
  inventory_digest="a" * 64,
199
  policy=self.policy,
 
200
  )
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  def test_unmanaged_existing_repo_requires_explicit_replacement(self) -> None:
203
  api = FakeHubApi()
204
  api.add_repo("suvradeepp/test-model", "model", {"README.md": b"unrelated"})
@@ -387,6 +447,7 @@ class PublishHfTest(unittest.TestCase):
387
  username="suvradeepp",
388
  model_name="test-model",
389
  space_name="test-space",
 
390
  receipt=str(receipt_path),
391
  private=False,
392
  replace_existing=False,
@@ -474,6 +535,7 @@ class PublishHfTest(unittest.TestCase):
474
  username="suvradeepp",
475
  model_name="test-model",
476
  space_name="test-space",
 
477
  receipt=str(receipt),
478
  private=False,
479
  replace_existing=False,
@@ -491,6 +553,10 @@ class PublishHfTest(unittest.TestCase):
491
  policy=self.policy,
492
  )
493
  self.assertEqual(result["status"], "content_verified_runtime_running")
 
 
 
 
494
  persisted = receipt.read_text(encoding="utf-8")
495
  self.assertNotIn("secret-token", persisted)
496
  self.assertEqual(json.loads(persisted)["space"]["runtime_stage"], "RUNNING")
@@ -543,6 +609,7 @@ class PublishHfTest(unittest.TestCase):
543
  username="suvradeepp",
544
  model_name="test-model",
545
  space_name="test-space",
 
546
  receipt=str(receipt_path),
547
  private=False,
548
  replace_existing=False,
 
10
  from pathlib import Path
11
  from types import SimpleNamespace
12
  from typing import Any
13
+ from unittest.mock import patch
14
 
15
  from scripts.publish_hf import (
16
  PublishFailure,
 
25
  _publish_component,
26
  _validate_inventory,
27
  _wait_for_space,
28
+ parse_args,
29
  )
30
 
31
 
 
39
  def __init__(self) -> None:
40
  self.repos: dict[tuple[str, str], dict[str, Any]] = {}
41
  self.history: dict[tuple[str, str, str], dict[str, bytes]] = {}
42
+ self.create_calls: list[dict[str, Any]] = []
43
  self.upload_calls: list[dict[str, Any]] = []
44
  self.runtime_stages = ["BUILDING", "RUNNING"]
45
  self.upload_mode = "normal"
 
97
  )
98
 
99
  def create_repo(self, **kwargs: Any) -> None:
100
+ self.create_calls.append(dict(kwargs))
101
  key = (kwargs["repo_type"], kwargs["repo_id"])
102
  if key not in self.repos:
103
  self.add_repo(
 
187
  *,
188
  repo_type: str = "model",
189
  replace_existing: bool = False,
190
+ space_hardware: str | None = None,
191
  ):
192
  return _publish_component(
193
  api,
 
202
  release_kind="development preview",
203
  inventory_digest="a" * 64,
204
  policy=self.policy,
205
+ space_hardware=space_hardware,
206
  )
207
 
208
+ def test_space_hardware_is_sent_only_when_creating_a_new_space(self) -> None:
209
+ api = FakeHubApi()
210
+ with tempfile.TemporaryDirectory() as directory:
211
+ folder = Path(directory)
212
+ (folder / "payload.bin").write_bytes(b"payload")
213
+ self._component(api, folder)
214
+ self._component(
215
+ api,
216
+ folder,
217
+ repo_type="space",
218
+ space_hardware="zero-a10g",
219
+ )
220
+ self._component(
221
+ api,
222
+ folder,
223
+ repo_type="space",
224
+ space_hardware="cpu-basic",
225
+ )
226
+
227
+ model_create, space_create = api.create_calls
228
+ self.assertNotIn("space_hardware", model_create)
229
+ self.assertEqual(space_create["space_sdk"], "gradio")
230
+ self.assertEqual(space_create["space_hardware"], "zero-a10g")
231
+
232
+ def test_space_hardware_cli_defaults_to_zerogpu(self) -> None:
233
+ with patch("sys.argv", ["publish_hf.py"]):
234
+ self.assertEqual(parse_args().space_hardware, "zero-a10g")
235
+
236
+ def test_space_create_402_is_actionable_sanitized_and_not_retried(self) -> None:
237
+ secret = "hf_do_not_print_me"
238
+
239
+ class PlanGatedApi(FakeHubApi):
240
+ def create_repo(self, **kwargs: Any) -> None:
241
+ self.create_calls.append(dict(kwargs))
242
+ raise FakeHttpError(402, f"billing failure containing {secret}")
243
+
244
+ api = PlanGatedApi()
245
+ with tempfile.TemporaryDirectory() as directory:
246
+ folder = Path(directory)
247
+ (folder / "payload.bin").write_bytes(b"payload")
248
+ with self.assertRaises(PublishFailure) as caught:
249
+ self._component(
250
+ api,
251
+ folder,
252
+ repo_type="space",
253
+ space_hardware="zero-a10g",
254
+ )
255
+
256
+ message = str(caught.exception)
257
+ self.assertEqual(len(api.create_calls), 1)
258
+ self.assertIn("HTTP 402", message)
259
+ self.assertIn("--space-hardware zero-a10g", message)
260
+ self.assertNotIn(secret, message)
261
+
262
  def test_unmanaged_existing_repo_requires_explicit_replacement(self) -> None:
263
  api = FakeHubApi()
264
  api.add_repo("suvradeepp/test-model", "model", {"README.md": b"unrelated"})
 
447
  username="suvradeepp",
448
  model_name="test-model",
449
  space_name="test-space",
450
+ space_hardware="zero-a10g",
451
  receipt=str(receipt_path),
452
  private=False,
453
  replace_existing=False,
 
535
  username="suvradeepp",
536
  model_name="test-model",
537
  space_name="test-space",
538
+ space_hardware="zero-a10g",
539
  receipt=str(receipt),
540
  private=False,
541
  replace_existing=False,
 
553
  policy=self.policy,
554
  )
555
  self.assertEqual(result["status"], "content_verified_runtime_running")
556
+ self.assertEqual(
557
+ result["space_creation"],
558
+ {"sdk": "gradio", "hardware_if_created": "zero-a10g"},
559
+ )
560
  persisted = receipt.read_text(encoding="utf-8")
561
  self.assertNotIn("secret-token", persisted)
562
  self.assertEqual(json.loads(persisted)["space"]["runtime_stage"], "RUNNING")
 
609
  username="suvradeepp",
610
  model_name="test-model",
611
  space_name="test-space",
612
+ space_hardware="zero-a10g",
613
  receipt=str(receipt_path),
614
  private=False,
615
  replace_existing=False,