Qwen3-VL-8B β€” Constat Amiable Extractor (LoRA adapter)

A LoRA fine-tune of Qwen3-VL-8B-Instruct that reads a photo or scan of a French constat amiable d'accident automobile (the European Accident Statement) and returns a single JSON object with a fixed 118-field schema β€” handwritten text, printed text, and checkbox states.

This model was trained and evaluated entirely on synthetic data. No real accident reports were used at any stage. The evaluation below measures performance on the synthetic distribution the model was trained on β€” it is not a measurement of real-world accuracy. Please read Limitations & honest caveats before using or citing these numbers.


TL;DR

Base model Qwen/Qwen3-VL-8B-Instruct (Apache-2.0)
Method 4-bit QLoRA via Unsloth β€” r=16, Ξ±=32
Trainable params 51,346,944 / 8,818,470,640 (0.58 %)
Training data 14,219 procedurally-generated synthetic constats
Training compute 200 optimizer steps (~0.23 epoch), 17 h on one NVIDIA DGX Spark (GB10)
Task image β†’ 118-field JSON (73 text fields + 45 checkboxes)
Released artifact LoRA adapter only (~205 MB) β€” apply on top of the base model
Languages French form; field content is mostly French, with some DE / EN / ES / IT / NL names

The point of this project is to show that a small, cheap, fully-synthetic LoRA fine-tune can specialize an open 8B vision-language model on a narrow document-extraction task. It is a methodology demonstration and a portfolio artifact β€” not a claim of production-grade or real-world state-of-the-art accuracy.


Table of contents

  1. What the model does
  2. Output schema
  3. How to use
  4. Training data
  5. Training procedure
  6. Evaluation
  7. Limitations & honest caveats
  8. License & legal
  9. Benchmark scripts & transparency

What the model does

The constat amiable is the standardized European Accident Statement that two drivers fill in by hand after a road accident. It is a dense, two-column form: identity and address of each party, vehicle and insurance details, a 17-item list of accident circumstances expressed as checkboxes per vehicle, free-text observations, and a sketch ("croquis") of the scene.

Given one image of such a form, the model produces one JSON object:

  • text fields β€” transcribed verbatim (names, addresses, plate numbers, dates, policy numbers, free-text observations…), or null when the field is blank or illegible;
  • checkbox fields β€” true / false for every checkbox, including the 17 circumstance boxes for each vehicle.

The model is driven by a fixed system prompt (a "forensic document examiner" role with anti-hallucination, checkbox and JSON-formatting protocols) plus a short user instruction. Both are shipped in prompts/ and must be used at inference time β€” the model was trained with them.

Intended use

  • Research and demonstration of synthetic-data specialization for VLMs.
  • Extracting fields from constat images of the template family seen in training, in a setting where outputs are human-reviewed.

Out of scope

  • Production insurance processing without a prior validation campaign on real, labelled constats.
  • Any automated legal, financial or claims decision.
  • Documents outside the trained template layouts, other form types, or other languages.
  • Treating the JSON output as ground truth β€” VLM outputs must be verified.

Output schema

The schema has 118 keys in a flat object (no nesting). Breakdown:

  • 73 text fields β†’ string or null
  • 45 checkbox fields β†’ boolean (true / false); croquis_ is true / null

Logical groups (keys suffixed _A / _B for vehicle A / vehicle B):

Group Example keys
Accident date_accident, heure_accident, pays, lieu
Injuries / damage case_blesses_oui/non, case_degats_vehicules_oui/non, case_degats_objets_oui/non
Witnesses temoins
Party identity nom_A, prenom_A, adresse_A, code_postal_A, tel_ou_mail_A
Vehicle marque_vehicule_A, immat_vehicule_A, immat_remorque_A
Insurance nom_societe_assurance_A, no_contrat_A, no_carte_verte_A, date_valable_du/au_A, agence_A
Driver nom_conducteur_A, date_de_naissance_conducteur_A, no_permis_conducteur_A
Damage / notes visible_damage_A, mes_observations_A, signature_A
Circumstances case_1_A … case_17_A, nb_cases_A
Sketch croquis_

The complete, authoritative schema (with the per-field type annotations the model was trained against) is in prompts/new_system_prompt.md.


How to use

The released artifact is a LoRA adapter. Load the base model, apply the adapter, and prompt with the shipped system prompt + instruction.

import json
from PIL import Image
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
from peft import PeftModel

BASE = "Qwen/Qwen3-VL-8B-Instruct"
ADAPTER = "hasanbasbunar/qwen3-vl-8b-constat-amiable-lora"          # this adapter

processor = AutoProcessor.from_pretrained(BASE)
model = Qwen3VLForConditionalGeneration.from_pretrained(BASE, dtype="auto", device_map="auto")
model = PeftModel.from_pretrained(model, ADAPTER)
model.eval()

