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:
# pip install -U transformers accelerate # 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=256) 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
File size: 9,380 Bytes
b28b620 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | """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
@dataclass(frozen=True)
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}"
)
@staticmethod
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}
|