Jina AI, part of Elastic, has released jina-ocr-v1, an end-to-end visual document parser. It takes PDFs, scans, tables, charts or invoices and returns clean Markdown in 1 pass. The model has 3.4B total parameters, with about 570M decoder parameters active per token. A speculative decoding head ships inside the checkpoint. Jina AI built it to serve on low-budget GPUs such as the NVIDIA L4. The technical report lists 91.14 on OmniDocBench v1.6 and 83.4 on olmOCR-Bench.
Is it deployable? Yes, for research and non-commercial use. The open weights are about 6.8 GB in BF16 and run on Transformers or vLLM. The CC BY-NC 4.0 license means commercial use requires contacting Jina AI.
What is jina-ocr-v1?
The model post-trains DeepSeek-OCR and keeps its 2 efficiency components. DeepEncoder has about 380M parameters and chains SAM, a 16x convolutional compressor and CLIP-L. It turns a 1024×1024 page view from 4,096 patches into 256 visual tokens. A dynamic-resolution mode adds up to 9 local tiles at 100 tokens each. That caps a page at 1,156 visual tokens.
The decoder is DeepSeek-3B-MoE with 12 layers, 64 routed experts and 2 shared experts. Top-6 routing activates about 570M parameters per token. The position limit is 32,768. Output is Markdown, with tables in HTML and formulas in LaTeX.
How FastMTP Speculative Decoding Works
OCR output is near-deterministic and locally structured. That makes it a good fit for speculative decoding. Jina AI adds a FastMTP head: 1 dense draft block applied recursively for K=3 steps. Draft parameters stay constant as depth grows.
The decoder then verifies the drafts greedily. It accepts the longest prefix that matches its own choices and commits 1 more token itself. If all 3 drafts match, that extra token is a bonus. The committed text always equals plain greedy decoding, so the speedup is lossless. At K=3 the model commits 2.73 tokens per step on average.
‘,1],[‘
‘,1],[‘
‘,1],[‘
‘,1],[‘\n’,1],
[‘
‘,1],[‘
‘,1],[‘
‘,1],[‘
‘,1],[‘\n’,1],
[‘
‘,1],[‘
‘,1],[‘
‘,1],[‘
‘,1],[‘\n’,1],
[”,1],[‘\n’,1],
[‘**Total:**’,0,’**Sum:**’],[‘ $42′,0,’ $47′],[‘.90′,0,’.98′]
];
var MEASURED = {
0:’Draft head off. The decoder emits 1 token per pass. Jina AI measured 42.7 tokens per second on an L4 in eager mode.’,
1:’Measured at K=1: 1.83 tokens per step, 82.6% of drafts accepted, 1.50x faster in eager mode.’,
2:’Measured at K=2: 2.38 tokens per step, 69.1% of drafts accepted, 1.82x faster in eager mode.’,
3:’Measured at K=3: 2.73 tokens per step, 57.6% of drafts accepted, 1.95x faster in eager mode. One speculative step costs about 1.4 plain steps, which is why 2.73 tokens per step becomes 1.95x.’
};
var K = 3, pos = 0, passes = 0, playing = false, runId = 0, busy = false, seed = 7;
function rnd(){ seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }
function sleep(ms){ return new Promise(function(r){ setTimeout(r, RM ? Math.min(ms, 60) : ms); }); }
function show(s){ return s === ‘\n’ ? ‘\\n’ : s; }
function resetSim(){
runId++; busy = false; pos = 0; passes = 0; seed = 7;
$(‘page’).innerHTML = ‘‘;
$(‘status’).textContent = K === 0 ? ‘Draft head is off. The decoder writes 1 token per pass.’ : ‘The draft block is ready to propose ‘ + K + (K === 1 ? ‘ token.’ : ‘ tokens.’);
buildSlots(); updateStats();
$(‘measured’).textContent = MEASURED[K];
postH();
}
function buildSlots(){
var h=””;
for(var i = 0; i ‘ + (K === 0 ? ‘off’ : ‘draft ‘ + (i + 1)) + ”; }
$(‘slots’).innerHTML = h;
}
function updateStats(){
$(‘s-pass’).textContent = passes;
$(‘s-tok’).textContent = pos;
$(‘s-tpp’).textContent = passes ? (pos / passes).toFixed(2) : ‘0.00’;
var plain = Math.min(T.length, passes);
$(‘l-spec’).textContent = pos + ‘ of ‘ + T.length + ‘ tokens’;
$(‘l-plain’).textContent = plain + ‘ of ‘ + T.length + ‘ tokens’;
$(‘f-spec’).style.width = (100 * pos / T.length) + ‘%’;
$(‘f-plain’).style.width = (100 * plain / T.length) + ‘%’;
}
function commit(text, cls){
var caret = $(‘caret’);
if(text === ‘\n’){ caret.parentNode.insertBefore(document.createElement(‘br’), caret); return; }
var s = document.createElement(‘span’);
s.className=”tk ” + cls; s.textContent = text;
caret.parentNode.insertBefore(s, caret);
}
async function stepOnce(){
if(busy || pos >= T.length) return;
busy = true; var my = runId;
var slots = [].slice.call($(‘slots’).children);
slots.forEach(function(s, i){ s.className=”slot”; s.textContent = K === 0 ? ‘off’ : ‘draft ‘ + (i + 1); });
var kk = Math.min(K, T.length – pos), m = 0, guesses = [];
// decide accept pattern
for(var i = 0; i ‘));
if(ok && m === i) m++;
}
if(kk > 0){
$(‘status’).textContent=”The shared draft block proposes ” + kk + (kk === 1 ? ‘ token.’ : ‘ tokens, reusing the same weights at each depth.’);
for(i = 0; i = T.length){
await sleep(200); if(my !== runId) return;
var out = T.map(function(t){ return t[0]; }).join(”);
$(‘status’).textContent=”Done in ” + passes + ‘ verifier passes. Plain decoding needs ‘ + T.length + ‘. The text is identical either way (‘ + out.length + ‘ characters).’;
setPlaying(false);
}
busy = false; postH();
}
async function loop(){
var my = runId;
while(playing && pos = T.length ? ‘Play again’ : ‘Play’);
if(v){ if(pos >= T.length){ resetSim(); } loop(); }
}
$(‘play’).addEventListener(‘click’, function(){ setPlaying(!playing); });
$(‘stepb’).addEventListener(‘click’, function(){ if(playing){ setPlaying(false); } if(pos >= T.length){ resetSim(); } stepOnce(); });
$(‘reset’).addEventListener(‘click’, function(){ var was = playing; playing = false; resetSim(); setPlaying(was); });
[].slice.call(document.querySelectorAll(‘#kseg button’)).forEach(function(b){
b.addEventListener(‘click’, function(){
K = +b.dataset.k;
[].slice.call(document.querySelectorAll(‘#kseg button’)).forEach(function(x){ x.setAttribute(‘aria-pressed’, x === b ? ‘true’ : ‘false’); });
var was = playing; playing = false; resetSim(); setPlaying(was || true);
});
});
/* ———- page to tokens ———- */
var cv = $(‘cv’), ctx = cv.getContext(‘2d’), gridT = 0, played = false, animId = 0;
function sizeCanvas(){
var w = Math.round(Math.min(300, cv.clientWidth || 300) * (window.devicePixelRatio || 1));
if(w && cv.width !== w){ cv.width = w; cv.height = w; }
}
function drawGrid(t){
var W = cv.width, c = W / 64, C = W / 16, i, j;
ctx.clearRect(0, 0, W, W);
ctx.fillStyle=”#F3FAF9″; ctx.fillRect(0, 0, W, W);
// faux document content
ctx.fillStyle=”#0B2B2C”;
ctx.fillRect(W * .09, W * .08, W * .46, W * .035);
ctx.fillStyle=”rgba(11,43,44,.55)”;
for(i = 0; i 0){
for(i = 0; i 100
‘; }
tilesEl.innerHTML = th;
function setTiles(n){
[].slice.call(tilesEl.children).forEach(function(el, i){ el.classList.toggle(‘on’, i ‘; }
exEl.innerHTML = eh;
function route(){
var pool = [], pick = {}, i;
for(i = 0; i rows[best][1]) best = i; }
rows.forEach(function(r, idx){
h += ‘
‘;
});
$(‘readout’).textContent = mode === ‘eager’
? ‘In eager mode, each plain step is slow, so drafting pays off most. K=3 lifts decoding from 42.7 to 83.1 tokens per second.’
: ‘CUDA graphs already make plain steps fast, so a speculative step costs relatively more. K=1 is the best setting at 185.6 tokens per second.’;
$(‘spdnote’).textContent=”NVIDIA L4, olmOCR-Bench, batch size 1, measured by Jina AI. Accept rates and tokens per step barely change between modes. Not comparable with the A100 batch figures.”;
} else {
rows = A100; max = 2.8;
rows.forEach(function(r, idx){
h += ‘
‘;
});
$(‘readout’).textContent=”Pages per second is tokens per second divided by tokens per page. jina-ocr-v1 pairs 2,792 tokens per second with 1,085 tokens per page. Surya OCR 2 is faster per token at 3,760 but writes 3,568 tokens per page.”;
$(‘spdnote’).textContent=”One A100 40 GB, concurrency 32, 1,403 olmOCR-Bench pages, measured by Jina AI. 5 of the 14 systems in the comparison are shown. chandra-ocr-2 and dots.mocr score higher on olmOCR-Bench but parse fewer pages per second.”;
}
$(‘bars’).innerHTML = h;
requestAnimationFrame(function(){ requestAnimationFrame(function(){
[].slice.call(document.querySelectorAll(‘#bars .fl’)).forEach(function(el){ el.style.width = el.dataset.w + ‘%’; });
}); });
postH();
}
function seg(id, attr, fn){
[].slice.call(document.querySelectorAll(‘#’ + id + ‘ button’)).forEach(function(b){
b.addEventListener(‘click’, function(){
[].slice.call(document.querySelectorAll(‘#’ + id + ‘ button’)).forEach(function(x){ x.setAttribute(‘aria-pressed’, x === b ? ‘true’ : ‘false’); });
fn(b.dataset[attr]); renderSpeed();
});
});
}
seg(‘hwseg’, ‘hw’, function(v){ hw = v; });
seg(‘modeseg’, ‘mode’, function(v){ mode = v; });
/* ———- rewards ———- */
var TERMS = [
{k:’content’, n:’Content match’, f:0, v:.95},
{k:’table’, n:’Table structure’, f:.1, v:.9},
{k:’struct’, n:’Valid markup’, f:.2, v:1},
{k:’unit’, n:’Unit tests passed’, f:.2, v:.8},
{k:’rep’, n:’No repetition’, f:0, v:1}
];
var PRE = {
clean:{content:.95, table:.9, struct:1, unit:.8, rep:1},
tag:{content:.95, table:.9, struct:0, unit:.8, rep:1},
loop:{content:.9, table:.9, struct:1, unit:.6, rep:0}
};
var sh=””;
TERMS.forEach(function(t){
sh += ‘
‘ + (t.f ? ‘floor ‘ + t.f : ‘no floor’) + ‘
‘;
});
$(‘sliders’).innerHTML = sh;
function calc(){
var a = 1, b = 1, zeroBy = null, hardZero = null;
TERMS.forEach(function(t){
var v = +$(‘r-‘ + t.k).value;
$(‘v-‘ + t.k).textContent = v.toFixed(2);
a *= Math.max(v, t.f); b *= v;
if(v === 0 && !zeroBy) zeroBy = t;
if(v === 0 && !t.f && !hardZero) hardZero = t;
});
function put(id, val){
var el = $(id); el.querySelector(‘b’).textContent = val.toFixed(2);
el.querySelector(‘.gauge div’).style.width = (100 * val) + ‘%’;
el.classList.toggle(‘zero’, val === 0);
}
put(‘r-with’, a); put(‘r-wo’, b);
var msg;
if(a === 0){ msg = hardZero.k === ‘rep’ ? ‘Repetition has no floor, so a degenerate loop zeroes the reward. Jina AI leaves it unfloored because loops can inflate the content score.’ : ‘The paper gives no floor for the content term, so a page with no matching text earns nothing.’; }
else if(b === 0){ msg = ‘Without floors, the failed “‘ + zeroBy.n.toLowerCase() + ‘” check zeroes the product and the page teaches nothing. With the floor, the reward stays at ‘ + a.toFixed(2) + ‘ and the gradient survives.’; }
else { msg = ‘Every term is graded between 0 and 1, so a partly correct page still earns partial credit.’; }
$(‘rewmsg’).textContent = msg;
postH();
}
TERMS.forEach(function(t){ $(‘r-‘ + t.k).addEventListener(‘input’, calc); });
[].slice.call(document.querySelectorAll(‘[data-pre]’)).forEach(function(b){
b.addEventListener(‘click’, function(){
var p = PRE[b.dataset.pre];
TERMS.forEach(function(t){ $(‘r-‘ + t.k).value = p[t.k]; });
calc();
});
});
calc();
/* ———- start ———- */
resetSim();
if(!RM){ setTimeout(function(){ setPlaying(true); }, 700); } else { $(‘play’).textContent=”Play”; }
})();
Credit: Source link

