# Prompts shipped with the model (prompts/ folder)
SYSTEM_PROMPT = open("prompts/new_system_prompt.md", encoding="utf-8").read()
INSTRUCTION   = open("prompts/instruction.txt", encoding="utf-8").read().strip()

image = Image.open("constat.jpg").convert("RGB")
messages = [
    {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
    {"role": "user", "content": [
        {"type": "text",  "text": INSTRUCTION},
        {"type": "image", "image": image},
    ]},
]
inputs = processor.apply_chat_template(
    messages, add_generation_prompt=True, tokenize=True,
    return_dict=True, return_tensors="pt",
).to(model.device)

out = model.generate(**inputs, max_new_tokens=3072, do_sample=False)  # greedy
text = processor.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
data = json.loads(text)

Notes:

  • The model was trained to emit raw JSON (no markdown fence, no preamble).
  • Greedy decoding (do_sample=False) is recommended for a deterministic, reproducible extraction.
  • Training and the benchmark used the Unsloth 4-bit build (unsloth/Qwen3-VL-8B-Instruct-unsloth-bnb-4bit); 4-bit inference reproduces the reported numbers and fits comfortably in <24 GB.

Training data

100 % synthetic, procedurally generated. No real constat was collected, scanned or used. The generator (main.py) builds each sample from:

  • a small set of blank constat amiable form layouts β€” the standardized European Accident Statement β€” used as the backgrounds onto which the synthetic fields are rendered;
  • realistic fake content from Faker, locale-weighted ~85 % fr_FR and ~3 % each of de_DE, en_GB, es_ES, it_IT, nl_NL, plus domain dictionaries for vehicles, insurers, European countries, postal-code formats, place names, etc.;
  • handwriting simulation β€” 304 fonts (filtered for French-glyph coverage) to render the "handwritten" entries;
  • photo-realistic degradation applied with calibrated probabilities, so the model sees the kind of images it would meet in the wild: fold lines, coffee stains, rain drops, shadows & highlights, blur spots, brightness/contrast variation, Gaussian blur, sensor noise, perspective distortion, rotation (Β±6Β°), and JPEG recompression (quality 15–100, weighted toward smartphone-grade 65–90);
  • final images resized to 1536–2240 px and aligned to the 32-pixel grid Qwen3-VL requires.

Each sample ships with a perfect, machine-generated ground-truth JSON β€” the key advantage of synthetic data: exhaustive, noise-free labels at scale, with no privacy exposure (all identities are fake).

Split Samples Use
train 14,219 fine-tuning
validation 749 in-training validation loss (first 128 used)
Total generated 14,968 requested 15,000; ~0.2 % generation failures dropped

The held-out evaluation set (599 samples) was produced by a separate generation run with the same generator β€” see Evaluation.

The honest trade-off: synthetic data is scalable and perfectly labelled, but it only approximates real handwriting, real paper wear and real photo conditions. The gap to real-world constats is real and is not measured here.


Training procedure

QLoRA fine-tuning with Unsloth FastVisionModel + TRL SFTTrainer. LoRA adapters were placed on both the vision and the language layers (attention + MLP).

Hyperparameter Value
Base unsloth/Qwen3-VL-8B-Instruct-unsloth-bnb-4bit (4-bit NF4)
LoRA rank / alpha / dropout 16 / 32 / 0
LoRA targets vision + language, attention + MLP projections
Trainable parameters 51,346,944 (0.58 % of 8.82 B)
Optimizer paged_adamw_8bit, Ξ² = (0.9, 0.99), weight decay 0.01
Learning rate 1e-4, cosine schedule, warmup ratio 0.05
Effective batch size 16 (per-device 1 Γ— grad-accumulation 16)
Max sequence length 16,384
Precision bf16
Loss next-token cross-entropy on the response only (train_on_responses_only)
Steps 200 (β‰ˆ 0.23 epoch over 14,219 samples)
Seed 2405
Hardware 1 Γ— NVIDIA DGX Spark (GB10, 128 GB unified memory)
Wall-clock 17 h 01 min
Peak GPU memory 20.2 GB reserved (16.6 % of 128 GB)

Loss. The first training steps logged a loss around 0.17; mean training loss over the run was 0.0798, and validation loss (128 held-out samples) reached 0.0535 at the end of training.

Note: this is a deliberately light fine-tune β€” 200 steps is roughly a quarter of one epoch. The validation-loss curve was still trending down, so the figures below should be read as a lower bound on what this recipe can reach with more training. Cross-entropy on a highly templated JSON target is also a weak proxy for extraction quality β€” the real measure is the field-accuracy benchmark below.


Evaluation

Methodology

The goal is an honest, transparent, auditable comparison. The benchmark scripts are published in full (see Benchmark scripts & transparency); the synthetic evaluation set itself is not released.

Held-out set. A separate run of the same generator produced 599 fresh synthetic constats (holdout_eval). Because every sample is an independent random draw, these constats do not appear in the training set β€” they are a held-out sample of the training distribution. The benchmark uses the first 64 of them.

Models compared (same 64 images, same prompts):

Model How it is run
Base β€” Qwen3-VL-8B-Instruct zero-shot, greedy decoding, free-form JSON
Fine-tuned β€” this model greedy decoding, free-form JSON (it was trained to emit the schema directly)
Gemini 3.1 Pro β€” gemini-3.1-pro-preview Google AI API, structured output (response_schema), default sampling

Each model is evaluated in its realistic deployment mode. Gemini is given structured output β€” a JSON schema it must conform to β€” because that is how one would actually deploy it for extraction; this is the configuration that gives Gemini its best, most reliable result (it is not a handicap). The fine-tuned model uses free-form generation because it was trained to emit the exact schema and does so reliably without constraint. All three receive the identical system prompt and instruction.

Gemini settings. temperature and thinking_level left at their defaults β€” Google explicitly advises against lowering the temperature for Gemini 3, as it can cause looping/degradation. max_output_tokens = 32768. All 64 requests succeeded on the first call (0 retries, 0 API failures); the retry logic in the script triggers only on transport/HTTP errors, never on output quality.

Scoring. All three models are scored with the same metric definitions, gathered in the module eval_metrics.py. Comparison is case-insensitive and whitespace-normalized (a line break inside a form field is not a content difference). With only 64 samples, the dominant source of uncertainty is sampling noise, not the metric β€” see Limitations.

Metrics

Metric Definition
json_valid the output parses as JSON
field_acc exact-match accuracy over all 118 fields
filled_acc accuracy over only the fields that carry information in the ground truth (ticked checkboxes + non-empty text) β€” the honest "did it read the content" measure
text_acc accuracy over the 73 text fields
checkbox_acc accuracy over the 45 checkbox fields
cer character error rate on text fields (Levenshtein Γ· length); lower is better
exact_full 1.0 only if all 118 fields are correct, else 0.0

Why two accuracy numbers? A real constat leaves many fields blank, and "blank ↔ blank" counts as a correct match. So field_acc is optimistic β€” it is partly rewarded for agreeing on empty fields. filled_acc is the number that matters: accuracy on the content actually present on the form.

Results

Benchmark on n = 64 held-out synthetic constats.

Base vs fine-tuned vs Gemini 3.1 Pro β€” field extraction on the held-out synthetic set (n = 64)

Chart: CER is shown as character accuracy (1 βˆ’ CER) so every bar points the same way (higher = better); exact_full is omitted β€” it is β‰ˆ 0 % for all three models. Full numbers in the table.

Metric Base Qwen3-VL-8B
(zero-shot)
Fine-tuned
(this model)
Gemini 3.1 Pro
(structured output)
json_valid 93.75 % 100.00 % 100.00 %
field_acc (118 fields) 67.52 % 90.10 % 81.81 %
filled_acc (filled fields) 45.50 % 76.62 % 60.27 %
text_acc (73 text fields) 60.45 % 83.91 % 75.05 %
checkbox_acc (45 checkboxes) 78.44 % 99.63 % 92.23 %
cer (text, ↓ better) 0.3275 0.0970 0.1752
exact_full 0.00 % 1.56 % 0.00 %

exact_full is near zero for every model: it scores 1.0 only when all 118 fields are correct at once, so a single slip anywhere drops the sample to 0. It is an intentionally brutal metric kept only for completeness β€” field_acc, filled_acc and cer are the informative ones.

Interpretation

Fine-tuning delivers a large, consistent gain over the zero-shot base β€” on every metric:

Metric Base β†’ Fine-tuned
field_acc 67.5 % β†’ 90.1 % (+22.6 pts)
filled_acc 45.5 % β†’ 76.6 % (+31.1 pts)
text_acc 60.5 % β†’ 83.9 %
checkbox_acc 78.4 % β†’ 99.6 %
cer 0.328 β†’ 0.097 (text error rate down ~70 %)
json_valid 93.8 % β†’ 100 %

This is the result the project set out to demonstrate: a light, fully-synthetic LoRA fine-tune (200 steps) strongly specializes an open 8B vision-language model for this extraction task.

On this benchmark the fine-tuned model matches or exceeds Gemini 3.1 Pro on every metric (tied at 100 % JSON validity, ahead on all the others) β€” but that comparison must be read carefully:

This is not a symmetric comparison. The fine-tuned model was trained on this exact synthetic style and is evaluated on a held-out sample of the same distribution. Gemini 3.1 Pro and the base model see this style for the first time. The fine-tuned model's lead therefore measures how well fine-tuning specialized it to the target distribution β€” it does not show that the fine-tuned model is better than Gemini at reading real constats. Gemini's score is best read as "what a strong general-purpose model achieves zero-shot on this layout"; the fine-tuned score as "what task-specific specialization achieves on its own distribution." A fair real-world comparison would need a labelled set of real constats, which this project does not have (see Limitations).

Honest takeaway. For a narrow, well-defined document-extraction task, fine-tuning a small open model on synthetic data is remarkably effective: a 200-step LoRA was enough to take an 8B model from clearly behind to β€” on this in-distribution benchmark β€” ahead of a frontier model, at a fraction of the size and with no per-call API cost. How much of that advantage survives on real-world documents is unmeasured, and is the main open question this project does not answer.

filled_acc (76.6 %) is the most informative single number: of the fields that actually carry content, roughly three in four are extracted exactly right β€” strong for a 200-step fine-tune, with clear headroom remaining.


Limitations & honest caveats

Read this section before trusting any number above.

  1. In-distribution evaluation β€” the comparison is not symmetric. The fine-tuned model was trained on this exact synthetic style and is then tested on a held-out sample of the same distribution. The base model and Gemini 3.1 Pro see this style cold. A higher score for the fine-tuned model therefore demonstrates successful specialization to the target distribution β€” it does not establish that the fine-tuned model is better than Gemini at reading real constats.

  2. Synthetic β‰  real. Training and evaluation are 100 % synthetic. Real constats have real handwriting, real paper wear, and real photo conditions that the generator only approximates. Real-world accuracy is unmeasured.

  3. No real labelled test set. The project has no corpus of real, annotated constats, so no real-world claim can be made. Anyone deploying this model should first run a validation campaign on real data.

  4. Small evaluation set (n = 64). Point estimates carry a sampling uncertainty on the order of several percentage points. Treat differences of a few points as noise.

  5. Light fine-tune. 200 steps β‰ˆ 0.23 epoch; the model is under-trained relative to the 14k samples available. The reported numbers are a lower bound for this recipe, not its ceiling.

  6. Template coverage. Only the ~5 template layouts seen in training are supported. Other constat layouts, other insurers' forms, or other document types are out of scope.

  7. Hallucination. Like any VLM, the model can produce plausible-looking but wrong field values, especially on illegible handwriting. The system prompt instructs it to return null when unsure; this mitigates but does not eliminate the risk. Outputs must be human-reviewed.

  8. Privacy / GDPR. Training identities are fake (Faker). But real constats contain personal data β€” names, addresses, plate numbers, dates of birth. Any real-world use must comply with GDPR and applicable insurance-data regulations.


License & legal

  • Adapter weights β€” released under Apache-2.0, inherited from the base model Qwen/Qwen3-VL-8B-Instruct (Apache-2.0). The adapter contains only low-rank weight deltas; it does not embed any template image or any training sample.
  • Base model β€” you must obtain Qwen3-VL-8B-Instruct separately, under its own Apache-2.0 license.
  • Datasets. The synthetic training and evaluation sets are not released. This card describes how they were generated; the data itself is kept private.
  • Form. The constat amiable is the standardized European Accident Statement β€” a public administrative form that insurers distribute freely to every driver. The generator uses blank layouts of this standard form purely as rendering backgrounds; the released adapter contains only weight deltas and does not embed or reproduce any form image.

Benchmark scripts & transparency

All three benchmark scripts are published in full so the exact scoring methodology can be read and audited:

File Role
eval_metrics.py the scoring metrics β€” shared module imported by both scripts below
eval_zeroshot_vs_finetuned.py runs the base and fine-tuned models on the hold-out
eval_gemini.py runs Gemini 3.1 Pro with structured output

Both scripts also write a per-sample *_details.jsonl β€” the raw model prediction and the ground truth for every sample β€” used to inspect and verify results.

The synthetic evaluation set itself is not released (see License & legal). The scripts are published for methodological transparency β€” so the scoring logic can be independently checked β€” not as a turnkey reproduction package.

How the runs were launched:

# Base vs fine-tuned (same 64 hold-out images, greedy decoding)
EVAL_N=64 EVAL_ADAPTER_PATH=./lora_model_XXXX \
  python eval_zeroshot_vs_finetuned.py

# Gemini 3.1 Pro (same 64 images, structured output)
EVAL_N=64 GEMINI_API_KEY=... \
  python eval_gemini.py

EVAL_N must be identical across the two scripts for the comparison to be valid.


Acknowledgements

Model card last updated: 2026-05-18.

Downloads last month
8
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for hasanbasbunar/qwen3-vl-8b-constat-amiable-lora

Adapter
(224)
this model