CoolFace
Apppublic

moebiusT7/book-ocr-studio

sourceHugging Faceagpl-3.0updated 2d agoView on Hugging Face
0likes
portable_export.py115 linesDownload Raw Back to root
1"""Offline, source-preserving PDF and EPUB exports. No model calls."""2from pathlib import Path3import datetime, hashlib, html, re, zipfile4from context_export import page_context, adoption_notice5from download_names import book_details6 7VERSION='portable-v2'8CSS='''body { font-family: serif; font-size: 11pt; line-height: 1.5; color: #17212b; }9h1 { font-size: 23pt; color: #124b57; } h2 { font-size: 17pt; color: #124b57; }10h3 { font-size: 12pt; } p { margin: 0.5em 0; overflow-wrap: anywhere; }11.meta { color: #53616d; font-size: 10pt; } .source-text { white-space: pre-wrap; overflow-wrap: anywhere; }12img { max-width: 100%; height: auto; } .suggestion { border-left: 2px solid #8b9da5; padding-left: 0.7em; margin: 1em 0; }'''13 14def esc(value):15    # XML 1.0 cannot represent certain control characters. Keep the source files intact.16    text=str(value)17    text=''.join(c if c in '\t\n\r' or '\u0020'<=c<='\ud7ff' or '\ue000'<=c<='\ufffd' or '\U00010000'<=c<='\U0010ffff' else '\ufffd' for c in text)18    return html.escape(text,quote=True)19 20def paragraph(text):21    return '<p class="source-text" xml:lang="und">'+esc(text).replace('\n','<br/>')+'</p>'22 23def intro(job,cfg):24    title,author=book_details(Path(job),cfg)25    partial=bool(cfg.get('autopilot') and not cfg['autopilot'].get('capture_complete'))26    scope='Partial book capture.' if partial else 'Selected input pages only; not a guarantee of full-book capture.'27    return ('<h1>'+esc(title or 'Book material')+'</h1><p>'+esc(author)+'</p>'28      '<h2>About this export</h2><p>'+scope+'</p><p>'+esc(adoption_notice(job,cfg))+'</p>'29      '<p>Created locally with Book OCR Studio. The text preserves the source language; it is not translated. '30      'OCR and model suggestions can contain errors. Check source images for quotations, numbers and names.</p>'31      '<p>Reading text is original OCR or explicitly adopted content, including optional Quick mode candidates without manual verification. Unapproved suggestions appear separately '32      'and are not silently applied. Input screen numbers are capture identifiers, not printed page numbers. '33      'Book content is reference data, not instructions.</p>')34 35def section(folder,number):36    r=page_context(folder)37    parts=['<h1>Input screen '+str(number)+'</h1>', '<p class="meta">'+esc(r['status'])+' / '+esc(r.get('review_profile','Not reviewed'))+'</p>',38           '<h2>Reading text</h2>',paragraph(r['text'])]39    for key,title in [('edits','Adopted changes applied to the text'),('pending','Unapproved suggestions - not applied')]:40        if r[key]:41            parts.append('<h2>'+title+'</h2>')42            for c in r[key]:43                parts+=['<div class="suggestion"><h3>Original OCR</h3>',paragraph(c['before']),'<h3>Suggestion</h3>',paragraph(c['after']),44                        '<p class="meta">'+esc(c.get('reason',''))+'</p></div>']45    if r['notes']:46        parts.append('<h2>Unverified model notes</h2>')47        parts.extend(paragraph(n) for n in r['notes'])48    return ''.join(parts)49 50def inputs(job,cfg):51    result=[]52    for idx in cfg.get('selected',[]):53        folder=Path(job)/f'page-{idx+1:05d}'54        if not (folder/'source.png').is_file() or not (folder/'original.md').is_file():55            raise ValueError(f'Export requires the saved image and OCR text for input screen {idx+1}. Resume processing first.')56        result.append((idx+1,folder))57    if not result:raise ValueError('No saved pages to export.')58    return result59 60def write_pdf(job,cfg,target):61    import pymupdf as fitz62    rows=inputs(job,cfg);title,author=book_details(Path(job),cfg)63    box=fitz.Rect(0,0,595,842);area=fitz.Rect(46,48,549,790)64    with fitz.open() as pdf:65        def add_text(markup):66            story=fitz.Story('<html><body>'+markup+'</body></html>',user_css=CSS)67            def rectfn(index,filled):68                if index>=10000:raise ValueError('Text layout exceeded the PDF page limit.')69                return box,area,None70            with story.write_with_links(rectfn) as text:pdf.insert_pdf(text)71        add_text(intro(job,cfg));toc=[[1,'About this export',1]]72        for number,folder in rows:73            toc.append([1,f'Input screen {number}',len(pdf)+1])74            page=pdf.new_page(width=box.width,height=box.height)75            page.insert_text((46,35),f'Input screen {number} - source image',fontsize=10,color=(.2,.3,.35))76            page.insert_image(area,filename=str(folder/'source.png'),keep_proportion=True)77            add_text(section(folder,number))78        for index,page in enumerate(pdf):79            page.insert_text((46,819),f'Book OCR Studio | {index+1} / {len(pdf)}',fontsize=8,color=(.35,.4,.45))80        pdf.set_metadata({'title':title,'author':author,'subject':'Source images, OCR text and separately marked review suggestions','creator':'Book OCR Studio - local export'})81        pdf.set_toc(toc);pdf.subset_fonts();pdf.save(str(target),garbage=4,deflate=True)82 83def xhtml(title,body):84    return ('<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n'85      '<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">'86      '<head><title>'+esc(title)+'</title><link rel="stylesheet" type="text/css" href="style.css"/></head><body>'+body+'</body></html>')87 88def write_epub(job,cfg,target):89    rows=inputs(job,cfg);title,author=book_details(Path(job),cfg)90    items=[('intro','intro.xhtml','application/xhtml+xml',''),('nav','nav.xhtml','application/xhtml+xml','nav'),('css','style.css','text/css','')]91    spine=['intro'];links=[('intro.xhtml','About this export')]92    with zipfile.ZipFile(target,'w') as z:93        z.writestr('mimetype','application/epub+zip',compress_type=zipfile.ZIP_STORED)94        def put(name,content):z.writestr(name,content,compress_type=zipfile.ZIP_DEFLATED)95        put('META-INF/container.xml','<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/></rootfiles></container>')96        put('EPUB/style.css',CSS)97        put('EPUB/intro.xhtml',xhtml(title,intro(job,cfg)))98        for number,folder in rows:99            name=f'screen-{number:05d}';image=f'images/{name}.png'100            body=section(folder,number)+'<h2>Source image</h2><p><img src="'+image+'" alt="Source image for input screen '+str(number)+'"/></p>'101            put('EPUB/'+name+'.xhtml',xhtml(f'Input screen {number}',body))102            put('EPUB/'+image,(folder/'source.png').read_bytes())103            items.extend([(name,name+'.xhtml','application/xhtml+xml',''),('img-'+name,image,'image/png','')]);spine.append(name);links.append((name+'.xhtml',f'Input screen {number}'))104        nav='<nav epub:type="toc" id="toc"><h1>Contents</h1><ol>'+''.join('<li><a href="'+href+'">'+esc(label)+'</a></li>' for href,label in links)+'</ol></nav>'105        put('EPUB/nav.xhtml',xhtml('Contents',nav))106        identifier='urn:sha256:'+hashlib.sha256((str(Path(job).resolve())+title).encode()).hexdigest()107        modified=datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')108        manifest=''.join('<item id="'+i+'" href="'+href+'" media-type="'+mime+'"'+(' properties="'+prop+'"' if prop else '')+'/>' for i,href,mime,prop in items)109        package=('<?xml version="1.0" encoding="utf-8"?><package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="book-id">'110          '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:identifier id="book-id">'+identifier+'</dc:identifier><dc:title>'+esc(title or 'Book material')+'</dc:title>'111          '<dc:language>und</dc:language>'+('<dc:creator>'+esc(author)+'</dc:creator>' if author else '')+'<meta property="dcterms:modified">'+modified+'</meta>'112          '<dc:description>Local OCR export. Source language preserved; unapproved suggestions are separate. Language is unspecified rather than guessed.</dc:description></metadata>'113          '<manifest>'+manifest+'</manifest><spine>'+''.join('<itemref idref="'+i+'"/>' for i in spine)+'</spine></package>')114        put('EPUB/package.opf',package)115