From eb151dac8d0979cf2148ae4cc4968b6c79ef1a74 Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Sun, 15 Feb 2026 19:27:23 +0100 Subject: [PATCH] Add Hugging Face Hub integration for safetensor files - Add `tsr hf search` to search HF models with safetensor files - Add `tsr hf get` to view model info and list safetensor files - Add `tsr hf files` to list safetensor files in a model - Add `tsr hf dl` to download safetensor files from HF Uses official huggingface_hub library for API access. Only safetensor files are supported (enforced at search and download). Co-Authored-By: Claude Opus 4.5 --- README.md | 36 +++++++ pyproject.toml | 5 + tensors/cli.py | 128 +++++++++++++++++++++++ tensors/display.py | 132 ++++++++++++++++++++++++ tensors/hf.py | 246 +++++++++++++++++++++++++++++++++++++++++++++ uv.lock | 88 +++++++++++++++- 6 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 tensors/hf.py diff --git a/README.md b/README.md index 2dd59c0..575a195 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,30 @@ tsr dl -H ABC123... tsr dl -m 12345 -o ./models ``` +### Hugging Face Integration + +Search and download safetensor files from Hugging Face Hub. + +```bash +# Search for models with safetensor files +tsr hf search "flux" + +# Filter by author or pipeline +tsr hf search -a stabilityai -p text-to-image + +# Get model info and list safetensor files +tsr hf get black-forest-labs/FLUX.1-schnell + +# List safetensor files only +tsr hf files black-forest-labs/FLUX.1-schnell + +# Download a specific safetensor file +tsr hf dl black-forest-labs/FLUX.1-schnell -f ae.safetensors + +# Download all safetensor files from a model +tsr hf dl author/model --all -o ./models +``` + ### Inspect Local Files ```bash @@ -293,6 +317,18 @@ Data is stored in XDG-compliant paths: | `--commercial` | none, image, rent, sell | | `--page` | Page number for pagination | +## Hugging Face Options + +| Option | Description | +|--------|-------------| +| `-a, --author` | Filter by author/organization | +| `-p, --pipeline` | Pipeline tag (text-to-image, text-generation, etc.) | +| `-s, --sort` | Sort by (downloads, likes, created_at, trending_score) | +| `-n, --limit` | Number of results (default: 25) | +| `-f, --file` | Specific file to download | +| `--all` | Download all safetensor files | +| `-o, --output` | Output directory | + ## Generate Options | Option | Description | diff --git a/pyproject.toml b/pyproject.toml index 25936eb..4f8a39d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "rich>=13.0.0", "typer>=0.15.0", "websocket-client>=1.9.0", + "huggingface_hub>=0.25.0", ] [project.optional-dependencies] @@ -98,6 +99,10 @@ ignore_missing_imports = true module = ["scalar_fastapi.*"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["huggingface_hub.*"] +ignore_missing_imports = true + [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-v --cov=tensors --cov-report=term-missing" diff --git a/tensors/cli.py b/tensors/cli.py index 596a212..b099d76 100644 --- a/tensors/cli.py +++ b/tensors/cli.py @@ -37,10 +37,19 @@ from tensors.display import ( _format_size, display_civitai_data, display_file_info, + display_hf_model_info, + display_hf_search_results, display_local_metadata, display_model_info, display_search_results, ) +from tensors.hf import ( + download_all_safetensors, + download_hf_safetensor, + get_hf_model, + list_safetensor_files, + search_hf_models, +) from tensors.safetensor import compute_sha256, get_base_name, read_safetensor_metadata # Key masking threshold @@ -720,6 +729,124 @@ def db_stats( console.print(table) +# ============================================================================= +# Hugging Face Commands +# ============================================================================= + +hf_app = typer.Typer(name="hf", help="Hugging Face Hub commands for safetensor files.") +app.add_typer(hf_app) + + +@hf_app.command("search") +def hf_search( + query: Annotated[str | None, typer.Argument(help="Search query")] = None, + author: Annotated[str | None, typer.Option("-a", "--author", help="Filter by author/org")] = None, + pipeline: Annotated[str | None, typer.Option("-p", "--pipeline", help="Pipeline tag (text-to-image, etc.)")] = None, + sort: Annotated[str | None, typer.Option("-s", "--sort", help="Sort by (downloads, likes, created_at)")] = None, + limit: Annotated[int, typer.Option("-n", "--limit", help="Max results")] = 25, + json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False, +) -> None: + """Search Hugging Face for models with safetensor files.""" + results = search_hf_models( + query=query, + author=author, + pipeline_tag=pipeline, + sort=sort, + limit=limit, + console=console, + ) + + if json_output: + console.print_json(data=results) + return + + display_hf_search_results(results, console) + + +@hf_app.command("get") +def hf_get( + model_id: Annotated[str, typer.Argument(help="Model ID (e.g., stabilityai/stable-diffusion-xl-base-1.0)")], + json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False, +) -> None: + """Get Hugging Face model info and list safetensor files.""" + model = get_hf_model(model_id, console=console) + + if not model: + raise typer.Exit(1) + + if json_output: + console.print_json(data=model) + return + + display_hf_model_info(model, console) + + +@hf_app.command("files") +def hf_files( + model_id: Annotated[str, typer.Argument(help="Model ID")], + json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False, +) -> None: + """List safetensor files in a Hugging Face model.""" + files = list_safetensor_files(model_id, console=console) + + if json_output: + console.print_json(data=files) + return + + if not files: + console.print("[yellow]No safetensor files found.[/yellow]") + return + + console.print(f"[bold]Safetensor files in {model_id}:[/bold]") + for i, f in enumerate(files, 1): + console.print(f" {i}. {f}") + + +@hf_app.command("dl") +def hf_download( + model_id: Annotated[str, typer.Argument(help="Model ID (e.g., stabilityai/stable-diffusion-xl-base-1.0)")], + filename: Annotated[str | None, typer.Option("-f", "--file", help="Specific file to download")] = None, + output: Annotated[Path | None, typer.Option("-o", "--output", help="Output directory")] = None, + all_files: Annotated[bool, typer.Option("--all", "-a", help="Download all safetensor files")] = False, +) -> None: + """Download safetensor files from Hugging Face. + + Examples: + tsr hf dl stabilityai/stable-diffusion-xl-base-1.0 -f sd_xl_base_1.0.safetensors + tsr hf dl author/model --all + """ + output_dir = output or Path.cwd() + + if all_files: + downloaded = download_all_safetensors(model_id, output_dir, console=console) + if downloaded: + console.print(f"[green]Downloaded {len(downloaded)} files[/green]") + else: + console.print("[red]No files downloaded[/red]") + raise typer.Exit(1) + return + + if not filename: + # List files and prompt or show help + files = list_safetensor_files(model_id, console=console) + if not files: + console.print("[red]No safetensor files found in model[/red]") + raise typer.Exit(1) + + if len(files) == 1: + filename = files[0] + console.print(f"[dim]Downloading only safetensor file: {filename}[/dim]") + else: + console.print("[yellow]Multiple safetensor files found. Specify one with -f or use --all:[/yellow]") + for i, f in enumerate(files, 1): + console.print(f" {i}. {f}") + raise typer.Exit(1) + + result = download_hf_safetensor(model_id, filename, output_dir, console=console) + if not result: + raise typer.Exit(1) + + def main() -> int: """Main entry point.""" # Handle legacy invocation: tsr -> tsr info @@ -732,6 +859,7 @@ def main() -> int: "config", "serve", "db", + "hf", ) if len(sys.argv) > 1 and not sys.argv[1].startswith("-"): arg = sys.argv[1] diff --git a/tensors/display.py b/tensors/display.py index 0466767..39360dc 100644 --- a/tensors/display.py +++ b/tensors/display.py @@ -329,3 +329,135 @@ def display_search_results(results: dict[str, Any], console: Console) -> None: total = metadata.get("totalItems", len(items)) console.print(f"\n[dim]Showing {len(items)} of {total:,} results[/dim]") console.print("[dim]Use 'tsr get ' to view details or 'tsr dl -m ' to download[/dim]") + + +# ============================================================================= +# Hugging Face Display Functions +# ============================================================================= + + +def _format_bytes(size_bytes: int) -> str: + """Format size in bytes to human-readable string.""" + if size_bytes < KB: + return f"{size_bytes} B" + if size_bytes < KB * KB: + return f"{size_bytes / KB:.1f} KB" + if size_bytes < KB * KB * KB: + return f"{size_bytes / KB / KB:.1f} MB" + return f"{size_bytes / KB / KB / KB:.2f} GB" + + +def _build_hf_search_table(console: Console) -> Table: + """Build Hugging Face search results table.""" + id_width = 40 + dls_width = 8 + likes_width = 6 + files_width = 5 + + terminal_width = console.size.width + fixed_width = id_width + dls_width + likes_width + files_width + overhead = 17 + author_width = max(15, (terminal_width - fixed_width - overhead) // 2) + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Model ID", style="cyan", width=id_width, no_wrap=True, overflow="ellipsis") + table.add_column("Author", style="yellow", width=author_width, no_wrap=True, overflow="ellipsis") + table.add_column("DLs", justify="right", width=dls_width, no_wrap=True) + table.add_column("Likes", justify="right", width=likes_width, no_wrap=True) + table.add_column("Files", justify="right", width=files_width, no_wrap=True) + + return table + + +def display_hf_search_results(models: list[dict[str, Any]], console: Console) -> None: + """Display Hugging Face search results in a table.""" + if not models: + console.print("[yellow]No results found.[/yellow]") + return + + table = _build_hf_search_table(console) + + for model in models: + model_id = model.get("id", "N/A") + author = model.get("author", model_id.split("/")[0] if "/" in model_id else "N/A") + downloads = _format_count(model.get("downloads", 0)) + likes = _format_count(model.get("likes", 0)) + safetensor_files = model.get("_safetensor_files", []) + files_count = str(len(safetensor_files)) + + table.add_row(model_id, author, downloads, likes, files_count) + + console.print() + console.print(table) + console.print(f"\n[dim]Showing {len(models)} models with safetensor files[/dim]") + console.print("[dim]Use 'tsr hf get ' to view details or 'tsr hf dl ' to download[/dim]") + + +def _build_hf_model_table(console: Console) -> Table: + """Build Hugging Face model info table.""" + prop_width = 12 + terminal_width = console.size.width + overhead = 7 + value_width = max(40, terminal_width - prop_width - overhead) + + table = Table(title="Hugging Face Model", show_header=True, header_style="bold magenta") + table.add_column("Property", style="cyan", width=prop_width, no_wrap=True) + table.add_column("Value", style="green", width=value_width, no_wrap=True, overflow="ellipsis") + + return table + + +def display_hf_model_info(model: dict[str, Any], console: Console) -> None: + """Display Hugging Face model information.""" + if not model: + console.print("[yellow]Model not found.[/yellow]") + return + + table = _build_hf_model_table(console) + + model_id = model.get("id", "N/A") + table.add_row("Model ID", model_id) + table.add_row("Author", model.get("author", "N/A")) + table.add_row("Downloads", f"{model.get('downloads', 0):,}") + table.add_row("Likes", f"{model.get('likes', 0):,}") + + # Handle datetime or string values + created = model.get("created_at") or model.get("createdAt") + if created: + created_str = created.strftime("%Y-%m-%d") if hasattr(created, "strftime") else str(created)[:10] + table.add_row("Created", created_str) + + updated = model.get("last_modified") or model.get("lastModified") + if updated: + updated_str = updated.strftime("%Y-%m-%d") if hasattr(updated, "strftime") else str(updated)[:10] + table.add_row("Updated", updated_str) + + tags = model.get("tags", []) + if tags: + table.add_row("Tags", ", ".join(tags[:MAX_TAGS_DISPLAY]) + ("..." if len(tags) > MAX_TAGS_DISPLAY else "")) + + pipeline = model.get("pipeline_tag") + if pipeline: + table.add_row("Pipeline", pipeline) + + console.print() + console.print(table) + + # Display safetensor files + safetensor_files = model.get("_safetensor_files", []) + if safetensor_files: + files_table = Table(title="Safetensor Files", show_header=True, header_style="bold magenta") + files_table.add_column("#", style="dim", width=3, justify="right") + files_table.add_column("Filename", style="cyan", no_wrap=True, overflow="ellipsis") + files_table.add_column("Size", style="green", justify="right", width=10) + + for i, f in enumerate(safetensor_files, 1): + filename = f.get("rfilename", "N/A") + size = _format_bytes(f.get("size", 0)) if f.get("size") else "N/A" + files_table.add_row(str(i), filename, size) + + console.print() + console.print(files_table) + + console.print() + console.print(f"[bold blue]View on HuggingFace:[/bold blue] https://huggingface.co/{model_id}") diff --git a/tensors/hf.py b/tensors/hf.py new file mode 100644 index 0000000..87b182e --- /dev/null +++ b/tensors/hf.py @@ -0,0 +1,246 @@ +"""Hugging Face Hub integration for safetensor files.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from huggingface_hub import HfApi, hf_hub_download, list_repo_files +from huggingface_hub.errors import RepositoryNotFoundError + +if TYPE_CHECKING: + from rich.console import Console + +# Shared API instance +_api: HfApi | None = None + + +def _get_api(token: str | None = None) -> HfApi: + """Get or create HfApi instance.""" + global _api # noqa: PLW0603 + if _api is None: + _api = HfApi(token=token) + return _api + + +def search_hf_models( + query: str | None = None, + *, + author: str | None = None, + tags: list[str] | None = None, + pipeline_tag: str | None = None, + sort: str | None = None, + limit: int = 25, + token: str | None = None, + console: Console | None = None, +) -> list[dict[str, Any]]: + """Search Hugging Face models with safetensor files. + + Args: + query: Search query string + author: Filter by author/organization + tags: Additional tags to filter by + pipeline_tag: Pipeline type (text-generation, text-to-image, etc.) + sort: Sort field (downloads, likes, created_at, trending_score) + limit: Maximum results + token: HuggingFace API token + console: Rich console for output + + Returns: + List of model info dictionaries with safetensor files + """ + api = _get_api(token) + + # Build filter list - always include safetensors + filters = ["safetensors"] + if tags: + filters.extend(tags) + + try: + models = api.list_models( + search=query, + author=author, + filter=filters, + pipeline_tag=pipeline_tag, + sort=sort or "downloads", + limit=limit, + expand=["siblings", "downloads", "likes", "author", "lastModified", "createdAt", "tags"], + ) + + results = [] + for model in models: + model_dict = model.__dict__.copy() + + # Get safetensor files from siblings + siblings = getattr(model, "siblings", None) or [] + safetensor_files = [ + {"rfilename": s.rfilename, "size": getattr(s, "size", None)} + for s in siblings + if s.rfilename.endswith(".safetensors") + ] + + if safetensor_files: + model_dict["_safetensor_files"] = safetensor_files + results.append(model_dict) + + return results + + except Exception as e: + if console: + console.print(f"[red]Error searching models: {e}[/red]") + return [] + + +def get_hf_model( + model_id: str, + token: str | None = None, + console: Console | None = None, +) -> dict[str, Any] | None: + """Get detailed model information from Hugging Face. + + Args: + model_id: Model ID (e.g., "stabilityai/stable-diffusion-xl-base-1.0") + token: HuggingFace API token + console: Rich console for output + + Returns: + Model info dictionary or None if not found + """ + api = _get_api(token) + + try: + model = api.model_info(model_id, files_metadata=True) + model_dict = model.__dict__.copy() + + # Get safetensor files + siblings = getattr(model, "siblings", None) or [] + safetensor_files = [ + {"rfilename": s.rfilename, "size": getattr(s, "size", None)} + for s in siblings + if s.rfilename.endswith(".safetensors") + ] + model_dict["_safetensor_files"] = safetensor_files + + return model_dict + + except RepositoryNotFoundError: + if console: + console.print(f"[red]Model not found: {model_id}[/red]") + return None + except Exception as e: + if console: + console.print(f"[red]Error fetching model: {e}[/red]") + return None + + +def list_safetensor_files( + model_id: str, + token: str | None = None, + console: Console | None = None, +) -> list[str]: + """List all safetensor files in a Hugging Face model. + + Args: + model_id: Model ID + token: HuggingFace API token + console: Rich console for output + + Returns: + List of safetensor filenames + """ + try: + files = list_repo_files(model_id, token=token) + return [f for f in files if f.endswith(".safetensors")] + except RepositoryNotFoundError: + if console: + console.print(f"[red]Model not found: {model_id}[/red]") + return [] + except Exception as e: + if console: + console.print(f"[red]Error listing files: {e}[/red]") + return [] + + +def download_hf_safetensor( + model_id: str, + filename: str, + output_dir: Path, + token: str | None = None, + console: Console | None = None, + *, + resume: bool = True, +) -> Path | None: + """Download a safetensor file from Hugging Face. + + Args: + model_id: Model ID (e.g., "stabilityai/stable-diffusion-xl-base-1.0") + filename: File name within the model repo + output_dir: Directory to save the file + token: HuggingFace API token + console: Rich console for progress output + resume: Whether to resume partial downloads + + Returns: + Path to downloaded file, or None on failure + """ + if not filename.endswith(".safetensors"): + if console: + console.print("[red]Only .safetensors files are supported[/red]") + return None + + try: + # hf_hub_download handles caching and resume automatically + downloaded_path = hf_hub_download( + repo_id=model_id, + filename=filename, + local_dir=output_dir, + token=token, + force_download=not resume, + ) + + if console: + console.print(f"[green]Downloaded: {downloaded_path}[/green]") + + return Path(downloaded_path) + + except RepositoryNotFoundError: + if console: + console.print(f"[red]Model not found: {model_id}[/red]") + return None + except Exception as e: + if console: + console.print(f"[red]Download failed: {e}[/red]") + return None + + +def download_all_safetensors( + model_id: str, + output_dir: Path, + token: str | None = None, + console: Console | None = None, +) -> list[Path]: + """Download all safetensor files from a model. + + Args: + model_id: Model ID + output_dir: Directory to save files + token: HuggingFace API token + console: Rich console for output + + Returns: + List of downloaded file paths + """ + files = list_safetensor_files(model_id, token, console) + if not files: + return [] + + downloaded = [] + for filename in files: + if console: + console.print(f"[dim]Downloading {filename}...[/dim]") + + path = download_hf_safetensor(model_id, filename, output_dir, token, console) + if path: + downloaded.append(path) + + return downloaded diff --git a/uv.lock b/uv.lock index 0dd179d..84c663c 100644 --- a/uv.lock +++ b/uv.lock @@ -180,6 +180,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -189,6 +198,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -217,6 +255,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" }, +] + [[package]] name = "identify" version = "2.6.16" @@ -716,10 +775,11 @@ wheels = [ [[package]] name = "tensors" -version = "0.1.16" +version = "0.1.18" source = { editable = "." } dependencies = [ { name = "httpx" }, + { name = "huggingface-hub" }, { name = "rich" }, { name = "safetensors" }, { name = "typer" }, @@ -751,6 +811,7 @@ dev = [ requires-dist = [ { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115" }, { name = "httpx", specifier = ">=0.27.0" }, + { name = "huggingface-hub", specifier = ">=0.25.0" }, { name = "rich", specifier = ">=13.0.0" }, { name = "safetensors", specifier = ">=0.4.0" }, { name = "scalar-fastapi", marker = "extra == 'server'", specifier = ">=1.6" }, @@ -774,6 +835,18 @@ dev = [ { name = "uvicorn", specifier = ">=0.30" }, ] +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + [[package]] name = "typer" version = "0.21.1" @@ -789,6 +862,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] +[[package]] +name = "typer-slim" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/0d9d822fd8a4c7e830cba36a2557b070d4b4a9558a0460377a61f8fb315d/typer_slim-0.21.2.tar.gz", hash = "sha256:78f20d793036a62aaf9c3798306142b08261d4b2a941c6e463081239f062a2f9", size = 120497, upload-time = "2026-02-10T19:33:45.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/03/e09325cfc40a33a82b31ba1a3f1d97e85246736856a45a43b19fcb48b1c2/typer_slim-0.21.2-py3-none-any.whl", hash = "sha256:4705082bb6c66c090f60e47c8be09a93158c139ce0aa98df7c6c47e723395e5f", size = 56790, upload-time = "2026-02-10T19:33:47.221Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"