This commit is contained in:
Vik Paruchuri
2025-10-15 16:06:57 -04:00
parent d511d5f9a6
commit a9ffa789c6
15 changed files with 724 additions and 177 deletions

View File

@@ -1,40 +0,0 @@
import math
from typing import Tuple
from PIL import Image
def scale_to_fit(
img: Image.Image,
max_size: Tuple[int, int] = (1024, 1024),
min_size: Tuple[int, int] = (28, 28),
):
resample_method = Image.Resampling.LANCZOS
width, height = img.size
# Check for empty or invalid image
if width == 0 or height == 0:
return img
max_width, max_height = max_size
min_width, min_height = min_size
current_pixels = width * height
max_pixels = max_width * max_height
min_pixels = min_width * min_height
if current_pixels > max_pixels:
scale_factor = (max_pixels / current_pixels) ** 0.5
new_width = math.floor(width * scale_factor)
new_height = math.floor(height * scale_factor)
elif current_pixels < min_pixels:
scale_factor = (min_pixels / current_pixels) ** 0.5
new_width = math.ceil(width * scale_factor)
new_height = math.ceil(height * scale_factor)
else:
return img
return img.resize((new_width, new_height), resample=resample_method)

View File

@@ -1,44 +0,0 @@
import json
from dataclasses import dataclass
from PIL import Image
from PIL.ImageDraw import ImageDraw
from bs4 import BeautifulSoup
@dataclass
class LayoutBlock:
bbox: list[int]
label: str
content: str
def parse_layout(html: str, image: Image.Image):
soup = BeautifulSoup(html, "html.parser")
top_level_divs = soup.find_all("div", recursive=False)
width, height = image.size
width_scaler = width / 1024
height_scaler = height / 1024
layout_blocks = []
for div in top_level_divs:
bbox = div.get("data-bbox")
bbox = json.loads(bbox)
bbox = list(map(int, bbox))
# Normalize bbox
bbox = [
max(0, int(bbox[0] * width_scaler)),
max(0, int(bbox[1] * height_scaler)),
min(int(bbox[2] * width_scaler), width),
min(int(bbox[3] * height_scaler), height),
]
label = div.get("data-label", "block")
content = str(div.decode_contents())
layout_blocks.append(LayoutBlock(bbox=bbox, label=label, content=content))
return layout_blocks
def draw_layout(image: Image.Image, layout_blocks: list[LayoutBlock]):
draw_image = image.copy()
draw = ImageDraw(draw_image)
for block in layout_blocks:
draw.rectangle(block.bbox, outline="red", width=2)
draw.text((block.bbox[0], block.bbox[1]), block.label, fill="blue")
return draw_image

35
chandra/model/__init__.py Normal file
View File

@@ -0,0 +1,35 @@
from typing import List
from chandra.model.hf import load_model, generate_hf
from chandra.model.schema import BatchInputItem, BatchOutputItem
from chandra.model.vllm import generate_vllm
from chandra.output import parse_markdown, parse_html, parse_chunks
class InferenceManager:
def __init__(self, method: str = "vllm"):
assert method in ("vllm", "hf"), "method must be 'vllm' or 'hf'"
self.method = method
if method == "hf":
self.model = load_model()
else:
self.model = None
def generate(self, batch: List[BatchInputItem], **kwargs) -> List[BatchOutputItem]:
if self.method == "vllm":
results = generate_vllm(batch, **kwargs)
else:
results = generate_hf(batch, self.model, **kwargs)
output = []
for result, input_item in zip(results, batch):
output.append(
BatchOutputItem(
markdown=parse_markdown(result),
html=parse_html(result),
chunks=parse_chunks(result, input_item.image),
raw=result,
)
)
return output

View File

