Views
No views yet
d9b13ed96ac7aecfc24242d2a09a0c0c5750e188.bfloat16 conversion prepared with mlx-vlm convert for Apple Silicon / MLX runtime use. It is not a 4-bit or 8-bit quantized artifact.string and verbatim-string leaves are evaluated with indel distance (i.e. Levenshtein without replacement), while all others are evaluated with exact-match.
Models were evaluated using vllm, with a temperature of 0.25 and a maximum of 65000 output token (for both thinking and answer), which largely exceeds 22000 which is the number of tokens of the largest ground truth output.| Model name | Average score | Num. failed⁽¹⁾ | Avg. num tokens thinking | Avg. num tokens answer |
|---|---|---|---|---|
| NuExtract3.4_4B-RL | 0.651 ± 0.019 | 27 | 2036 | 1856 |
| gemma-4-E4B-it | 0.538 ± 0.023 | 31 | 3005 | 1287 |
| Qwen3.5-9B | 0.479 ± 0.030 | 170 | 22409 | 1257 |
| Qwen3.5-4B | 0.417 ± 0.031 | 229 | 27177 | 1201 |
| GLM-4.6V-Flash | 0.435 ± 0.026 | 153 | 2989 | 1357 |
| Nemotron-3-Nano-Omni | 0.387 ± 0.028 | 204 | 25827 | 522 |
| Ministral-3-3B | 0.240 ± 0.022 | 344 | 27586 | 362 |
<figure data-type="image" data-id="img_n"><img src="/NM-dev/model_card-A/resolve/main/img_n.png" alt="Detail description of the images"/> 1{
2 "invoice_number": "verbatim-string",
3 "invoice_date": "date",
4 "total_amount": "number",
5 "currency": "currency",
6 "line_items": [
7 {
8 "description": "verbatim-string",
9 "item_type": ["electronics", "clothing", "vehicle", "furniture", "other"],
10 "quantity": "integer",
11 "unit_price": "number",
12 "total": "number"
13 }
14 ]
15}verbatim-string: extract text exactly as it appears in the document;string: generic string field, allowing abstraction or light paraphrasing;integer: whole number;number: integer or decimal number;date-time: ISO-8601 date, time or date-time;data, time, country, currency, email and so on.
For more details, read the complete types specifications and examples["string"];["yes", "no", "maybe"];[["A", "B", "C"]].null or [].pip install numind) offers a method to convert JSON schemas to NuExtract templates:1from typing import Literal
2
3from pydantic import Field, BaseModel
4from numind.nuextract_utils import convert_json_schema_to_nuextract_template
5
6
7class HotelBooking(BaseModel):
8 city: str
9 check_in_date: str = Field(description="date")
10 check_out_date: str = Field(description="date")
11 number_of_guests: int
12 room_type: Literal["single", "double", "suite"]
13
14
15template, dropped_branches = convert_json_schema_to_nuextract_template(
16 HotelBooking.model_json_schema()
17)
18
19# {'check_in_date': 'date', 'check_out_date': 'date', 'city': 'string', 'number_of_guests': 'integer', 'room_type': ['single', 'double', 'suite']}<figure data-type="image" data-id="img_n"><img src="/NM-dev/model_card-A/resolve/main/img_n.png" alt="Detail description of the images"/> 1<figure data-type="image" data-id="img_1">
2 <img src="img_1.png" alt="Logo of Mobilier 2000 with contact information: Tél.: (418) 275-4232, 1654, boul. Marcotte, Roberval (Qc) G8H 2P2"/>
3</figure>
4
5# COMMANDE
6**NUMÉRO 72259**
7
81
9
10**Vendu à**
11TREMBLAY ERIC
12ERIC TREMBLAY
13348 BOUL. DE L'ANSE
14ROBERVAL
15G8H 1Y9
16
17**Livré à**
18TREMBLAY ERIC
19ERIC TREMBLAY
20348 BOUL. DE L'ANSE
21ROBERVAL
22G8H 1Y9
23
24<table>
25 <thead>
26 <tr>
27 <th># CLIENT</th>
28 <th>EXPÉDITEUR</th>
29 <th>TERME DE CRÉDIT</th>
30 <th>DATE</th>
31 </tr>
32 </thead>
33 <tbody>
34 <tr>
35 <td>2753133</td>
36 <td>Notre camion</td>
37 <td>à la livraison</td>
38 <td>22/06/2023</td>
39 </tr>
40 </tbody>
41</table>
42
43<table>
44 <thead>
45 <tr>
46 <th>NOM DU VENDEUR</th>
47 <th>VOTRE ÉCONOMIE !</th>
48 <th># COMMANDE</th>
49 </tr>
50 </thead>
51 <tbody>
52 <tr>
53 <td>Éric</td>
54 <td>0.00</td>
55 <td></td>
56 </tr>
57 </tbody>
58</table>1enable_thinking = False
2temperature = 0.21enable_thinking = True
2temperature = 0.61vllm serve numind/NuExtract3 \
2 --trust-remote-code \
3 --limit-mm-per-prompt '{"image": 99, "video": 0}' \
4 --chat-template-content-format openai \
5 --generation-config vllm \
6 --max-model-len 131072 \
7 --speculative-config '{"method": "qwen3_next_mtp", "num_speculative_tokens": 2}'--speculative-config '{"method": "qwen3_next_mtp", "num_speculative_tokens": 2}'num_speculative_tokens for your hardware and workload, or remove --speculative-config if your vLLM version or environment does not support this speculative decoding method.1vllm serve numind/NuExtract-3 \
2 --trust-remote-code \
3 --limit-mm-per-prompt '{"image": 6, "video": 0}' \
4 --chat-template-content-format openai \
5 --generation-config vllm \
6 --max-model-len 16384 \
7 --speculative-config '{"method": "qwen3_next_mtp", "num_speculative_tokens": 2}'1import json
2from openai import OpenAI
3
4client = OpenAI(
5 api_key="EMPTY",
6 base_url="http://localhost:8000/v1",
7)
8
9template = {
10 "store": "verbatim-string",
11 "date": "date-time",
12 "total": "number",
13 "currency": ["USD", "EUR", "GBP", "JPY", "Other"],
14 "items": [
15 {
16 "name": "verbatim-string",
17 "price": "number"
18 }
19 ]
20}
21
22response = client.chat.completions.create(
23 model="numind/NuExtract3",
24 temperature=0.2,
25 messages=[
26 {
27 "role": "user",
28 "content": [
29 {
30 "type": "text",
31 "text": "Yesterday I bought apples and coffee at Trader Joe's for a total of $12.40."
32 }
33 ],
34 }
35 ],
36 extra_body={
37 "chat_template_kwargs": {
38 "template": json.dumps(template, indent=4),
39 "enable_thinking": False
40 }
41 }
42)
43
44print(response.choices[0].message.content)1{
2 "store": "Trader Joe's",
3 "date": null,
4 "total": 12.40,
5 "currency": "USD",
6 "items": [
7 {
8 "name": "apples",
9 "price": null
10 },
11 {
12 "name": "coffee",
13 "price": null
14 }
15 ]
16}1import json
2import base64
3from openai import OpenAI
4
5client = OpenAI(
6 api_key="EMPTY",
7 base_url="http://localhost:8000/v1",
8)
9
10def encode_image(image_path):
11 with open(image_path, "rb") as image_file:
12 return base64.b64encode(image_file.read()).decode("utf-8")
13
14image_base64 = encode_image("receipt.png")
15data_url = f"data:image/png;base64,{image_base64}"
16
17template = {
18 "store": "verbatim-string",
19 "date": "date-time",
20 "total": "number",
21 "payment_method": "verbatim-string"
22}
23
24response = client.chat.completions.create(
25 model="numind/NuExtract3",
26 temperature=0.2,
27 messages=[
28 {
29 "role": "user",
30 "content": [
31 {
32 "type": "image_url",
33 "image_url": {"url": data_url}
34 }
35 ],
36 }
37 ],
38 extra_body={
39 "chat_template_kwargs": {
40 "template": json.dumps(template, indent=4),
41 "enable_thinking": False
42 }
43 }
44)
45
46print(response.choices[0].message.content)1{
2 "store": "Trader Joe's",
3 "date": "2025-04-12",
4 "total": 42.85,
5 "payment_method": "Visa"
6}1import base64
2import json
3
4import fitz # pip install pymupdf
5from openai import OpenAI
6
7client = OpenAI(
8 api_key="EMPTY",
9 base_url="http://localhost:8000/v1",
10)
11
12def pdf_to_png_data_urls(pdf_path, dpi=170):
13 data_urls = []
14
15 with fitz.open(pdf_path) as doc:
16 for page in doc:
17 pix = page.get_pixmap(dpi=dpi, alpha=False)
18 png_bytes = pix.tobytes("png")
19 png_base64 = base64.b64encode(png_bytes).decode("utf-8")
20 data_urls.append(f"data:image/png;base64,{png_base64}")
21
22 return data_urls
23
24data_urls = pdf_to_png_data_urls("invoice.pdf", dpi=170)
25
26template = {
27 "invoice_number": "verbatim-string",
28 "invoice_date": "date",
29 "total": "number",
30 "currency": "currency",
31 "line_items": [
32 {
33 "description": "verbatim-string",
34 "quantity": "number",
35 "unit_price": "number",
36 "total": "number"
37 }
38 ]
39}
40
41response = client.chat.completions.create(
42 model="numind/NuExtract3",
43 temperature=0.2,
44 messages=[
45 {
46 "role": "user",
47 "content": [
48 {
49 "type": "image_url",
50 "image_url": {"url": data_url}
51 }
52 for data_url in data_urls
53 ],
54 }
55 ],
56 extra_body={
57 "chat_template_kwargs": {
58 "template": json.dumps(template, indent=4),
59 "enable_thinking": False
60 }
61 }
62)
63
64print(response.choices[0].message.content)mode="markdown" or mode="content" without a template.1import base64
2from openai import OpenAI
3
4client = OpenAI(
5 api_key="EMPTY",
6 base_url="http://localhost:8000/v1",
7)
8
9def encode_image(image_path):
10 with open(image_path, "rb") as image_file:
11 return base64.b64encode(image_file.read()).decode("utf-8")
12
13image_base64 = encode_image("document.png")
14data_url = f"data:image/png;base64,{image_base64}"
15
16response = client.chat.completions.create(
17 model="numind/NuExtract3",
18 temperature=0,
19 messages=[
20 {
21 "role": "user",
22 "content": [
23 {
24 "type": "image_url",
25 "image_url": {"url": data_url}
26 }
27 ],
28 }
29 ],
30 extra_body={
31 "chat_template_kwargs": {
32 "mode": "markdown",
33 "enable_thinking": False
34 }
35 }
36)
37
38print(response.choices[0].message.content)1response = client.chat.completions.create(
2 model="numind/NuExtract3",
3 temperature=0.7,
4 messages=[
5 {
6 "role": "user",
7 "content": [
8 {
9 "type": "image_url",
10 "image_url": {"url": data_url}
11 }
12 ],
13 }
14 ],
15 extra_body={
16 "chat_template_kwargs": {
17 "mode": "markdown",
18 "enable_thinking": True
19 }
20 }
21)
22
23result = response.choices[0].message.content
24
25if "</think>" in result:
26 reasoning = result.split("<think>")[1].split("</think>")[0]
27 answer = result.split("</think>")[-1].strip()
28else:
29 reasoning = None
30 answer = result
31
32print(answer)developer messages, for which all items of the contents except the last one are the input, and the last one is the expected output.1import json
2from openai import OpenAI
3
4client = OpenAI(
5 api_key="EMPTY",
6 base_url="http://localhost:8000/v1",
7)
8
9template = {
10 "names": ["string"]
11}
12
13response = client.chat.completions.create(
14 model="numind/NuExtract3",
15 temperature=0,
16 messages=[
17 {
18 "role": "developer",
19 "content": [
20 {
21 "type": "text",
22 "text": "Stephen is the manager at Susan's store.",
23 },
24 {
25 "type": "text",
26 "text": "{\"names\": [\"-STEPHEN-\", \"-SUSAN-\"]}",
27 }
28 ],
29 },
30 {
31 "role": "user",
32 "content": [
33 {
34 "type": "text",
35 "text": "John went to the restaurant with Mary. James went to the cinema."
36 }
37 ],
38 }
39 ],
40 extra_body={
41 "chat_template_kwargs": {
42 "template": json.dumps(template, indent=4),
43 "enable_thinking": False
44 }
45 }
46)
47
48print(response.choices[0].message.content)1{
2 "names": ["-JOHN-", "-MARY-", "-JAMES-"]
3}1from openai import OpenAI
2
3client = OpenAI(
4 api_key="EMPTY",
5 base_url="http://localhost:8000/v1",
6)
7
8response = client.chat.completions.create(
9 model="numind/NuExtract3",
10 temperature=0,
11 messages=[
12 {
13 "role": "user",
14 "content": [
15 {
16 "type": "text",
17 "text": "I want to extract the key details from a rental contract."
18 }
19 ],
20 }
21 ],
22 extra_body={
23 "chat_template_kwargs": {
24 "mode": "template-generation"
25 }
26 }
27)
28
29print(response.choices[0].message.content)1{
2 "contract_title": "verbatim-string",
3 "landlord": "verbatim-string",
4 "tenant": "verbatim-string",
5 "property_address": "verbatim-string",
6 "start_date": "date-time",
7 "end_date": "date-time",
8 "monthly_rent": "number",
9 "currency": "verbatim-string",
10 "deposit": "number",
11 "signatories": ["verbatim-string"]
12}jq to build valid JSON request bodies without manually escaping the image data or template string.1API_KEY="EMPTY"
2IMAGE_BASE64_FILE=$(mktemp)
3REQUEST_BODY_FILE=$(mktemp)
4
5base64 < receipt.png | tr -d '\n' > "$IMAGE_BASE64_FILE"
6
7TEMPLATE=$(cat <<'JSON'
8{
9 "store": "verbatim-string",
10 "date": "date-time",
11 "total": "number",
12 "payment_method": "verbatim-string"
13}
14JSON
15)
16
17jq -n \
18 --rawfile image_base64 "$IMAGE_BASE64_FILE" \
19 --arg template "$TEMPLATE" \
20 '{
21 model: "numind/NuExtract3",
22 temperature: 0,
23 messages: [
24 {
25 role: "user",
26 content: [
27 {
28 type: "image_url",
29 image_url: {url: ("data:image/png;base64," + $image_base64)}
30 }
31 ]
32 }
33 ],
34 chat_template_kwargs: {
35 template: $template,
36 enable_thinking: false
37 }
38 }' > "$REQUEST_BODY_FILE"
39
40curl http://localhost:8000/v1/chat/completions \
41 -H "Content-Type: application/json" \
42 -H "Authorization: Bearer $API_KEY" \
43 --data-binary "@$REQUEST_BODY_FILE"
44
45rm "$IMAGE_BASE64_FILE" "$REQUEST_BODY_FILE"1API_KEY="EMPTY"
2IMAGE_BASE64_FILE=$(mktemp)
3REQUEST_BODY_FILE=$(mktemp)
4
5base64 < document.png | tr -d '\n' > "$IMAGE_BASE64_FILE"
6
7jq -n \
8 --rawfile image_base64 "$IMAGE_BASE64_FILE" \
9 '{
10 model: "numind/NuExtract3",
11 temperature: 0,
12 messages: [
13 {
14 role: "user",
15 content: [
16 {
17 type: "image_url",
18 image_url: {url: ("data:image/png;base64," + $image_base64)}
19 }
20 ]
21 }
22 ],
23 chat_template_kwargs: {
24 mode: "content",
25 enable_thinking: false
26 }
27 }' > "$REQUEST_BODY_FILE"
28
29curl http://localhost:8000/v1/chat/completions \
30 -H "Content-Type: application/json" \
31 -H "Authorization: Bearer $API_KEY" \
32 --data-binary "@$REQUEST_BODY_FILE"
33
34rm "$IMAGE_BASE64_FILE" "$REQUEST_BODY_FILE"1import json
2
3import torch
4from PIL import Image
5from transformers import AutoModelForImageTextToText, AutoProcessor
6
7model_id = "numind/NuExtract3"
8
9processor = AutoProcessor.from_pretrained(
10 model_id,
11 trust_remote_code=True,
12)
13model = AutoModelForImageTextToText.from_pretrained(
14 model_id,
15 dtype=torch.bfloat16,
16 device_map="auto",
17 trust_remote_code=True,
18).eval()
19
20def run_nuextract(messages, **chat_template_kwargs):
21 inputs = processor.apply_chat_template(
22 messages,
23 add_generation_prompt=True,
24 tokenize=True,
25 return_dict=True,
26 return_tensors="pt",
27 **chat_template_kwargs,
28 ).to(model.device)
29
30 with torch.inference_mode():
31 generated_ids = model.generate(
32 **inputs,
33 max_new_tokens=4096,
34 do_sample=False,
35 )
36
37 generated_ids = generated_ids[:, inputs.input_ids.shape[1]:]
38 return processor.batch_decode(
39 generated_ids,
40 skip_special_tokens=True,
41 clean_up_tokenization_spaces=False,
42 )[0].strip()
43
44# Single image structured extraction
45receipt_image = Image.open("receipt.png").convert("RGB")
46receipt_messages = [
47 {
48 "role": "user",
49 "content": [
50 {
51 "type": "image",
52 "image": receipt_image,
53 }
54 ],
55 }
56]
57
58template = {
59 "store": "verbatim-string",
60 "date": "date-time",
61 "total": "number",
62 "payment_method": "verbatim-string"
63}
64
65structured_output = run_nuextract(
66 receipt_messages,
67 template=json.dumps(template, indent=4),
68 enable_thinking=False,
69)
70print(structured_output)
71
72# Single image content extraction
73document_image = Image.open("document.png").convert("RGB")
74document_messages = [
75 {
76 "role": "user",
77 "content": [
78 {
79 "type": "image",
80 "image": document_image,
81 }
82 ],
83 }
84]
85
86content_output = run_nuextract(
87 document_messages,
88 mode="content",
89 enable_thinking=False,
90)
91print(content_output)1@misc{nuextract3,
2 title = {NuExtract3},
3 author = {NuMind},
4 year = {2026},
5 url = {https://nuextract.ai/}
6}