wjbmattingly commited on
Commit
320c52d
·
verified ·
1 Parent(s): 241b77e

comma-qwen-3.5-0.8b-full-33k: continued full fine-tune on 33,112 CATMuS pages (cer_nfd 0.1240)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3.5-0.8B
4
+ library_name: transformers
5
+ datasets:
6
+ - comma-project/deep-jsonl
7
+ language:
8
+ - la
9
+ pipeline_tag: image-text-to-text
10
+ tags:
11
+ - ocr
12
+ - htr
13
+ - handwritten-text-recognition
14
+ - manuscripts
15
+ - palaeography
16
+ - catmus
17
+ - medieval-latin
18
+ - qwen3.5
19
+ - vision-language
20
+ metrics:
21
+ - cer
22
+ - wer
23
+ ---
24
+
25
+ # comma-qwen-3.5-0.8b-full-33k
26
+
27
+ `Qwen/Qwen3.5-0.8B` fine-tuned to produce **CATMuS-compliant, line-by-line transcriptions**
28
+ of medieval Latin manuscript pages.
29
+
30
+ Given a page image, the model returns one line of text for each physical written
31
+ line, in reading order. CATMuS is a *graphemic* standard: the sequence of letters
32
+ and signs as written, reduced to the modern Latin alphabet, with no editorial
33
+ intervention. The output is not a translation and not a normalised reading.
34
+
35
+ This is a **full fine-tune** — a standalone checkpoint, no adapter and no `peft` dependency at inference.
36
+
37
+ **This is a continued fine-tune.** It starts from `comma-qwen-3.5-0.8b-full (9,906 pages)` and trains further on new pages, so the *base* column below is that earlier model rather than a stock checkpoint — the gain shown is what the additional data bought, measured within one job.
38
+
39
+ - **Training data** — 33,112 pages from [`comma-project/deep-jsonl`](https://huggingface.co/datasets/comma-project/deep-jsonl)
40
+ - **Evaluation** — 243 held-out pages, each a different hand, none seen in training
41
+ - **Demo** — [comma-qwen-3.5-demo](https://huggingface.co/spaces/wjbmattingly/comma-qwen-3.5-demo)
42
+
43
+ ## Results
44
+
45
+ Greedy decoding, 3072 max new tokens, 2048 visual tokens per page,
46
+ batch 4. "Base" is the previous **comma-qwen-3.5-0.8b-full (9,906 pages)**, scored on the same pages in the same job — a base number from another job on another
47
+ day is not a control.
48
+
49
+ | metric | base | **this model** |
50
+ |---|---|---|
51
+ | CER (NFD) | 0.1458 | **0.1240** |
52
+ | CER (raw code points) | 0.1859 | 0.1651 |
53
+ | WER (NFD) | 0.4029 | 0.3584 |
54
+ | line recall (NFD) | 0.1672 | 0.1883 |
55
+ | CER macro (NFD) | 0.1318 | 0.1385 |
56
+ | degenerate pages | 0.0041 | 0.0000 |
57
+ | truncated pages | 0.0165 | 0.0123 |
58
+
59
+ CER/WER are micro-averaged (total edits ÷ total reference characters), so a long
60
+ page outweighs a short one; the macro mean is given alongside because a gap
61
+ between them says the errors are concentrated. `line_recall` is the share of
62
+ reference lines reproduced exactly and in order — for a line-by-line CATMuS
63
+ target that is the number a palaeographer looks at first.
64
+
65
+ ## Usage
66
+
67
+ ```python
68
+ import torch
69
+ from PIL import Image
70
+ from transformers import AutoModelForImageTextToText, AutoProcessor
71
+
72
+ REPO = "wjbmattingly/comma-qwen-3.5-0.8b-full-33k"
73
+ BASE = "Qwen/Qwen3.5-0.8B"
74
+
75
+ processor = AutoProcessor.from_pretrained(REPO)
76
+ model = AutoModelForImageTextToText.from_pretrained(
77
+ REPO, dtype=torch.bfloat16, device_map="cuda"
78
+ )
79
+ model.eval()
80
+
81
+ # The prompt is the 11.6k-character CATMuS rule set the model was trained under.
82
+ # Serving a different prompt serves a different task: the rules are what the
83
+ # target obeys, so the model was taught to read them as part of the input.
84
+ prompt = open("prompt.txt", encoding="utf-8").read()
85
+
86
+ image = Image.open("page.jpg").convert("RGB")
87
+ messages = [{
88
+ "role": "user",
89
+ "content": [{"type": "image"}, {"type": "text", "text": prompt}],
90
+ }]
91
+
92
+ # `enable_thinking=False` is not optional. With no kwarg, Qwen3.5 0.8B/2B leave
93
+ # the thinking block CLOSED and 4B/9B leave it OPEN -- and a model trained on
94
+ # pure transcription that is handed an open block will think instead of
95
+ # transcribing, emitting prose until the token budget runs out.
96
+ text = processor.apply_chat_template(
97
+ messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
98
+ )
99
+ assert text.count("<think>") == text.count("</think>"), "thinking block left open"
100
+
101
+ inputs = processor(text=[text], images=[[image]], return_tensors="pt").to(model.device)
102
+
103
+ with torch.inference_mode():
104
+ out = model.generate(
105
+ **inputs,
106
+ do_sample=False, # greedy is the published control
107
+ repetition_penalty=1.1, # the measured serving default; 1.0 reproduces the table
108
+ max_new_tokens=3072,
109
+ eos_token_id=[processor.tokenizer.eos_token_id],
110
+ pad_token_id=processor.tokenizer.pad_token_id,
111
+ )
112
+
113
+ print(processor.tokenizer.decode(out[0][inputs["input_ids"].shape[1]:],
114
+ skip_special_tokens=True).strip())
115
+ ```
116
+
117
+ `prompt.txt` is in the
118
+ [demo Space](https://huggingface.co/spaces/wjbmattingly/comma-qwen-3.5-demo/blob/main/prompt.txt).
119
+
120
+ ### The image budget travels with the checkpoint
121
+
122
+ This repo's `processor_config.json` is already capped to **2048 visual
123
+ tokens** per page (2,097,152 pixels after smart-resize, one token per
124
+ 32×32 block), which is what training and scoring used. Load the processor from
125
+ this repo rather than from the base model: the library default is 16,777,216
126
+ pixels — 16k visual tokens for one page — and serving at that budget shows the
127
+ model a page at a resolution it never saw.
128
+
129
+ ## Training
130
+
131
+ | | |
132
+ |---|---|
133
+ | tuning | full fine-tune |
134
+ | trainable parameters | 852,985,920 of 852,985,920 (100.0%) |
135
+ | epochs | 2 |
136
+ | effective batch | 8 |
137
+ | learning rate | 5e-05 |
138
+ | max sequence length | 8192 |
139
+ | visual tokens per page | 2048 |
140
+ | precision | bf16 |
141
+ | hardware | NVIDIA RTX PRO 6000 Blackwell Server Edition |
142
+ | wall time | 38.9 h |
143
+
144
+ `max_length` never truncates: every sample is measured first and over-budget
145
+ ones are dropped, because a truncated completion teaches the model to stop
146
+ mid-transcription and a truncated prompt removes the rules the target obeys.
147
+
148
+ ## Limitations
149
+
150
+ - **Nothing under 0.02 CER is a result.** Four seeds of one configuration in this
151
+ project gave CER 0.1271 / 0.1334 / 0.1370 / 0.1485 — mean 0.1365, sd 0.0090.
152
+ bf16 training is nondeterministic across nodes and greedy decoding amplifies a
153
+ sub-millivolt logit difference into a different token. Treat differences
154
+ smaller than ~0.02 as ties.
155
+ - **Greedy is deterministic given identical batching, but not batch-size
156
+ invariant.** Re-scoring the same pages at batch 1 instead of 4 changes almost
157
+ every prediction and moves aggregate CER by ±0.013 for the smallest model here.
158
+ Any comparison across a batch-size change is invalid.
159
+ - **Quote `cer_nfd`, not `cer`.** `ũ` (U+0169) and `u` + combining tilde are one
160
+ glyph, and 77% of training targets mix the two forms — so an edit distance over
161
+ raw code points charges two edits for a difference that is not on the page,
162
+ about 16% of all edit operations. Every checkpoint is biased toward one form
163
+ and training does not fix it (the targets are mixed, so there is no signal to
164
+ fit), which means raw `cer` partly scores which Unicode form a tokenizer
165
+ prefers. NFD and not NFC: `t̃ r̃ m̃ p̃ c̃ q̃` have no precomposed form.
166
+ - **The remaining error is mostly convention, not reading.** On the best run of
167
+ this project, word-division spaces are ~17% of all edit operations and
168
+ allographs another ~5%, against ~1% for genuine letter confusion (`r`↔`s`,
169
+ `f`↔`s`). Both are decisions about the transcription convention. Do not read a
170
+ CER at this level as a statement about how well the model reads the script.
171
+ - **Latin, and this convention.** Trained on Latin-dominant manuscripts under the
172
+ CATMuS graphemic standard. deep-jsonl uses the full MUFI superscript-letter
173
+ repertoire (`qͥ` qui, `qͣ` qua, `qͦ` quo) where coarser conventions flatten almost
174
+ everything to a tilde, so scoring this model against a corpus transcribed more
175
+ coarsely costs 1–2% CER for reasons unrelated to reading.
176
+ - **Blank and non-text pages** return the literal string `VLM-NO-TEXT`.
177
+
178
+ ## Citation
179
+
180
+ ```bibtex
181
+ @misc{comma_qwen35,
182
+ title = {comma-qwen-3.5: CATMuS transcription models for medieval Latin manuscripts},
183
+ author = {Mattingly, William J. B.},
184
+ year = {2026},
185
+ url = {https://huggingface.co/wjbmattingly/comma-qwen-3.5-0.8b-full-33k}
186
+ }
187
+ ```
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is true %}
150
+ {{- '<think>\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n\n</think>\n\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5ForConditionalGeneration"
4
+ ],
5
+ "dtype": "bfloat16",
6
+ "eos_token_id": 248046,
7
+ "image_token_id": 248056,
8
+ "model_type": "qwen3_5",
9
+ "pad_token_id": 248044,
10
+ "text_config": {
11
+ "attention_bias": false,
12
+ "attention_dropout": 0.0,
13
+ "attn_output_gate": true,
14
+ "bos_token_id": null,
15
+ "dtype": "bfloat16",
16
+ "eos_token_id": 248044,
17
+ "full_attention_interval": 4,
18
+ "head_dim": 256,
19
+ "hidden_act": "silu",
20
+ "hidden_size": 1024,
21
+ "initializer_range": 0.02,
22
+ "intermediate_size": 3584,
23
+ "layer_types": [
24
+ "linear_attention",
25
+ "linear_attention",
26
+ "linear_attention",
27
+ "full_attention",
28
+ "linear_attention",
29
+ "linear_attention",
30
+ "linear_attention",
31
+ "full_attention",
32
+ "linear_attention",
33
+ "linear_attention",
34
+ "linear_attention",
35
+ "full_attention",
36
+ "linear_attention",
37
+ "linear_attention",
38
+ "linear_attention",
39
+ "full_attention",
40
+ "linear_attention",
41
+ "linear_attention",
42
+ "linear_attention",
43
+ "full_attention",
44
+ "linear_attention",
45
+ "linear_attention",
46
+ "linear_attention",
47
+ "full_attention"
48
+ ],
49
+ "linear_conv_kernel_dim": 4,
50
+ "linear_key_head_dim": 128,
51
+ "linear_num_key_heads": 16,
52
+ "linear_num_value_heads": 16,
53
+ "linear_value_head_dim": 128,
54
+ "mamba_ssm_dtype": "float32",
55
+ "max_position_embeddings": 262144,
56
+ "mlp_only_layers": [],
57
+ "model_type": "qwen3_5_text",
58
+ "mtp_num_hidden_layers": 1,
59
+ "mtp_use_dedicated_embeddings": false,
60
+ "num_attention_heads": 8,
61
+ "num_hidden_layers": 24,
62
+ "num_key_value_heads": 2,
63
+ "pad_token_id": null,
64
+ "partial_rotary_factor": 0.25,
65
+ "rms_norm_eps": 1e-06,
66
+ "rope_parameters": {
67
+ "mrope_interleaved": true,
68
+ "mrope_section": [
69
+ 11,
70
+ 11,
71
+ 10
72
+ ],
73
+ "partial_rotary_factor": 0.25,
74
+ "rope_theta": 10000000,
75
+ "rope_type": "default"
76
+ },
77
+ "tie_word_embeddings": true,
78
+ "use_cache": true,
79
+ "vocab_size": 248320
80
+ },
81
+ "tie_word_embeddings": true,
82
+ "transformers_version": "5.15.0",
83
+ "use_cache": false,
84
+ "video_token_id": 248057,
85
+ "vision_config": {
86
+ "deepstack_visual_indexes": [],
87
+ "depth": 12,
88
+ "dtype": "bfloat16",
89
+ "hidden_act": "gelu_pytorch_tanh",
90
+ "hidden_size": 768,
91
+ "in_channels": 3,
92
+ "initializer_range": 0.02,
93
+ "intermediate_size": 3072,
94
+ "model_type": "qwen3_5_vision",
95
+ "num_heads": 12,
96
+ "num_position_embeddings": 2304,
97
+ "out_hidden_size": 1024,
98
+ "patch_size": 16,
99
+ "spatial_merge_size": 2,
100
+ "temporal_patch_size": 2
101
+ },
102
+ "vision_end_token_id": 248054,
103
+ "vision_start_token_id": 248053
104
+ }
generation_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": [
4
+ 248046,
5
+ 248044
6
+ ],
7
+ "pad_token_id": 248044,
8
+ "transformers_version": "5.15.0",
9
+ "use_cache": true
10
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27542c7c65470285272b25f3d6ab1b479490eaaade84ef7d93ebe059d7a17a36
3
+ size 1706030528
processor_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 2097152,
24
+ "shortest_edge": 65536
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "merge_size": 2,
48
+ "min_frames": 4,
49
+ "patch_size": 16,
50
+ "resample": 3,
51
+ "rescale_factor": 0.00392156862745098,
52
+ "return_metadata": false,
53
+ "size": {
54
+ "longest_edge": 25165824,
55
+ "shortest_edge": 4096
56
+ },
57
+ "temporal_patch_size": 2,
58
+ "video_processor_type": "Qwen3VLVideoProcessor"
59
+ }
60
+ }
prompt.txt ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are a palaeographer producing CATMuS-compliant training data from the attached page image.
2
+
3
+ Transcribe the page LINE BY LINE: output exactly one line of text for each physical written line on the page, in reading order, top to bottom (and for a page in columns, finish the left column before starting the right one).
4
+
5
+ CATMuS TRANSCRIPTION RULES
6
+ You MUST follow the CATMuS guidelines (Consistent Approaches to Transcribing ManuScripts) exactly. CATMuS is a GRAPHEMIC transcription standard for producing machine-learning training data: reproduce the sequence of letters and signs as written, reduced to the modern Latin alphabet, with NO editorial intervention.
7
+
8
+ A. GENERAL PRINCIPLES
9
+ A1. Transcribe what is on the page, grapheme by grapheme, in the order written. Never translate, modernize, correct, complete, or improve the text.
10
+ A2. Scribal errors, repetitions, and odd spellings MUST be reproduced as written. Never mark them, never add [sic].
11
+ A3. Never expand abbreviations. Never resolve them silently. See section D.
12
+ A4. Decoration, ornament, ruling, and stains are NOT text: do not transcribe them.
13
+ A5. Prefer Unicode's public domain and MUFI characters; the character table in section I is authoritative and takes priority over your own preference.
14
+ A6. Do not add any markup, tags, comments, notes, numbering, or explanations of your own.
15
+
16
+ B. LETTERS AND NUMBERS
17
+ B1. Transcribe alphabetic signs with the modern Latin alphabet. Letterform variants (allographs) are NOT distinguished.
18
+ B2. Long s (ſ) MUST be transcribed as s. Insular/other obsolete letterforms MUST be reduced to their modern Latin equivalent (e.g. ƞ as n).
19
+ B3. Capital letters MUST be preserved. Small caps MUST be rendered as ordinary uppercase. Lowercase MUST NOT be normalized to uppercase.
20
+ B3a. Case follows the script on the page: a line written in a display majuscule (rustic capitals, uncial, epigraphic capitals — typical of incipits and rubrics) MUST be transcribed in uppercase; a line written in minuscule MUST be transcribed in lowercase, whatever its size or ink.
21
+ B3b. Letters spaced widely apart for display MUST NOT be split into separate words: transcribe INNOCENTI, never IN NOCEN TI. Insert a space only where the source marks an actual word division.
22
+ B4. Italic, bold, and other stylistic variation MUST NOT be transcribed.
23
+ B5. Ramist letters: in medieval sources v MUST be transcribed as u, and j MUST be transcribed as i (they are allographic, not separate letters). In modern/printed sources follow the source's own usage.
24
+ B6. Ligatures MUST be split into their separate letters, EXCEPT the ampersand & [U+0026], which MUST be kept as &, and ß [U+00DF] in modern sources, which MUST be kept as ß.
25
+ B7. Numbers MUST be transcribed as they appear, Roman or Arabic. Keep the dots or other punctuation surrounding a numeral (e.g. .iiii.). Apply B5 to Roman numerals (uii, not vij).
26
+ B8. Old-style Roman numerals CIↃ and IↃ MUST be transcribed as M and D.
27
+ B9. Drop capitals and oversized/decorated initials MUST be transcribed as uppercase letters, and belong to their OWN line, not to the text line they introduce.
28
+
29
+ C. SEGMENTATION AND SPACING
30
+ C1. Word division MUST follow the source as closely as possible. Where the spacing is genuinely unclear, fall back on modernized word division using a single space [U+0020].
31
+ C2. Original agglutination MUST be kept and MUST NOT be given an apostrophe: write qil, not q'il.
32
+ C3. Doubled consonants marking elision MUST be kept: write arriva, not á riva.
33
+ C4. Locutions being lexicalized MUST be written as they appear (enchargier or en chargier — whichever the page shows).
34
+ C5. Hyphenation MUST be transcribed only where the source marks it, with - [U+002D], whatever its shape in the source. Never add a hyphen the source does not have. If the mark is repeated at the start of the next line, transcribe that one as - too.
35
+ C6. Diastoles — vertical or oblique strokes drawn between two letters to show they belong to different words — MUST be transcribed as / [U+002F].
36
+ C7. No double spaces. No space before a punctuation mark. One space after a punctuation mark, except after quotation marks and apostrophes.
37
+ C7a. Medieval scribes routinely leave a gap on BOTH sides of a dot or colon. You MUST NOT reproduce that gap: spacing is normalized, not imitated. Write opus est. ut and euanescãt: opus — NEVER opus est . ut, NEVER euanescãt : opus, NEVER est .ut. Every . : ; , ! ? touches the character before it and is followed by exactly one space (or by the end of the line).
38
+ C7b. Never use uppercase I or L inside an otherwise lowercase word: Basilea, never BasiIea. A tall undotted stroke inside a lowercase word is l.
39
+
40
+ D. ABBREVIATIONS
41
+ D1. Abbreviations MUST be reproduced and MUST NOT be expanded. Expansion is a separate task performed after transcription.
42
+ D2. Use the character whose SHAPE matches the mark on the page, and whose semantic value is correct: for p with a stroke through the descender use ꝑ [U+A751], never the Armenian ք [U+0554].
43
+ D3. A tilde or macron above letters MUST be transcribed as combining tilde [U+0303] on the letter it sits over: ẽ, õ, ũ, ñ, ã. A straight bar and a wavy stroke are the SAME sign in CATMuS: ALWAYS use combining tilde [U+0303] and NEVER combining macron [U+0304] — write scõrũ, never scōrū.
44
+ D4. When one tilde covers more than one element, either double it (ẽẽ = esse) or place it over the single most appropriate letter. Be consistent within a page.
45
+ D5. Superscript/overwritten letters MUST be transcribed as the matching COMBINING letter on the base letter (e.g. q + [U+0365] = qͥ, u + [U+036C] = uͬ), not as a normal letter and not as a caret sequence.
46
+ D6. Strike-through abbreviations MUST use the dedicated characters: đ ħ ł ꝑ ꝓ ꝙ ꝵ ẜ (section I).
47
+ D7. The Tironian et MUST be transcribed as ⁊ [U+204A]; an ampersand-shaped et as & [U+0026]. A tilde over either (⁊̃) is kept.
48
+ D8. Do not invent abbreviation characters. If no table character matches the mark, transcribe the letters that are actually written and place a combining tilde [U+0303] over the letter carrying the mark.
49
+
50
+ E. ACCENTS AND CEDILLAS
51
+ E1. Accents MUST be transcribed wherever they appear.
52
+ E2. A pointed/dotted i MUST be transcribed as a plain i — NEVER as í.
53
+ E3. An accent drawn out of place (common in cursive) MUST be transcribed over the letter it logically belongs to, not where it physically sits.
54
+ E4. Cedillas MUST be transcribed with the combining cedilla [U+0327]: ȩ (which in 10th–13th c. Latin usually stands for the ae diphthong).
55
+
56
+ F. PUNCTUATION
57
+ F1. Medieval sources: EVERY single dot, at any height (low, mid, high, punctus elevatus), MUST be transcribed as . [U+002E].
58
+ F2. Medieval sources: double dots and the punctus interrogativus MUST be transcribed as : [U+003A].
59
+ F3. The virgula suspensiva MUST be transcribed as / [U+002F] (same sign as the diastole).
60
+ F4. Modern/contemporary sources: transcribe punctuation exactly as it appears. All dashes, long or short, MUST be transcribed as - [U+002D]. Any quotation-mark-like character MUST be transcribed as " [U+0022]; any apostrophe-like character as ' [U+0027].
61
+ F5. Never add punctuation that is not on the page, and never delete punctuation that is.
62
+
63
+ G. FUNCTIONAL AND CORRECTION SIGNS
64
+ G1. Pilcrows and their variants (reverse pilcrow, ornamental paragraph marks) MUST be transcribed as ¶ [U+00B6], whatever their orientation. In modern documents a section sign is § [U+00A7].
65
+ G1a. A pilcrow belongs to the line it stands at the head of, followed by one space: "¶ Bonũ ergo est.". NEVER put a pilcrow on a line of its own.
66
+ G2. Insertion/omission carets MUST be transcribed as ‸ [U+2038].
67
+ G3. Reference marks (asterisks, ※ and similar) MUST be normalized to * [U+002A].
68
+ G4. Manicules MUST be transcribed as ☞ [U+261E].
69
+ G5. Text struck out or cancelled by the scribe MUST be kept and wrapped in ⟦ [U+27E6] and ⟧ [U+27E7]: ⟦chapelain⟧.
70
+ G5a. Mark ⟦ ⟧ ONLY where you can actually see strokes cancelling the letters. Never bracket a passage because it reads oddly, looks redundant, or is hard to make out.
71
+ G6. Text that cannot be read (damage, stain, erasure) MUST be marked with [ [U+005B] and ] [U+005D], optionally containing one dot per illegible letter: escr[]pt, or escr[..]pt.
72
+ G6a. Brackets NEVER contain a conjecture: write p[..]uintias, never p[ro]uintias, and never [s] for a letter you think was there. If you can read a letter, transcribe it; if you cannot, it is dots or nothing.
73
+ G7. An interlinear addition is its own text line, not part of the line above or below.
74
+
75
+ H. DETERMINISM
76
+ H1. Apply these rules mechanically. Given the same page you MUST produce the same output every time.
77
+ H2. Never guess at content you cannot see: use the [] convention of G6 instead.
78
+ H3. Never output an explanation, apology, confidence note, or alternative reading.
79
+ H4. Transcribe each line of the page exactly once, then move to the next line. NEVER repeat a line, a phrase, or a reference more times than it is written on the page.
80
+ H5. Stop as soon as you reach the last line on the page. If you notice yourself producing the same text again, stop immediately — a truncated transcription is far better than a repeated one.
81
+ H6. Read the page; do not continue a pattern. Never extend a series (of citations, numbers, or letters) beyond what is actually written.
82
+
83
+ I. CHARACTER TABLE (authoritative — prefer these characters)
84
+ Strike/bar abbreviations: đ [U+0111] · ħ [U+0127] · ł [U+0142] · ꝑ [U+A751] p-with-stroke · ꝓ [U+A753] p-with-flourish · ꝙ [U+A759] q-with-stroke · ꝵ [U+A775] -rum · ẜ [U+1E9C] long-s-with-stroke
85
+ Standalone abbreviation signs: ⁊ [U+204A] et · & [U+0026] et · ꝭ [U+A76D] -is · ꝯ [U+A76F] con- · ꝰ [U+A770] -us/-os (superscript) · ≈ [U+2248] esse · ÷ [U+00F7] est/id est · ∅ [U+2205] instans
86
+ Combining marks (placed on the preceding letter): ̃ [U+0303] tilde — nasalisation, suspension, contraction · ̾ [U+033E] vertical tilde — -r/-er · ̧ [U+0327] cedilla
87
+ Combining letters: ͣ [U+0363] a · ᷓ [U+1DD3] flattened open a · ͨ [U+0368] c · ͤ [U+0364] e · ͥ [U+0365] i · ͫ [U+036B] m · ͦ [U+0366] o · ͬ [U+036C] r · ᷤ [U+1DE4] s · ͭ [U+036D] t · ͯ [U+036F] x · ᷑ [U+1DD1] ur · ᷒ [U+1DD2] us
88
+ Punctuation and marks: . [U+002E] · : [U+003A] · / [U+002F] diastole & virgula · - [U+002D] hyphenation · ¶ [U+00B6] · § [U+00A7] · * [U+002A] · ‸ [U+2038] · ☞ [U+261E] · ⟦ ⟧ [U+27E6 U+27E7] struck out · [ ] [U+005B U+005D] illegible
89
+ Units: ℥ [U+2125] ounce · 𐆒 [U+10192] semuncia
90
+
91
+ J. LINE-LEVEL OUTPUT
92
+ J0. Before you write each line, apply the spacing normalization of rules C7–C7a: no space before . : ; , ? !, exactly one space after, and never two spaces in a row — however widely spaced the marks are on the page.
93
+ J1. One physical line on the page = one line of output. NEVER merge two written lines into one output line, and NEVER split one written line across two.
94
+ J2. Keep each line exactly as it ends on the page, including its hyphenation mark (rule C5). Do not rejoin a word broken across two lines.
95
+ J3. A drop capital or oversized initial is its own output line (rule B9).
96
+ J4. An interlinear addition is its own output line, placed after the line it sits above (rule G7).
97
+ J5. Marginalia, rubrics, running titles, page numbers, catchwords, and signature marks ARE text and MUST each be transcribed on their own line, in the position where a reader meets them (running title and page number first if they head the page, catchword last).
98
+ J6. Leave no blank lines between output lines.
99
+ J7. Do not number the lines. Do not add a heading. Do not wrap the output in quotes or code fences.
100
+ J8. If the page carries no text at all, return exactly: VLM-NO-TEXT
101
+
102
+ Return ONLY the transcription.
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
tokenizer_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|im_end|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": true,
14
+ "model_max_length": 262144,
15
+ "model_specific_special_tokens": {
16
+ "audio_bos_token": "<|audio_start|>",
17
+ "audio_eos_token": "<|audio_end|>",
18
+ "audio_token": "<|audio_pad|>",
19
+ "image_token": "<|image_pad|>",
20
+ "video_token": "<|video_pad|>",
21
+ "vision_bos_token": "<|vision_start|>",
22
+ "vision_eos_token": "<|vision_end|>"
23
+ },
24
+ "pad_token": "<|endoftext|>",
25
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
+ "processor_class": "Qwen3VLProcessor",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null,
30
+ "video_token": "<|video_pad|>",
31
+ "vision_bos_token": "<|vision_start|>",
32
+ "vision_eos_token": "<|vision_end|>"
33
+ }