@@ -1,56 +1,15 @@
from dataclasses import dataclass
from typing import List
from PIL import Image
from qwen_vl_utils import process_vision_info
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2_5_VLProcessor
from chandra.image import scale_to_fit
from chandra.model.schema import BatchInputItem
from chandra.model.util import scale_to_fit
from chandra.prompts import PROMPT_MAPPING
from chandra.settings import settings
from qwen_vl_utils import process_vision_info
@dataclass
class BatchItem:
images: List[Image.Image]
prompt: str | None = None
prompt_type: str | None = None
def load():
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
settings.MODEL_CHECKPOINT,
dtype=settings.TORCH_DTYPE,
device_map="auto",
attn_implementation=settings.TORCH_ATTN_IMPLEMENTATION,
).to(settings.TORCH_DEVICE_MODEL)
model = model.eval()
processor = Qwen2_5_VLProcessor.from_pretrained(settings.MODEL_CHECKPOINT)
model.processor = processor
return model
def process_batch_element(item: BatchItem, processor):
prompt = item.prompt
prompt_type = item.prompt_type
images = item.images
if not prompt:
prompt = PROMPT_MAPPING[prompt_type]
content = []
for image in images:
image = scale_to_fit(image) # Guarantee max size
content.append({"type": "image", "image": image})
content.append({"type": "text", "text": prompt})
message = {
"role": "user",
"content": content
}
return message
def generate(batch: List[BatchItem], model):
def generate_hf(batch: List[BatchInputItem], model, **kwargs):
messages = [process_batch_element(item, model.processor) for item in batch]
text = model.processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
@@ -67,7 +26,7 @@ def generate(batch: List[BatchItem], model):
inputs = inputs.to("cuda")
# Inference: Generation of the output
generated_ids = model.generate(**inputs, max_new_tokens=settings.MAX_OUTPUT_TOKENS)
generated_ids = model.generate_hf(**inputs, max_new_tokens=settings.MAX_OUTPUT_TOKENS)
generated_ids_trimmed = [
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
@@ -76,3 +35,34 @@ def generate(batch: List[BatchItem], model):
)
return output_text
def process_batch_element(item: BatchInputItem, processor):
prompt = item.prompt
prompt_type = item.prompt_type
if not prompt:
prompt = PROMPT_MAPPING[prompt_type]
content = []
image = scale_to_fit(item.image) # Guarantee max size
content.append({"type": "image", "image": image})
content.append({"type": "text", "text": prompt})
message = {
"role": "user",
"content": content
}
return message
def load_model():
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
settings.MODEL_CHECKPOINT,
dtype=settings.TORCH_DTYPE,
device_map="auto",
attn_implementation=settings.TORCH_ATTN_IMPLEMENTATION,
).to(settings.TORCH_DEVICE_MODEL)
model = model.eval()
processor = Qwen2_5_VLProcessor.from_pretrained(settings.MODEL_CHECKPOINT)
model.processor = processor
return model

18
chandra/model/schema.py Normal file
View File

@@ -0,0 +1,18 @@
from dataclasses import dataclass
from typing import List
from PIL import Image
@dataclass
class BatchInputItem:
image: Image.Image
prompt: str | None = None
prompt_type: str | None = None
@dataclass
class BatchOutputItem:
markdown: str
html: str
chunks: dict
raw: str

76
chandra/model/util.py Normal file
View File

