failed09's picture
Initial public release of BashkirTrOCR v3 (ONNX FP16 CUDA & INT8 CPU)
c8cdee4 verified
Raw History Blame Contribute Delete
10.6 kB
<!doctype html>
<html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bashkir OCR Studio</title>
<style>
:root{color-scheme:light;--ink:#183333;--muted:#637778;--border:#d8e3df;--accent:#196c56}*{box-sizing:border-box}body{margin:0;background:#f3f6f2;color:var(--ink);font:16px/1.55 system-ui,sans-serif}main{max-width:1300px;margin:auto;padding:32px 24px}header{display:flex;justify-content:space-between;align-items:start;gap:20px}h1{font-size:30px;letter-spacing:-1px;margin:0}p{max-width:850px}.muted{color:var(--muted)}.card{background:white;border:1px solid var(--border);border-radius:16px;padding:24px;margin:22px 0}.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap}button,.file{border:1px solid var(--border);background:white;border-radius:9px;padding:11px 17px;font:inherit;color:var(--ink);cursor:pointer}button.primary{background:var(--accent);color:white;border-color:var(--accent)}button:disabled{opacity:.45;cursor:wait}input[type=file]{max-width:400px}#original{max-height:420px;max-width:100%;object-fit:contain;margin:18px 0}.tabs{display:inline-flex;background:#e8efec;padding:4px;border-radius:11px;border:1px solid var(--border);gap:4px}.tab{border:none;background:transparent;border-radius:8px;padding:8px 15px;font-weight:600;color:var(--muted);cursor:pointer}.tab.active{background:var(--accent);color:white;box-shadow:0 1px 3px rgba(0,0,0,.12)}.row{display:grid;grid-template-columns:42px minmax(0,1fr) minmax(0,1fr);gap:16px;padding:18px 0;border-bottom:1px solid var(--border);align-items:start}.row img{max-width:100%;background:#fff}.num{color:var(--muted);font-size:13px}textarea{width:100%;min-height:62px;resize:vertical;border:1px solid var(--border);border-radius:8px;padding:10px;font:17px/1.5 system-ui;color:var(--ink);background:#fcfdfb}.page-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;align-items:start;margin-top:12px}.page-pane{position:sticky;top:16px}.page-pane img{max-width:100%;max-height:78vh;object-fit:contain;border:1px solid var(--border);border-radius:10px;background:#fff;display:block}#fullText{min-height:520px;height:72vh;font:17px/1.6 system-ui;padding:14px}.warning{color:#9b5b12;font-size:13px}.rowhead{font-weight:600;color:var(--muted);font-size:13px}pre{white-space:pre-wrap;font:inherit}.badge{white-space:nowrap;border:1px solid var(--border);padding:7px 12px;border-radius:30px}#message[role=alert]{color:#9b3612}@media(max-width:700px){main{padding:22px 14px}.card{padding:16px}.row{grid-template-columns:25px minmax(0,1fr)}.row textarea,.row .result{grid-column:2}.rowhead{display:none}.page-split{grid-template-columns:1fr}.page-pane{position:static}header{display:block}.badge{display:inline-block;margin-top:12px}}
</style>
<main><header><div><h1>Bashkir OCR Studio</h1><div class="muted">Распознайте. Сверьте со сканом. Сохраните текст.</div></div><span class="badge" id="backend">Загрузка…</span></header>
<section class="card"><div class="controls"><input id="file" type="file" accept="image/png,image/jpeg,image/webp,image/tiff,image/bmp"><button class="primary" id="recognize" disabled>Распознать</button><div style="flex:1"></div><div class="tabs" role="tablist" aria-label="Режим просмотра"><button type="button" class="tab active" id="tabLines" role="tab" aria-selected="true">По строкам</button><button type="button" class="tab" id="tabPage" role="tab" aria-selected="false">Страница и текст</button></div></div><p class="muted">Изображение обрабатывается на этом компьютере. Оно не отправляется в облако и не сохраняется сервером. Поддерживается одно изображение за раз.</p><details><summary>На что обратить внимание</summary><p class="muted">Проверьте мелкие сноски: <b>1</b> может превратиться в <b>!</b>. Возможны ошибки регистра, похожих букв и тире. Длинные строки разделяются на части; сложные колонки, наклон и декоративные элементы могут нарушать порядок и полноту текста. Результат требует вычитки.</p></details><img id="original" alt="Выбранное изображение" hidden><p id="message" aria-live="polite">Выберите изображение со страницы книги или документа.</p></section>
<section class="card" id="results" hidden><div class="controls"><div class="tabs" role="tablist" aria-label="Режим просмотра результатов"><button type="button" class="tab active" id="tabLines2" role="tab" aria-selected="true">По строкам</button><button type="button" class="tab" id="tabPage2" role="tab" aria-selected="false">Страница и текст</button></div><div style="flex:1"></div><button id="copyText">Скопировать текст</button><button id="saveText">Сохранить TXT</button></div><p class="muted" id="summary"></p><div id="viewLines"><div class="row rowhead"><span>№</span><span>Оригинальная строка</span><span>Распознанный текст — можно исправить</span></div><div id="rows"></div></div><div id="viewPage" class="page-split" hidden><div class="page-pane"><div class="rowhead" style="margin-bottom:8px">Страница</div><img id="pageImage" alt="Исходная страница"></div><div class="text-pane"><div class="rowhead" style="margin-bottom:8px">Весь распознанный текст — можно исправить</div><textarea id="fullText" aria-label="Весь распознанный текст"></textarea></div></div></section><footer class="muted">TrOCR v3 · Без словарной автозамены · Закройте окно запуска, чтобы остановить OCR.</footer></main>
<script>
const $=id=>document.getElementById(id);let chosen=null,objectUrl=null,answer=null,mode='lines';
function setMode(next){mode=next;const isPage=mode==='page';['tabLines','tabLines2'].forEach(id=>{$(id).classList.toggle('active',!isPage);$(id).setAttribute('aria-selected',String(!isPage))});['tabPage','tabPage2'].forEach(id=>{$(id).classList.toggle('active',isPage);$(id).setAttribute('aria-selected',String(isPage))});$('viewLines').hidden=isPage;$('viewPage').hidden=!isPage}
$('tabLines').onclick=$('tabLines2').onclick=()=>setMode('lines');
$('tabPage').onclick=$('tabPage2').onclick=()=>setMode('page');
function syncFullFromRows(){$('fullText').value=[...$('rows').querySelectorAll('textarea')].map(x=>x.value).join('\n')}
$('fullText').addEventListener('input',()=>{const boxes=[...$('rows').querySelectorAll('textarea')];const parts=$('fullText').value.split('\n');if(parts.length===boxes.length)boxes.forEach((b,i)=>{b.value=parts[i]})});
fetch('/status').then(r=>r.json()).then(x=>{$('backend').textContent=x.backend==='gpu'?'NVIDIA GPU · FP16':'CPU · INT8'}).catch(()=>{$('backend').textContent='Сервер недоступен'});
$('file').addEventListener('change',()=>{chosen=$('file').files[0];$('recognize').disabled=!chosen;if(objectUrl)URL.revokeObjectURL(objectUrl);if(chosen){objectUrl=URL.createObjectURL(chosen);$('original').src=objectUrl;$('pageImage').src=objectUrl;$('original').hidden=false;$('results').hidden=true;$('message').textContent='Нажмите «Распознать».'}});
$('recognize').onclick=async()=>{if(!chosen)return;$('recognize').disabled=true;$('message').removeAttribute('role');$('message').textContent='Идёт распознавание. На CPU это может занять несколько минут.';$('results').hidden=true;try{const data=new FormData();data.append('file',chosen);const response=await fetch('/recognize',{method:'POST',body:data});if(!response.ok){const e=await response.json();throw new Error(e.detail||'Не удалось распознать изображение')}answer=await response.json();$('rows').replaceChildren();answer.lines.forEach((line,index)=>{const row=document.createElement('div');row.className='row';const n=document.createElement('span');n.className='num';n.textContent=index+1;const img=document.createElement('img');img.src=line.image;img.alt='Строка '+(index+1);const result=document.createElement('div');result.className='result';const edit=document.createElement('textarea');edit.value=line.text;edit.setAttribute('aria-label','Текст строки '+(index+1));edit.addEventListener('input',syncFullFromRows);result.append(edit);if(line.truncated){const note=document.createElement('div');note.className='warning';note.textContent='Достигнут предел длины: проверьте, не обрезана ли строка.';result.append(note)}row.append(n,img,result);$('rows').append(row)});syncFullFromRows();$('summary').textContent=`Строк: ${answer.lines.length}. Время: ${answer.seconds.toFixed(1)} с. ${answer.backend==='gpu'?'GPU FP16':'CPU INT8'}.`;$('message').textContent=answer.no_text_detected?'Текст не обнаружен. Проверьте, есть ли он на изображении; модель не распознаёт иллюстрацию как целую строку.':'Готово. Переключайте вкладки «По строкам» и «Страница и текст» для удобной сверки.';$('results').hidden=answer.no_text_detected;if(!answer.no_text_detected)$('original').hidden=true}catch(e){$('message').setAttribute('role','alert');$('message').textContent=e.message}finally{$('recognize').disabled=!chosen}};
function currentText(){return($('fullText').value||[...$('rows').querySelectorAll('textarea')].map(x=>x.value).join('\n'))}
function save(content,type,name){const url=URL.createObjectURL(new Blob([content],{type}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
$('copyText').onclick=async()=>{await navigator.clipboard.writeText(currentText());const old=$('copyText').textContent;$('copyText').textContent='Скопировано!';setTimeout(()=>{$('copyText').textContent=old},1500)};
$('saveText').onclick=()=>save(currentText()+'\n','text/plain;charset=utf-8','ocr-result.txt');
</script></html>