mirror of
https://github.com/datalab-to/chandra.git
synced 2025-11-29 00:23:12 +00:00
Refactor
This commit is contained in:
@@ -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)
|
||||
@@ -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
35
chandra/model/__init__.py
Normal 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
|
||||
@@ -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
18
chandra/model/schema.py
Normal 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
76
chandra/model/util.py
Normal 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
80
chandra/model/vllm.py
Normal 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
180
chandra/output.py
Normal 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
|
||||
|
||||
@@ -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
17
chandra/util.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
62
scripts/start_vllm.py
Normal 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
153
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user