@@ -0,0 +1,76 @@
import math
from typing import Tuple
from PIL import Image
def scale_to_fit(
img: Image.Image,
max_size: Tuple[int, int] = (3072, 2048),
min_size: Tuple[int, int] = (28, 28),
):
resample_method = Image.Resampling.LANCZOS
width, height = img.size
# Check for empty or invalid image
if width == 0 or height == 0:
return img
max_width, max_height = max_size
min_width, min_height = min_size
current_pixels = width * height
max_pixels = max_width * max_height
min_pixels = min_width * min_height
if current_pixels > max_pixels:
scale_factor = (max_pixels / current_pixels) ** 0.5
new_width = math.floor(width * scale_factor)
new_height = math.floor(height * scale_factor)
elif current_pixels < min_pixels:
scale_factor = (min_pixels / current_pixels) ** 0.5
new_width = math.ceil(width * scale_factor)
new_height = math.ceil(height * scale_factor)
else:
return img
return img.resize((new_width, new_height), resample=resample_method)
def detect_repeat_token(
predicted_tokens: str, max_repeats: int = 4, window_size: int = 50
):
if len(predicted_tokens) < window_size:
return False
# Look at the last window_size tokens
recent_tokens = predicted_tokens[-window_size:].lower()
# Try different sequence lengths (1 to window_size//2)
for seq_len in range(1, window_size // 2 + 1):
# Skip if we can't fit enough repetitions
if seq_len * (max_repeats + 1) > window_size:
continue
# Extract the potential repeating sequence from the end
candidate_seq = recent_tokens[-seq_len:]
# Count how many times this sequence appears consecutively at the end
repeat_count = 0
pos = len(recent_tokens) - seq_len
while pos >= 0:
if recent_tokens[pos : pos + seq_len] == candidate_seq:
repeat_count += 1
pos -= seq_len
else:
break
# If we found more than max_repeats consecutive occurrences
if repeat_count > max_repeats:
return True
return False

80
chandra/model/vllm.py Normal file
View File

@@ -0,0 +1,80 @@
import base64
import io
from concurrent.futures import ThreadPoolExecutor
from typing import List
from PIL import Image
from openai import OpenAI
from chandra.model.schema import BatchInputItem
from chandra.model.util import scale_to_fit, detect_repeat_token
from chandra.prompts import PROMPT_MAPPING
from chandra.settings import settings
def image_to_base64(image: Image.Image) -> str:
"""Convert PIL Image to base64 string."""
buffered = io.BytesIO()
image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode()
def generate_vllm(batch: List[BatchInputItem], max_retries: int = 5):
client = OpenAI(
api_key=settings.VLLM_API_KEY,
base_url=settings.VLLM_API_BASE,
)
model_name = settings.VLLM_MODEL_NAME
if model_name is None:
models = client.models.list()
model_name = models.data[0].id
def _generate(item: BatchInputItem, temperature: float = 0, top_p: float = .1):
prompt = item.prompt
if not prompt:
prompt = PROMPT_MAPPING[item.prompt_type]
content = []
image = scale_to_fit(item.image)
image_b64 = image_to_base64(image)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_b64}"
}
})
content.append({
"type": "text",
"text": prompt
})
completion = client.chat.completions.create(
model=model_name,
messages=[{
"role": "user",
"content": content
}],
max_tokens=settings.MAX_OUTPUT_TOKENS,
temperature=temperature,
top_p=top_p,
)
return completion.choices[0].message.content
def process_item(item, max_retries=3):
result = _generate(item)
retries = 0
while retries < max_retries and (detect_repeat_token(result) or
(len(result) > 50 and detect_repeat_token(result[:-50]))):
print(f"Detected repeat token, retrying generation (attempt {retries + 1})...")
result = _generate(item, temperature=0.2, top_p=0.9)
retries += 1
return result
with ThreadPoolExecutor(max_workers=len(batch)) as executor:
results = list(executor.map(process_item, batch))
return results

180
chandra/output.py Normal file
View File

