From 96792320d44b5738cd59bae476b32e0cbcd2f8a9 Mon Sep 17 00:00:00 2001 From: zhangchi47 Date: Wed, 12 Aug 2026 23:26:03 +0800 Subject: [PATCH] fix(files): OCR image-only PDFs before parser fallback MarkItDown only passed image uploads to its configured vision model. PDFs without a text layer therefore returned no content even when OCR was enabled. When normal PDF extraction is empty, render each page at 200 DPI and pass the temporary PNG through MarkItDown's existing image OCR path. Preserve useful pages when one page fails, and raise an actionable error when OCR is disabled or produces no content. The existing parser chain can then advance to the next configured parser, such as llama_parse. Add pypdfium2 for page rendering, document the behavior, and cover page OCR, partial failures, disabled OCR, and parser-chain fallback with regression tests. --- .../engine/parsers/markitdown.py | 74 ++++- hindsight-api-slim/pyproject.toml | 1 + hindsight-api-slim/tests/test_file_retain.py | 266 ++++++++++++++++++ .../docs/developer/configuration.md | 2 +- .../references/developer/configuration.md | 2 +- uv.lock | 94 ++++--- 6 files changed, 390 insertions(+), 49 deletions(-) diff --git a/hindsight-api-slim/hindsight_api/engine/parsers/markitdown.py b/hindsight-api-slim/hindsight_api/engine/parsers/markitdown.py index 624fb5086..11c6052f8 100644 --- a/hindsight-api-slim/hindsight_api/engine/parsers/markitdown.py +++ b/hindsight-api-slim/hindsight_api/engine/parsers/markitdown.py @@ -33,6 +33,8 @@ ".html", ".htm", } +_PDF_EXTENSION = ".pdf" +_PDF_OCR_DPI = 200 @dataclass(frozen=True) @@ -165,17 +167,38 @@ def _convert_sync(self, file_data: bytes, filename: str) -> str: tmp.write(file_data) tmp_path = tmp.name + pdf_ocr_attempted = False try: # Parse using markitdown, passing an explicit charset hint for text # files to avoid markitdown's sample-based (and crash-prone) detection. result = self._markitdown.convert(tmp_path, stream_info=self._utf8_stream_info(file_data, filename)) - if not result or not result.text_content: + if ( + not result + or not result.text_content + or (Path(filename).suffix.lower() == _PDF_EXTENSION and not result.text_content.strip()) + ): + if Path(filename).suffix.lower() == _PDF_EXTENSION: + pdf_ocr_attempted = True + return self._ocr_pdf_sync(file_data, filename) raise RuntimeError(f"No content extracted from '{filename}'") return result.text_content except Exception as e: + if Path(filename).suffix.lower() == _PDF_EXTENSION and not pdf_ocr_attempted: + # Some PDF converters raise instead of returning an empty result + # for image-only PDFs; give the OCR fallback the same opportunity. + try: + return self._ocr_pdf_sync(file_data, filename) + except Exception as ocr_error: + logger.error( + "MarkItDown PDF parsing and OCR failed for %s: %s; OCR error: %s", + filename, + e, + ocr_error, + ) + raise RuntimeError(f"{e}; PDF OCR fallback failed: {ocr_error}") from ocr_error logger.error(f"Markitdown parsing failed for {filename}: {e}") raise RuntimeError(f"Failed to parse '{filename}': {e}") from e @@ -186,6 +209,55 @@ def _convert_sync(self, file_data: bytes, filename: str) -> str: except Exception: pass + def _ocr_pdf_sync(self, file_data: bytes, filename: str) -> str: + """Render an image-only PDF page-by-page and run the configured image OCR.""" + if not self._ocr_enabled: + raise RuntimeError( + f"PDF '{filename}' appears to be scanned images without selectable text. " + "Configure MarkItDown OCR or choose an OCR-capable parser." + ) + + import pypdfium2 as pdfium + + page_text: list[str] = [] + try: + document = pdfium.PdfDocument(bytes(file_data)) + try: + for page_number, page in enumerate(document): + bitmap = page.render(scale=_PDF_OCR_DPI / 72) + with tempfile.NamedTemporaryFile(suffix=f"-{page_number + 1}.png", delete=False) as tmp: + image_path = tmp.name + try: + bitmap.to_pil().save(image_path, format="PNG") + try: + result = self._markitdown.convert(image_path) + text = result.text_content if result else "" + if text and text.strip(): + page_text.append(text.strip()) + except Exception as page_error: + # A damaged page should not discard useful OCR from the rest + # of the document. If every page fails, the empty-result error + # below advances the configured parser fallback chain. + logger.warning( + "MarkItDown OCR failed for page %d of '%s': %s", + page_number + 1, + filename, + page_error, + ) + finally: + try: + Path(image_path).unlink() + except OSError: + pass + finally: + document.close() + except Exception as e: + raise RuntimeError(f"OCR failed for scanned PDF '{filename}': {e}") from e + + if not page_text: + raise RuntimeError(f"No OCR content extracted from scanned PDF '{filename}'") + return "\n\n".join(page_text) + @staticmethod def _utf8_stream_info(file_data: bytes, filename: str) -> "StreamInfo | None": """Return a UTF-8 charset hint for text files that decode cleanly as UTF-8. diff --git a/hindsight-api-slim/pyproject.toml b/hindsight-api-slim/pyproject.toml index d3e08ce7b..3959ee3a2 100644 --- a/hindsight-api-slim/pyproject.toml +++ b/hindsight-api-slim/pyproject.toml @@ -75,6 +75,7 @@ dependencies = [ # wheels (tracked upstream in BerriAI/litellm#31261). "litellm>=1.91.3,<1.92; sys_platform == 'darwin'", "markitdown[pdf,docx,pptx,xlsx,xls]>=0.1.4", # File to markdown conversion + "pypdfium2>=5.4.0", # Render scanned PDF pages for OCR fallback "obstore>=0.4.0", # S3/GCS/Azure object storage client (Rust-backed) "winloop>=0.1.0; sys_platform == 'win32'", "uvloop>=0.22.1; sys_platform != 'win32'", diff --git a/hindsight-api-slim/tests/test_file_retain.py b/hindsight-api-slim/tests/test_file_retain.py index c0bcdc9f1..8a25bc9ad 100644 --- a/hindsight-api-slim/tests/test_file_retain.py +++ b/hindsight-api-slim/tests/test_file_retain.py @@ -5,6 +5,7 @@ import asyncio import io import json +import sys from datetime import datetime, timezone import pytest @@ -519,6 +520,271 @@ def convert(self, path): await parser.convert(b"\x89PNG\r\n\x1a\n", "screenshot.png") +@pytest.mark.asyncio +async def test_markitdown_scanned_pdf_uses_page_ocr(monkeypatch): + """An empty PDF conversion is rasterized and OCR'd one page at a time.""" + import markitdown + + from hindsight_api.engine.parsers import MarkitdownParser + + class FakeResult: + def __init__(self, text_content): + self.text_content = text_content + + class FakeMarkItDown: + def __init__(self, **kwargs): + self.calls = [] + + def convert(self, path, **kwargs): + self.calls.append(path) + return FakeResult("" if path.endswith(".pdf") else f"OCR {len(self.calls)}") + + class FakeBitmap: + def to_pil(self): + return type("Image", (), {"save": lambda self, path, format: None})() + + class FakePage: + def render(self, **kwargs): + assert kwargs == {"scale": 200 / 72} + return FakeBitmap() + + class FakeDocument: + def __iter__(self): + return iter([FakePage(), FakePage()]) + + def close(self): + pass + + class FakePdfium: + @staticmethod + def PdfDocument(file_data): + assert file_data == b"pdf" + return FakeDocument() + + monkeypatch.setattr(markitdown, "MarkItDown", FakeMarkItDown) + monkeypatch.setitem(sys.modules, "pypdfium2", FakePdfium) + + parser = MarkitdownParser( + ocr_enabled=True, ocr_api_key="key", ocr_base_url="https://example.test/v1", ocr_model="vision" + ) + assert await parser.convert(b"pdf", "scan.pdf") == "OCR 2\n\nOCR 3" + + +@pytest.mark.asyncio +async def test_markitdown_pdf_parse_error_uses_page_ocr(monkeypatch): + """A PDF converter exception still gets an OCR fallback attempt.""" + import markitdown + + from hindsight_api.engine.parsers import MarkitdownParser + + class FakeResult: + def __init__(self, text_content): + self.text_content = text_content + + class FakeMarkItDown: + def __init__(self, **kwargs): + self.image_calls = 0 + + def convert(self, path, **kwargs): + if path.endswith(".pdf"): + raise RuntimeError("pdf text extraction failed") + self.image_calls += 1 + return FakeResult("Recovered from OCR") + + class FakePage: + def render(self, **kwargs): + return type( + "Bitmap", (), {"to_pil": lambda self: type("Image", (), {"save": lambda self, path, format: None})()} + )() + + class FakeDocument: + def __iter__(self): + return iter([FakePage()]) + + def close(self): + pass + + class FakePdfium: + @staticmethod + def PdfDocument(file_data): + return FakeDocument() + + monkeypatch.setattr(markitdown, "MarkItDown", FakeMarkItDown) + monkeypatch.setitem(sys.modules, "pypdfium2", FakePdfium) + + parser = MarkitdownParser( + ocr_enabled=True, + ocr_api_key="key", + ocr_base_url="https://example.test/v1", + ocr_model="vision", + ) + assert await parser.convert(b"pdf", "scan.pdf") == "Recovered from OCR" + + +@pytest.mark.asyncio +async def test_markitdown_scanned_pdf_without_ocr_has_actionable_error(monkeypatch): + """A scanned PDF without OCR explains the remediation and can trigger outer fallback.""" + import markitdown + + from hindsight_api.engine.parsers import MarkitdownParser + + class FakeResult: + text_content = "" + + class FakeMarkItDown: + def __init__(self, **kwargs): + pass + + def convert(self, path, **kwargs): + return FakeResult() + + monkeypatch.setattr(markitdown, "MarkItDown", FakeMarkItDown) + parser = MarkitdownParser() + with pytest.raises(RuntimeError, match="scanned images without selectable text"): + await parser.convert(b"pdf", "scan.pdf") + + +@pytest.mark.asyncio +async def test_markitdown_scanned_pdf_skips_failed_ocr_page(monkeypatch): + """One damaged page does not discard OCR content extracted from other pages.""" + import markitdown + + from hindsight_api.engine.parsers import MarkitdownParser + + class FakeResult: + def __init__(self, text_content): + self.text_content = text_content + + class FakeMarkItDown: + def __init__(self, **kwargs): + self.image_calls = 0 + + def convert(self, path, **kwargs): + if path.endswith(".pdf"): + return FakeResult("") + self.image_calls += 1 + if self.image_calls == 1: + raise RuntimeError("damaged page") + return FakeResult("Recovered page") + + class FakePage: + def render(self, **kwargs): + return type( + "Bitmap", (), {"to_pil": lambda self: type("Image", (), {"save": lambda self, path, format: None})()} + )() + + class FakeDocument: + def __iter__(self): + return iter([FakePage(), FakePage()]) + + def close(self): + pass + + class FakePdfium: + @staticmethod + def PdfDocument(file_data): + return FakeDocument() + + monkeypatch.setattr(markitdown, "MarkItDown", FakeMarkItDown) + monkeypatch.setitem(sys.modules, "pypdfium2", FakePdfium) + + parser = MarkitdownParser( + ocr_enabled=True, + ocr_api_key="key", + ocr_base_url="https://example.test/v1", + ocr_model="vision", + ) + assert await parser.convert(b"pdf", "scan.pdf") == "Recovered page" + + +@pytest.mark.asyncio +async def test_markitdown_scanned_pdf_ocr_runs_once_on_failure(monkeypatch): + """A failed PDF OCR fallback is not retried by the parser's error wrapper.""" + import markitdown + + from hindsight_api.engine.parsers import MarkitdownParser + + class FakeResult: + text_content = "" + + pdf_calls = 0 + + class FakeMarkItDown: + def __init__(self, **kwargs): + pass + + def convert(self, path, **kwargs): + if path.endswith(".pdf"): + nonlocal pdf_calls + pdf_calls += 1 + return FakeResult() + raise RuntimeError("OCR unavailable") + + class FakePage: + def render(self, **kwargs): + return type( + "Bitmap", (), {"to_pil": lambda self: type("Image", (), {"save": lambda self, path, format: None})()} + )() + + class FakeDocument: + def __iter__(self): + return iter([FakePage()]) + + def close(self): + pass + + class FakePdfium: + @staticmethod + def PdfDocument(file_data): + return FakeDocument() + + monkeypatch.setattr(markitdown, "MarkItDown", FakeMarkItDown) + monkeypatch.setitem(sys.modules, "pypdfium2", FakePdfium) + + parser = MarkitdownParser( + ocr_enabled=True, + ocr_api_key="key", + ocr_base_url="https://example.test/v1", + ocr_model="vision", + ) + with pytest.raises(RuntimeError, match="Failed to parse"): + await parser.convert(b"pdf", "scan.pdf") + assert pdf_calls == 1 + + +@pytest.mark.asyncio +async def test_scanned_pdf_ocr_failure_advances_parser_chain(): + """A failed MarkItDown PDF OCR attempt advances to the next configured parser.""" + from hindsight_api.engine.parsers import FileParserRegistry + from hindsight_api.engine.parsers.base import FileParser + + class EmptyMarkitdownParser(FileParser): + async def convert(self, file_data: bytes, filename: str) -> str: + raise RuntimeError("No OCR content extracted from scanned PDF") + + def name(self) -> str: + return "markitdown" + + class OcrFallbackParser(FileParser): + async def convert(self, file_data: bytes, filename: str) -> str: + return "Fallback OCR text" + + def name(self) -> str: + return "llama_parse" + + registry = FileParserRegistry() + registry.register(EmptyMarkitdownParser()) + registry.register(OcrFallbackParser()) + + result = await registry.convert_with_fallback( + parsers=["markitdown", "llama_parse"], + file_data=b"pdf", + filename="scan.pdf", + ) + assert result.content == "Fallback OCR text" + assert result.parser_name == "llama_parse" + + def test_markitdown_converter_can_enable_ocr(monkeypatch): """When enabled, Markitdown receives an OpenAI-compatible client, model, and OCR prompt.""" import markitdown diff --git a/hindsight-docs/docs/developer/configuration.md b/hindsight-docs/docs/developer/configuration.md index f3299d197..cf1df100b 100644 --- a/hindsight-docs/docs/developer/configuration.md +++ b/hindsight-docs/docs/developer/configuration.md @@ -1519,7 +1519,7 @@ Local file-to-markdown conversion using [Microsoft's markitdown](https://github. For image workloads, MarkItDown can optionally use an OpenAI-compatible OCR/vision endpoint. This is disabled by default. Without it, image uploads fail with an actionable configuration error instead of low-level parser output. When enabled, configure the MarkItDown OCR API key, base URL, and model explicitly; they do not inherit from `HINDSIGHT_API_LLM_*` because MarkItDown uses the OpenAI SDK directly. The selected endpoint must implement OpenAI Chat Completions and the selected model must support image input. -This OCR path uses MarkItDown's image converter hook. It applies to image inputs such as JPG and PNG (and image handling inside converters that consume MarkItDown's `llm_client`), but it does not rasterize scanned PDF pages into images. Scanned PDFs with no text layer may still extract poorly through the default PDF converter. For scanned PDFs or complex document layouts, use an OCR-capable document parser such as `iris` or `llama_parse`, or configure a parser fallback chain like `llama_parse,markitdown`. +This OCR path uses MarkItDown's image converter hook. It applies directly to image inputs such as JPG and PNG. When normal PDF extraction returns no text, Hindsight also renders PDF pages at 200 DPI and sends each page through the same OCR hook. If PDF OCR is unavailable or produces no content, the configured parser fallback chain continues to the next parser. For more complex document layouts, use an OCR-capable document parser such as `iris` or `llama_parse`, or configure a chain such as `markitdown,llama_parse`. | Variable | Description | Default | |----------|-------------|---------| diff --git a/skills/hindsight-docs/references/developer/configuration.md b/skills/hindsight-docs/references/developer/configuration.md index 1c8a7bff2..869f720e9 100644 --- a/skills/hindsight-docs/references/developer/configuration.md +++ b/skills/hindsight-docs/references/developer/configuration.md @@ -1519,7 +1519,7 @@ Local file-to-markdown conversion using [Microsoft's markitdown](https://github. For image workloads, MarkItDown can optionally use an OpenAI-compatible OCR/vision endpoint. This is disabled by default. Without it, image uploads fail with an actionable configuration error instead of low-level parser output. When enabled, configure the MarkItDown OCR API key, base URL, and model explicitly; they do not inherit from `HINDSIGHT_API_LLM_*` because MarkItDown uses the OpenAI SDK directly. The selected endpoint must implement OpenAI Chat Completions and the selected model must support image input. -This OCR path uses MarkItDown's image converter hook. It applies to image inputs such as JPG and PNG (and image handling inside converters that consume MarkItDown's `llm_client`), but it does not rasterize scanned PDF pages into images. Scanned PDFs with no text layer may still extract poorly through the default PDF converter. For scanned PDFs or complex document layouts, use an OCR-capable document parser such as `iris` or `llama_parse`, or configure a parser fallback chain like `llama_parse,markitdown`. +This OCR path uses MarkItDown's image converter hook. It applies directly to image inputs such as JPG and PNG. When normal PDF extraction returns no text, Hindsight also renders PDF pages at 200 DPI and sends each page through the same OCR hook. If PDF OCR is unavailable or produces no content, the configured parser fallback chain continues to the next parser. For more complex document layouts, use an OCR-capable document parser such as `iris` or `llama_parse`, or configure a chain such as `markitdown,llama_parse`. | Variable | Description | Default | |----------|-------------|---------| diff --git a/uv.lock b/uv.lock index bb1b4d93b..b9177bb23 100644 --- a/uv.lock +++ b/uv.lock @@ -1715,6 +1715,7 @@ dependencies = [ { name = "pydantic" }, { name = "pygments" }, { name = "pyjwt", extra = ["crypto"] }, + { name = "pypdfium2" }, { name = "python-dateutil" }, { name = "python-dotenv" }, { name = "python-multipart" }, @@ -1857,6 +1858,7 @@ requires-dist = [ { name = "pygments", specifier = ">=2.20.0" }, { name = "pyjwt", specifier = ">=2.12.0" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.8.0" }, + { name = "pypdfium2", specifier = ">=5.4.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.21.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0" }, @@ -2493,18 +2495,18 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform == 'darwin'", ] dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, + { name = "aiohttp", marker = "sys_platform == 'darwin'" }, + { name = "click", marker = "sys_platform == 'darwin'" }, + { name = "fastuuid", marker = "sys_platform == 'darwin'" }, + { name = "httpx", marker = "sys_platform == 'darwin'" }, + { name = "importlib-metadata", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin'" }, + { name = "openai", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin'" }, + { name = "tiktoken", marker = "sys_platform == 'darwin'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/2a/a544cfdf8957accd303a99923f55c9fadc3a919d4ed6feca45e73001cade/litellm-1.91.4.tar.gz", hash = "sha256:b810d4c9e63e46908f4eeb14dde76b9811ca7101bec6290eb2522abd273c076d", size = 14875903, upload-time = "2026-07-19T02:45:39.187Z" } wheels = [ @@ -2526,18 +2528,18 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, + { name = "aiohttp", marker = "sys_platform != 'darwin'" }, + { name = "click", marker = "sys_platform != 'darwin'" }, + { name = "fastuuid", marker = "sys_platform != 'darwin'" }, + { name = "httpx", marker = "sys_platform != 'darwin'" }, + { name = "importlib-metadata", marker = "sys_platform != 'darwin'" }, + { name = "jinja2", marker = "sys_platform != 'darwin'" }, + { name = "jsonschema", marker = "sys_platform != 'darwin'" }, + { name = "openai", marker = "sys_platform != 'darwin'" }, + { name = "pydantic", marker = "sys_platform != 'darwin'" }, + { name = "python-dotenv", marker = "sys_platform != 'darwin'" }, + { name = "tiktoken", marker = "sys_platform != 'darwin'" }, + { name = "tokenizers", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/e1/4f05ca4cbb4efb739c9e66a182ecd5c816bc05bf3665ec8e0fb4ab408379/litellm-1.93.0.tar.gz", hash = "sha256:140bf215e264c71601bca9c06d2436c5451bb59e1e195ea23fc2d3d87b6929ec", size = 15948866, upload-time = "2026-07-19T03:01:24.389Z" } wheels = [ @@ -2924,13 +2926,13 @@ name = "mlx-lm" version = "0.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2" }, + { name = "jinja2", marker = "sys_platform != 'win32'" }, { name = "mlx", marker = "sys_platform == 'darwin'" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "sentencepiece" }, - { name = "transformers" }, + { name = "numpy", marker = "sys_platform != 'win32'" }, + { name = "protobuf", marker = "sys_platform != 'win32'" }, + { name = "pyyaml", marker = "sys_platform != 'win32'" }, + { name = "sentencepiece", marker = "sys_platform != 'win32'" }, + { name = "transformers", marker = "sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/f9/3f5597c62bd5733ebb3c9f96c33f2065db16353d743b8548bb05a01b7dd3/mlx_lm-0.31.1.tar.gz", hash = "sha256:1b2362ea301427004e5dda43b9241d751d4cb80eba641f6b85b29fc493affac5", size = 285473, upload-time = "2026-03-11T02:02:57.466Z" } wheels = [ @@ -5099,8 +5101,8 @@ name = "secretstorage" version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/8a/ed6747b1cc723c81f526d4c12c1b1d43d07190e1e8258dbf934392fc850e/secretstorage-3.4.1.tar.gz", hash = "sha256:a799acf5be9fb93db609ebaa4ab6e8f1f3ed5ae640e0fa732bfea59e9c3b50e8", size = 19871, upload-time = "2025-11-11T11:30:23.798Z" } wheels = [ @@ -5485,13 +5487,13 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform == 'darwin'", ] dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "typing-extensions" }, + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", marker = "sys_platform == 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0826ac8e409551e12b2360ac18b4161a838cbd111933e694752f351191331d09", upload-time = "2026-02-06T16:27:14Z" }, @@ -5523,13 +5525,13 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "typing-extensions" }, + { name = "filelock", marker = "sys_platform != 'darwin'" }, + { name = "fsspec", marker = "sys_platform != 'darwin'" }, + { name = "jinja2", marker = "sys_platform != 'darwin'" }, + { name = "networkx", marker = "sys_platform != 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform != 'darwin'" }, + { name = "sympy", marker = "sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_aarch64.whl", hash = "sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa", upload-time = "2026-01-23T15:10:11Z" },