Instructions to use wjbmattingly/comma-qwen-3.5-0.8b-full with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use wjbmattingly/comma-qwen-3.5-0.8b-full with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="wjbmattingly/comma-qwen-3.5-0.8b-full") 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?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("wjbmattingly/comma-qwen-3.5-0.8b-full") model = AutoModelForMultimodalLM.from_pretrained("wjbmattingly/comma-qwen-3.5-0.8b-full", 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]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use wjbmattingly/comma-qwen-3.5-0.8b-full with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "wjbmattingly/comma-qwen-3.5-0.8b-full" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wjbmattingly/comma-qwen-3.5-0.8b-full", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/wjbmattingly/comma-qwen-3.5-0.8b-full
- SGLang
How to use wjbmattingly/comma-qwen-3.5-0.8b-full with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "wjbmattingly/comma-qwen-3.5-0.8b-full" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wjbmattingly/comma-qwen-3.5-0.8b-full", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "wjbmattingly/comma-qwen-3.5-0.8b-full" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wjbmattingly/comma-qwen-3.5-0.8b-full", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use wjbmattingly/comma-qwen-3.5-0.8b-full with Docker Model Runner:
docker model run hf.co/wjbmattingly/comma-qwen-3.5-0.8b-full
comma-qwen-3.5-0.8b-full
Qwen/Qwen3.5-0.8B fine-tuned to produce CATMuS-compliant, line-by-line transcriptions
of medieval Latin manuscript pages.
Given a page image, the model returns one line of text for each physical written line, in reading order. CATMuS is a graphemic standard: the sequence of letters and signs as written, reduced to the modern Latin alphabet, with no editorial intervention. The output is not a translation and not a normalised reading.
This is a full fine-tune โ a standalone checkpoint, no adapter and no peft dependency at inference.
- Training data โ 9,905 pages from
comma-project/deep-jsonl - Evaluation โ 243 held-out pages, each a different hand, none seen in training
- Demo โ comma-qwen-3.5-demo
Results
Greedy decoding, 3072 max new tokens, 2048 visual tokens per page,
batch 4. "Base" is the stock Qwen/Qwen3.5-0.8B scored on the same pages in the same
job โ a base number from another job on another day is not a control.
| metric | base | this model |
|---|---|---|
| CER (NFD) | 1.4038 | 0.1458 |
| CER (raw code points) | 1.3614 | 0.1859 |
| WER (NFD) | 2.7416 | 0.4029 |
| line recall (NFD) | 0.0005 | 0.1672 |
| CER macro (NFD) | 1.6670 | 0.1318 |
| degenerate pages | 0.1646 | 0.0041 |
| truncated pages | 0.4691 | 0.0165 |
CER/WER are micro-averaged (total edits รท total reference characters), so a long
page outweighs a short one; the macro mean is given alongside because a gap
between them says the errors are concentrated. line_recall is the share of
reference lines reproduced exactly and in order โ for a line-by-line CATMuS
target that is the number a palaeographer looks at first.
Usage
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor
REPO = "wjbmattingly/comma-qwen-3.5-0.8b-full"
BASE = "Qwen/Qwen3.5-0.8B"
processor = AutoProcessor.from_pretrained(REPO)
model = AutoModelForImageTextToText.from_pretrained(
REPO, dtype=torch.bfloat16, device_map="cuda"
)
model.eval()
# The prompt is the 11.6k-character CATMuS rule set the model was trained under.
# Serving a different prompt serves a different task: the rules are what the
# target obeys, so the model was taught to read them as part of the input.
prompt = open("prompt.txt", encoding="utf-8").read()
image = Image.open("page.jpg").convert("RGB")
messages = [{
"role": "user",
"content": [{"type": "image"}, {"type": "text", "text": prompt}],
}]
# `enable_thinking=False` is not optional. With no kwarg, Qwen3.5 0.8B/2B leave
# the thinking block CLOSED and 4B/9B leave it OPEN -- and a model trained on
# pure transcription that is handed an open block will think instead of
# transcribing, emitting prose until the token budget runs out.
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
)
assert text.count("<think>") == text.count("</think>"), "thinking block left open"
inputs = processor(text=[text], images=[[image]], return_tensors="pt").to(model.device)
with torch.inference_mode():
out = model.generate(
**inputs,
do_sample=False, # greedy is the published control
repetition_penalty=1.1, # the measured serving default; 1.0 reproduces the table
max_new_tokens=3072,
eos_token_id=[processor.tokenizer.eos_token_id],
pad_token_id=processor.tokenizer.pad_token_id,
)
print(processor.tokenizer.decode(out[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True).strip())
prompt.txt is in the
demo Space.
The image budget travels with the checkpoint
This repo's processor_config.json is already capped to 2048 visual
tokens per page (2,097,152 pixels after smart-resize, one token per
32ร32 block), which is what training and scoring used. Load the processor from
this repo rather than from the base model: the library default is 16,777,216
pixels โ 16k visual tokens for one page โ and serving at that budget shows the
model a page at a resolution it never saw.
Training
| tuning | full fine-tune |
| trainable parameters | 852,985,920 of 852,985,920 (100.0%) |
| epochs | 3 |
| effective batch | 8 |
| learning rate | 5e-05 |
| max sequence length | 8192 |
| visual tokens per page | 2048 |
| precision | bf16 |
| hardware | NVIDIA RTX PRO 6000 Blackwell Server Edition |
| wall time | 19.8 h |
max_length never truncates: every sample is measured first and over-budget
ones are dropped, because a truncated completion teaches the model to stop
mid-transcription and a truncated prompt removes the rules the target obeys.
Limitations
- Nothing under 0.02 CER is a result. Four seeds of one configuration in this project gave CER 0.1271 / 0.1334 / 0.1370 / 0.1485 โ mean 0.1365, sd 0.0090. bf16 training is nondeterministic across nodes and greedy decoding amplifies a sub-millivolt logit difference into a different token. Treat differences smaller than ~0.02 as ties.
- Greedy is deterministic given identical batching, but not batch-size invariant. Re-scoring the same pages at batch 1 instead of 4 changes almost every prediction and moves aggregate CER by ยฑ0.013 for the smallest model here. Any comparison across a batch-size change is invalid.
- Quote
cer_nfd, notcer.ลฉ(U+0169) andu+ combining tilde are one glyph, and 77% of training targets mix the two forms โ so an edit distance over raw code points charges two edits for a difference that is not on the page, about 16% of all edit operations. Every checkpoint is biased toward one form and training does not fix it (the targets are mixed, so there is no signal to fit), which means rawcerpartly scores which Unicode form a tokenizer prefers. NFD and not NFC:tฬ rฬ mฬ pฬ cฬ qฬhave no precomposed form. - The remaining error is mostly convention, not reading. On the best run of
this project, word-division spaces are ~17% of all edit operations and
allographs another ~5%, against ~1% for genuine letter confusion (
rโs,fโs). Both are decisions about the transcription convention. Do not read a CER at this level as a statement about how well the model reads the script. - Latin, and this convention. Trained on Latin-dominant manuscripts under the
CATMuS graphemic standard. deep-jsonl uses the full MUFI superscript-letter
repertoire (
qอฅqui,qอฃqua,qอฆquo) where coarser conventions flatten almost everything to a tilde, so scoring this model against a corpus transcribed more coarsely costs 1โ2% CER for reasons unrelated to reading. - Blank and non-text pages return the literal string
VLM-NO-TEXT.
Citation
@misc{comma_qwen35,
title = {comma-qwen-3.5: CATMuS transcription models for medieval Latin manuscripts},
author = {Mattingly, William J. B.},
year = {2026},
url = {https://huggingface.co/wjbmattingly/comma-qwen-3.5-0.8b-full}
}
- Downloads last month
- 31