@@ -0,0 +1,180 @@
import json
import re
from dataclasses import dataclass, asdict
import six
from PIL import Image
from bs4 import BeautifulSoup, NavigableString
from markdownify import MarkdownConverter, re_whitespace
def parse_html(html: str, include_headers_footers: bool = False):
soup = BeautifulSoup(html, "html.parser")
top_level_divs = soup.find_all("div", recursive=False)
out_html = ""
for div in top_level_divs:
label = div.get("data-label")
# Skip headers and footers if not included
if label and not include_headers_footers:
if label in ["Page-Header", "Page-Footer"]:
continue
content = str(div.decode_contents())
out_html += content
return out_html
def escape_dollars(text):
return text.replace("$", r"\$")
def get_formatted_table_text(element):
text = []
for content in element.contents:
if content is None:
continue
if isinstance(content, NavigableString):
stripped = content.strip()
if stripped:
text.append(escape_dollars(stripped))
elif content.name == "br":
text.append("<br>")
elif content.name == "math":
text.append("$" + content.text + "$")
else:
content_str = escape_dollars(str(content))
text.append(content_str)
full_text = ""
for i, t in enumerate(text):
if t == "<br>":
full_text += t
elif i > 0 and text[i - 1] != "<br>":
full_text += " " + t
else:
full_text += t
return full_text
class Markdownify(MarkdownConverter):
def __init__(
self,
inline_math_delimiters,
block_math_delimiters,
**kwargs,
):
super().__init__(**kwargs)
self.inline_math_delimiters = inline_math_delimiters
self.block_math_delimiters = block_math_delimiters
def convert_math(self, el, text, parent_tags):
block = el.has_attr("display") and el["display"] == "block"
if block:
return (
"\n"
+ self.block_math_delimiters[0]
+ text.strip()
+ self.block_math_delimiters[1]
+ "\n"
)
else:
return (
" "
+ self.inline_math_delimiters[0]
+ text.strip()
+ self.inline_math_delimiters[1]
+ " "
)
def convert_table(self, el, text, parent_tags):
return "\n\n" + str(el) + "\n\n"
def convert_a(self, el, text, parent_tags):
text = self.escape(text)
# Escape brackets and parentheses in text
text = re.sub(r"([\[\]()])", r"\\\1", text)
return super().convert_a(el, text, parent_tags)
def escape(self, text, parent_tags=None):
text = super().escape(text, parent_tags)
if self.options["escape_dollars"]:
text = text.replace("$", r"\$")
return text
def process_text(self, el, parent_tags=None):
text = six.text_type(el) or ""
# normalize whitespace if we're not inside a preformatted element
if not el.find_parent("pre"):
text = re_whitespace.sub(" ", text)
# escape special characters if we're not inside a preformatted or code element
if not el.find_parent(["pre", "code", "kbd", "samp", "math"]):
text = self.escape(text)
# remove trailing whitespaces if any of the following condition is true:
# - current text node is the last node in li
# - current text node is followed by an embedded list
if el.parent.name == "li" and (
not el.next_sibling or el.next_sibling.name in ["ul", "ol"]
):
text = text.rstrip()
return text
def parse_markdown(html: str, include_headers_footers: bool = False):
html = parse_html(html, include_headers_footers)
md_cls = Markdownify(
heading_style="ATX",
bullets="-",
escape_misc=False,
escape_underscores=True,
escape_asterisks=True,
escape_dollars=True,
sub_symbol="<sub>",
sup_symbol="<sup>",
inline_math_delimiters=("$", "$"),
block_math_delimiters=("$$", "$$"),
)
markdown = md_cls.convert(html)
return markdown.strip()
@dataclass
class LayoutBlock:
bbox: list[int]
label: str
content: str
def parse_layout(html: str, image: Image.Image):
soup = BeautifulSoup(html, "html.parser")
top_level_divs = soup.find_all("div", recursive=False)
width, height = image.size
width_scaler = width / 1024
height_scaler = height / 1024
layout_blocks = []
for div in top_level_divs:
bbox = div.get("data-bbox")
bbox = json.loads(bbox)
bbox = list(map(int, bbox))
# Normalize bbox
bbox = [
max(0, int(bbox[0] * width_scaler)),
max(0, int(bbox[1] * height_scaler)),
min(int(bbox[2] * width_scaler), width),
min(int(bbox[3] * height_scaler), height),
]
label = div.get("data-label", "block")
content = str(div.decode_contents())
layout_blocks.append(LayoutBlock(bbox=bbox, label=label, content=content))
return layout_blocks
def parse_chunks(html: str, image: Image.Image):
layout = parse_layout(html, image)
chunks = [asdict(block) for block in layout]
return chunks

View File

@@ -9,10 +9,18 @@ class Settings(BaseSettings):
# Paths
BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMAGE_DPI: int = 96
MODEL_CHECKPOINT: str = "datalab-to/chandra-0.2.1"
MODEL_CHECKPOINT: str = "datalab-to/chandra-0.2.8"
TORCH_DEVICE: str | None = None
MAX_OUTPUT_TOKENS: int = 2048
MAX_OUTPUT_TOKENS: int = 8192
# vLLM server settings
USE_VLLM: bool = False
VLLM_API_KEY: str = "EMPTY"
VLLM_API_BASE: str = "http://localhost:8000/v1"
VLLM_MODEL_NAME: str = "chandra"
VLLM_GPUS: str = "0"
# Transformers settings
@computed_field
@property
def TORCH_DEVICE_MODEL(self) -> str:

17
chandra/util.py Normal file
View File

@@ -0,0 +1,17 @@
from PIL import Image
from PIL.ImageDraw import ImageDraw
from chandra.output import LayoutBlock
def draw_layout(image: Image.Image, layout_blocks: list[LayoutBlock]):
draw_image = image.copy()
draw = ImageDraw(draw_image)
for block in layout_blocks:
if block.bbox[2] <= block.bbox[0] or block.bbox[3] <= block.bbox[1]:
continue
draw.rectangle(block.bbox, outline="red", width=2)
draw.text((block.bbox[0], block.bbox[1]), block.label, fill="blue")
return draw_image

View File

