Text Ranking
Transformers
Safetensors
sentence-transformers
English
qwen3_vl
image-text-to-text
multimodal rerank
text rerank
conversational
custom_code
Instructions to use nlpai-lab/RenderRank-2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use nlpai-lab/RenderRank-2B with Transformers:
# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("nlpai-lab/RenderRank-2B", trust_remote_code=True) model = AutoModelForMultimodalLM.from_pretrained("nlpai-lab/RenderRank-2B", trust_remote_code=True, device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - sentence-transformers
How to use nlpai-lab/RenderRank-2B with sentence-transformers:
from sentence_transformers import CrossEncoder model = CrossEncoder("nlpai-lab/RenderRank-2B", trust_remote_code=True) query = "Which planet is known as the Red Planet?" passages = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", "Saturn, famous for its rings, is sometimes mistaken for the Red Planet." ] scores = model.predict([(query, passage) for passage in passages]) print(scores) - Notebooks
- Google Colab
- Kaggle
Download rendering.py from nlpai-lab/RenderRank-2B: direct link, hf CLI and curl.
- Browser
- Download file 9.38 kB
-
https://huggingface.co/nlpai-lab/RenderRank-2B/resolve/main/rendering.py
- Command line
-
hf download hf://nlpai-lab/RenderRank-2B/rendering.py
-
curl -L -o rendering.py https://huggingface.co/nlpai-lab/RenderRank-2B/resolve/main/rendering.py
9.38 kB
| """Existing evaluation renderer, packaged without evaluation/cache dependencies.""" | |
| from __future__ import annotations | |
| import math | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any, Sequence | |
| from PIL import Image, ImageDraw, ImageFont | |
| class DocumentImageConfig: | |
| """Exact document rendering parameters used by the vision evaluation.""" | |
| font_path: Path | |
| font_index: int = 0 | |
| font_family: str = "Roboto" | |
| font_style: str = "Regular" | |
| font_size_px: int = 16 | |
| dpi: int = 96 | |
| line_height_px: int | None = None | |
| width_px: int = 896 | |
| min_height_px: int = 32 | |
| height_step_px: int = 32 | |
| max_height_px: int = 896 | |
| max_lines_per_page: int | None = None | |
| def __post_init__(self) -> None: | |
| object.__setattr__(self, "font_path", Path(self.font_path)) | |
| line_height_px = self.font_size_px if self.line_height_px is None else self.line_height_px | |
| object.__setattr__(self, "line_height_px", line_height_px) | |
| max_lines_per_page = ( | |
| min(56, self.max_height_px // line_height_px) | |
| if self.max_lines_per_page is None | |
| else self.max_lines_per_page | |
| ) | |
| object.__setattr__(self, "max_lines_per_page", max_lines_per_page) | |
| numeric_values = { | |
| "font_index": self.font_index, | |
| "font_size_px": self.font_size_px, | |
| "dpi": self.dpi, | |
| "line_height_px": line_height_px, | |
| "width_px": self.width_px, | |
| "min_height_px": self.min_height_px, | |
| "height_step_px": self.height_step_px, | |
| "max_height_px": self.max_height_px, | |
| "max_lines_per_page": max_lines_per_page, | |
| } | |
| for name, value in numeric_values.items(): | |
| if name == "font_index" and value < 0: | |
| raise ValueError("font_index cannot be negative") | |
| if name != "font_index" and value < 1: | |
| raise ValueError(f"{name} must be positive") | |
| if self.min_height_px > self.max_height_px: | |
| raise ValueError("min_height_px cannot exceed max_height_px") | |
| if self.min_height_px % self.height_step_px or self.max_height_px % self.height_step_px: | |
| raise ValueError("page heights must be exact multiples of height_step_px") | |
| if line_height_px * max_lines_per_page > self.max_height_px: | |
| raise ValueError("max_lines_per_page does not fit in max_height_px") | |
| def to_dict(self) -> dict[str, Any]: | |
| rendering = { | |
| "font": f"{self.font_family} {self.font_style}", | |
| "font_path": str(self.font_path), | |
| "font_size_px": self.font_size_px, | |
| "dpi": self.dpi, | |
| "line_height_px": self.line_height_px, | |
| "width_px": self.width_px, | |
| "min_height_px": self.min_height_px, | |
| "height_step_px": self.height_step_px, | |
| "max_height_px": self.max_height_px, | |
| "max_lines_per_page": self.max_lines_per_page, | |
| "margins_px": 0, | |
| "background": "white", | |
| "foreground": "black", | |
| "whitespace_normalization": "all_runs_to_single_space", | |
| "wrap_measurement": ( | |
| "roboto_pixel_width" | |
| if self.font_family.casefold() == "roboto" | |
| else "font_pixel_width" | |
| ), | |
| "overwide_word_split": "character", | |
| "page_overlap": 0, | |
| } | |
| if self.font_index: | |
| rendering["font_index"] = self.font_index | |
| return rendering | |
| class DocumentImageRenderer: | |
| """Normalize, pixel-wrap, paginate, and render document text.""" | |
| def __init__(self, config: DocumentImageConfig) -> None: | |
| self.config = config | |
| if not config.font_path.is_file(): | |
| raise FileNotFoundError(f"font not found: {config.font_path}") | |
| self.font = ImageFont.truetype( | |
| str(config.font_path), | |
| config.font_size_px, | |
| index=config.font_index, | |
| ) | |
| family, style = self.font.getname() | |
| if ( | |
| family.casefold() != config.font_family.casefold() | |
| or style.casefold() != config.font_style.casefold() | |
| ): | |
| raise ValueError( | |
| f"{config.font_family} {config.font_style} is required, " | |
| f"but {config.font_path} reports {family} {style}" | |
| ) | |
| def normalize_text(text: str) -> str: | |
| return " ".join(str(text).split()) | |
| def text_length(self, text: str) -> float: | |
| return self.font.getlength(text) | |
| def draw_line(self, draw: ImageDraw.ImageDraw, line: str, y: int) -> None: | |
| draw.text( | |
| (0, y), | |
| line, | |
| font=self.font, | |
| fill="black", | |
| anchor="lt", | |
| ) | |
| def rendering_metadata(self) -> dict[str, Any]: | |
| return self.config.to_dict() | |
| def _split_overwide_word(self, word: str, max_width: int) -> list[str]: | |
| chunks: list[str] = [] | |
| offset = 0 | |
| while offset < len(word): | |
| low, high = offset + 1, len(word) | |
| best = offset | |
| while low <= high: | |
| middle = (low + high) // 2 | |
| if self.text_length(word[offset:middle]) <= max_width: | |
| best = middle | |
| low = middle + 1 | |
| else: | |
| high = middle - 1 | |
| if best == offset: | |
| raise ValueError( | |
| f"width {max_width}px is narrower than one rendered character" | |
| ) | |
| chunks.append(word[offset:best]) | |
| offset = best | |
| return chunks | |
| def wrap_text(self, text: str, *, max_width: int | None = None) -> list[str]: | |
| max_width = self.config.width_px if max_width is None else max_width | |
| if max_width < 1: | |
| raise ValueError("max_width must be positive") | |
| normalized = self.normalize_text(text) | |
| if not normalized: | |
| return [] | |
| lines: list[str] = [] | |
| current = "" | |
| for word in normalized.split(" "): | |
| if self.text_length(word) > max_width: | |
| if current: | |
| lines.append(current) | |
| current = "" | |
| chunks = self._split_overwide_word(word, max_width) | |
| lines.extend(chunks[:-1]) | |
| current = chunks[-1] | |
| continue | |
| candidate = word if not current else f"{current} {word}" | |
| if self.text_length(candidate) <= max_width: | |
| current = candidate | |
| else: | |
| lines.append(current) | |
| current = word | |
| if current: | |
| lines.append(current) | |
| return lines | |
| def paginate_lines(self, lines: Sequence[str]) -> list[list[str]]: | |
| if not lines: | |
| return [[]] | |
| page_size = self.config.max_lines_per_page | |
| return [list(lines[start : start + page_size]) for start in range(0, len(lines), page_size)] | |
| def page_height(self, line_count: int) -> int: | |
| if line_count < 0 or line_count > self.config.max_lines_per_page: | |
| raise ValueError( | |
| f"line_count must be between 0 and {self.config.max_lines_per_page}" | |
| ) | |
| content_height = max(1, line_count) * self.config.line_height_px | |
| stepped_height = math.ceil(content_height / self.config.height_step_px) * self.config.height_step_px | |
| return min( | |
| self.config.max_height_px, | |
| max(self.config.min_height_px, stepped_height), | |
| ) | |
| def render_document(self, text: str) -> list[Image.Image]: | |
| lines = self.wrap_text(text) | |
| rendered: list[Image.Image] = [] | |
| for page_lines in self.paginate_lines(lines): | |
| image = Image.new( | |
| "RGB", | |
| (self.config.width_px, self.page_height(len(page_lines))), | |
| "white", | |
| ) | |
| draw = ImageDraw.Draw(image) | |
| for line_index, line in enumerate(page_lines): | |
| self.draw_line(draw, line, line_index * self.config.line_height_px) | |
| rendered.append(image) | |
| return rendered | |
| def prepare_render_inputs(inputs: dict, renderer: DocumentImageRenderer) -> dict: | |
| """Convert render documents to images; leave query/instruction/scoring untouched.""" | |
| if not isinstance(inputs, dict): | |
| raise TypeError("inputs must be a dictionary") | |
| documents = inputs.get("documents", []) | |
| if not isinstance(documents, list): | |
| raise TypeError("documents must be a list") | |
| converted = [] | |
| for document in documents: | |
| if not isinstance(document, dict): | |
| raise TypeError("each document must be a dictionary") | |
| if "render" not in document: | |
| converted.append(document) | |
| continue | |
| if any(key in document for key in ("text", "image", "video")): | |
| raise ValueError("render cannot be combined with text, image, or video") | |
| if not isinstance(document["render"], str): | |
| raise TypeError("render must contain document text as a string") | |
| converted.append({ | |
| **{key: value for key, value in document.items() if key != "render"}, | |
| "image": renderer.render_document(document["render"]), | |
| }) | |
| return {**inputs, "documents": converted} | |