CoolFace
Apppublic

moebiusT7/book-ocr-studio

sourceHugging Faceagpl-3.0updated 2d agoView on Hugging Face
0likes
reader_detection.py81 linesDownload Raw Back to root
1"""Inspect rendered reader geometry and explicitly labelled page navigation."""2import math3 4SCAN=r'''() => {5 const visible = el => {const r=el.getBoundingClientRect(),s=getComputedStyle(el);return r.width>0&&r.height>0&&s.display!=='none'&&s.visibility!=='hidden';};6 const surfaces=[...document.querySelectorAll('canvas,img')].filter(visible).map(el=>{7 const r=el.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,tag:el.tagName};8 }).filter(r=>r.width>=200&&r.height>=200);9 const next=[...document.querySelectorAll('button,[role="button"],a')].filter(visible).map(el=>{10 const name=[el.getAttribute('aria-label'),el.getAttribute('title'),el.textContent].filter(Boolean).join(' ').trim();const r=el.getBoundingClientRect();11 return {name:name.slice(0,160),x:r.x+r.width/2,y:r.y+r.height/2,disabled:el.disabled||el.getAttribute('aria-disabled')==='true'};12 }).filter(b=>!b.disabled&&(/next\s*page|次のページ|次ページ|ページを進む|ページをめくる/i.test(b.name)));13 return {surfaces,next,width:innerWidth,height:innerHeight};14}'''15 16def detect(page):17    view=page.viewport_size;surfaces=[];buttons=[]18    for frame in page.frames:19        try:20            info=frame.evaluate(SCAN);ox=oy=0;sx=sy=121            if frame!=page.main_frame:22                box=frame.frame_element().bounding_box()23                if not box:continue24                ox,oy=box['x'],box['y'];sx=box['width']/info['width'];sy=box['height']/info['height']25            for item in info['surfaces']:26                item.update(x=ox+item['x']*sx,y=oy+item['y']*sy,width=item['width']*sx,height=item['height']*sy)27                if item['x']<view['width'] and item['y']<view['height'] and item['x']+item['width']>0 and item['y']+item['height']>0:surfaces.append(item)28            for b in info['next']:29                b.update(x=ox+b['x']*sx,y=oy+b['y']*sy)30                if 0<=b['x']<view['width'] and 0<=b['y']<view['height']:buttons.append(b)31        except Exception:continue32    if not surfaces:return dict(selection=None,reason='Could not identify the text area. Refresh the reader or select it manually.',clipped=False)33    largest=max(s['width']*s['height'] for s in surfaces)34    substantial=[s for s in surfaces if s['width']*s['height']>=largest*.45]35    x=min(s['x'] for s in substantial);y=min(s['y'] for s in substantial)36    right=max(s['x']+s['width'] for s in substantial);bottom=max(s['y']+s['height'] for s in substantial)37    clipped=x < -2 or y < -2 or right>view['width']+2 or bottom>view['height']+238    clip=dict(x=max(0,math.floor(x)),y=max(0,math.floor(y)),width=0,height=0)39    clip['width']=min(view['width'],math.ceil(right))-clip['x'];clip['height']=min(view['height'],math.ceil(bottom))-clip['y']40    # Ambiguous navigation stays manual; never silently guess RTL vs LTR.41    unique={(round(b['x']),round(b['y'])):b for b in buttons}42    nxt=list(next(iter(unique))) if len(unique)==1 else None43    return dict(selection=dict(clip=clip,next=nxt),reason='Detected the text area and page navigation.' if nxt else 'Detected the text area. Select the next-page arrow in the image.',clipped=clipped,surface_count=len(substantial))44 45def fit_and_detect(page):46    # Browser keyboard zoom is not reliable through automation. Apply reversible layout zoom47    # to the document root, then re-measure the rendered surfaces after every change.48    page.bring_to_front()49    page.evaluate("() => {document.documentElement.style.zoom='1';}")50    page.wait_for_timeout(400)51    result=detect(page);steps=0;zoom=1.052    while result['clipped'] and steps<4:53        zoom*=.854        page.evaluate("z => {document.documentElement.style.zoom=String(z);window.dispatchEvent(new Event('resize'));}",zoom)55        page.wait_for_timeout(500);steps+=1;result=detect(page)56    result['zoom_out_steps']=steps;result['layout_zoom']=zoom57    if result['clipped']:result['reason']='The whole page does not fit yet. Choose the full-page view in Kindle display settings.'58    return result59 60 61from contextlib import contextmanager62 63@contextmanager64def clean_reader_capture(page):65    """Temporarily remove observed Kindle overlay chrome; preserve position text."""66    token = page.evaluate("""() => {67      const elements = [...document.querySelectorAll('#reader-header, #kr-scrubber-bar, ion-footer.reader-footer, .bookmark[role="checkbox"]')];68      const saved = elements.map(e => ({selector: e.id ? '#'+e.id : e.tagName.toLowerCase()+'.'+(e.classList.contains('reader-footer')?'reader-footer':'bookmark'), value:e.style.getPropertyValue('opacity'), priority:e.style.getPropertyPriority('opacity')}));69      elements.forEach(e => e.style.setProperty('opacity','0','important'));70      return saved;71    }""")72    try:73        page.evaluate("() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))")74        yield75    finally:76        page.evaluate("""saved => {for(const item of saved) {77          const e=document.querySelector(item.selector);if(!e)continue;78          if(item.value)e.style.setProperty('opacity',item.value,item.priority);79          else e.style.removeProperty('opacity');80        }}""", token)81