@@ -2,19 +2,23 @@ import pypdfium2 as pdfium
import streamlit as st
from PIL import Image
from chandra.layout import parse_layout, draw_layout
from chandra.load import load_pdf_images
from chandra.model import load, BatchItem, generate
from chandra.model import InferenceManager
from chandra.util import draw_layout
from chandra.input import load_pdf_images
from chandra.model.schema import BatchInputItem
from chandra.output import parse_layout
@st.cache_resource()
def load_model():
return load()
def load_model(method: str):
return InferenceManager(method=method)
@st.cache_data()
def get_page_image(pdf_file, page_num):
return load_pdf_images(pdf_file, [page_num])[0]
@st.cache_data()
def page_counter(pdf_file):
doc = pdfium.PdfDocument(pdf_file)
@@ -22,40 +26,45 @@ def page_counter(pdf_file):
doc.close()
return doc_len
# Function for OCR
def ocr_layout(
img: Image.Image,
model=None,
) -> (Image.Image, str):
batch = BatchItem(
images=[img],
batch = BatchInputItem(
image=img,
prompt_type="ocr_layout",
)
html = generate([batch], model=model)[0]
print(f"Generated HTML: {html[:500]}...")
layout = parse_layout(html, img)
result = model.generate([batch])[0]
layout = parse_layout(result.raw, img)
layout_image = draw_layout(img, layout)
return html, layout_image
return result.html, layout_image, result.markdown
def ocr(
img: Image.Image,
) -> str:
batch = BatchItem(
images=[img],
prompt_type="ocr"
)
return generate([batch], model=model)[0]
st.set_page_config(layout="wide")
col1, col2 = st.columns([0.5, 0.5])
model = load_model()
st.markdown("""
# Chandra OCR Demo
This app will let you try chandra, a multilingual OCR toolkit.
This app will let you try chandra, a layout-aware vision language model.
""")
# Get model mode selection
model_mode = st.sidebar.selectbox(
"Model Mode",
["None", "hf", "vllm"],
index=0,
help="Select how to run inference: hf loads the model in memory using huggingface transformers, vllm connects to a running vLLM server."
)
# Only load model if a mode is selected
model = None
if model_mode == "None":
st.warning("Please select a model mode (Local Model or vLLM Server) to run OCR.")
else:
model = load_model(model_mode)
in_file = st.sidebar.file_uploader(
"PDF file or image:", type=["pdf", "png", "jpg", "jpeg", "gif", "webp"]
)
@@ -77,37 +86,35 @@ else:
page_number = None
run_ocr = st.sidebar.button("Run OCR")
prompt_type = st.sidebar.selectbox(
"Prompt type",
["ocr_layout", "ocr"],
index=0,
help="Select the prompt type for OCR.",
)
if pil_image is None:
st.stop()
if run_ocr:
if prompt_type == "ocr_layout":
pred, layout_image = ocr_layout(
pil_image,
)
if model_mode == "None":
st.error("Please select a model mode (hf or vllm) to run OCR.")
else:
pred = ocr(
pred, layout_image, markdown = ocr_layout(
pil_image,
model,
)
layout_image = None
with col1:
html_tab, text_tab, layout_tab = st.tabs(["HTML", "HTML as text", "Layout Image"])
with html_tab:
st.markdown(pred, unsafe_allow_html=True)
with text_tab:
st.text(pred)
with col1:
html_tab, text_tab, layout_tab = st.tabs(["HTML", "HTML as text", "Layout Image"])
with html_tab:
st.markdown(markdown, unsafe_allow_html=True)
st.download_button(
label="Download Markdown",
data=markdown,
file_name=f"{in_file.name.rsplit('.', 1)[0]}_page{page_number if page_number is not None else 0}.md",
mime="text/markdown",
)
with text_tab:
st.text(pred)
if layout_image:
with layout_tab:
st.image(layout_image, caption="Detected Layout", use_container_width=True)
if layout_image:
with layout_tab:
st.image(layout_image, caption="Detected Layout", use_container_width=True)
with col2:
st.image(pil_image, caption="Uploaded Image", use_container_width=True)

View File

@@ -3,10 +3,12 @@ name = "chandra"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.10"
dependencies = [
"beautifulsoup4>=4.14.2",
"filetype>=1.2.0",
"markdownify==1.1.0",
"openai>=2.2.0",
"pillow>=11.3.0",
"pydantic>=2.12.0",
"pydantic-settings>=2.11.0",
@@ -17,3 +19,6 @@ dependencies = [
"torch>=2.8.0",
"transformers>=4.57.0",
]
[tool.setuptools.packages.find]
include = ["chandra*"]

62
scripts/start_vllm.py Normal file
View File

@@ -0,0 +1,62 @@
import os
import subprocess
import sys
from chandra.settings import settings
# backend can be FLASH_ATTN
"""
sudo docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HUGGING_FACE_HUB_TOKEN=$HF_TOKEN" \
--env "VLLM_ATTENTION_BACKEND=TORCH_SDPA" \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model datalab-to/chandra-0.2.4 \
--no-enforce-eager \
--max-num-seqs 32 \
--dtype bfloat16 \
--max-model-len 32768 \
--max_num_batched_tokens 65536 \
--gpu-memory-utilization .9 \
--served-model-name chandra
"""
def main():
cmd = [
"sudo",
"docker",
"run",
"--runtime", "nvidia",
"--gpus", f"device={settings.VLLM_GPUS}",
"-v", f"{os.path.expanduser('~')}/.cache/huggingface:/root/.cache/huggingface",
"--env", f"HUGGING_FACE_HUB_TOKEN={os.getenv('HF_TOKEN')}",
"--env", "VLLM_ATTENTION_BACKEND=TORCH_SDPA",
"-p", "8000:8000",
"--ipc=host",
"vllm/vllm-openai:latest",
"--model", settings.MODEL_CHECKPOINT,
"--no-enforce-eager",
"--max-num-seqs", "32",
"--dtype", "bfloat16",
"--max-model-len", "32768",
"--max_num_batched_tokens", "65536",
"--gpu-memory-utilization", ".9",
"--served-model-name", settings.VLLM_MODEL_NAME,
]
print(f"Starting vLLM server with command: {' '.join(cmd)}")
try:
# Use subprocess.run() which blocks and streams output automatically
subprocess.run(cmd, check=True)
except KeyboardInterrupt:
print("\nShutting down vLLM server...")
sys.exit(0)
except subprocess.CalledProcessError as e:
print(f"\nvLLM server exited with error code {e.returncode}")
sys.exit(e.returncode)
if __name__ == "__main__":
main()

153
uv.lock generated
View File

@@ -26,6 +26,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 },
]
[[package]]
name = "attrs"
version = "25.4.0"
@@ -118,6 +132,8 @@ source = { virtual = "." }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "filetype" },
{ name = "markdownify" },
{ name = "openai" },
{ name = "pillow" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
@@ -133,6 +149,8 @@ dependencies = [
requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "filetype", specifier = ">=1.2.0" },
{ name = "markdownify", specifier = "==1.1.0" },
{ name = "openai", specifier = ">=2.2.0" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "pydantic", specifier = ">=2.12.0" },
{ name = "pydantic-settings", specifier = ">=2.11.0" },
@@ -207,6 +225,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
]
[[package]]
name = "filelock"
version = "3.20.0"
@@ -258,6 +285,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "hf-xet"
version = "1.1.10"
@@ -273,6 +309,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691 },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "huggingface-hub"
version = "0.35.3"
@@ -313,6 +377,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "jiter"
version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510 },
{ url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521 },
{ url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214 },
{ url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280 },
{ url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895 },
{ url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421 },
{ url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932 },
{ url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959 },
{ url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187 },
{ url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461 },
{ url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664 },
{ url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520 },
{ url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021 },
{ url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384 },
{ url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389 },
{ url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519 },
{ url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198 },
{ url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835 },
{ url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655 },
{ url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135 },
{ url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063 },
{ url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139 },
{ url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369 },
{ url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538 },
{ url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737 },
{ url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183 },
{ url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225 },
{ url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414 },
{ url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223 },
{ url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306 },
{ url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565 },
{ url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465 },
{ url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581 },
{ url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102 },
{ url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477 },
{ url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004 },
{ url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855 },
{ url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802 },
{ url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405 },
{ url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102 },
]
[[package]]
name = "jsonschema"
version = "4.25.1"
@@ -340,6 +452,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
]
[[package]]
name = "markdownify"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901 },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
@@ -619,6 +744,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 },
]
[[package]]
name = "openai"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b8/b1/8201e321a7d64a25c6f5a560320272d8be70547add40311fceb916518632/openai-2.2.0.tar.gz", hash = "sha256:bc49d077a8bf0e370eec4d038bc05e232c20855a19df0b58e5b3e5a8da7d33e0", size = 588512 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/92/6aeef1836e66dfec7f7f160a4f06d7041be7f6ccfc47a2f0f5738b332245/openai-2.2.0-py3-none-any.whl", hash = "sha256:d222e63436e33f3134a3d7ce490dc2d2f146fa98036eb65cc225df3ce163916f", size = 998972 },
]
[[package]]
name = "packaging"
version = "25.0"
@@ -1233,6 +1377,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "soupsieve"
version = "2.8"