hanhaonan02 Claude Fable 5.1 commited on
Commit
033094c
·
1 Parent(s): 77e6466

One request per ZeroGPU step; keep small-caps titles intact

Browse files

- The ZeroGPU proxy token is bound to the HTTP request; parsing plus several 120 s steps in one
request failed with 'Expired ZeroGPU proxy token'. Generate is now a chain: /analyze (CPU) then
up to 10 chained /step events, one GPU step each; Continue re-enters the chain.
- Title extraction joins touching spans and takes whole title lines, so ICLR small-caps titles no
longer collapse to initials; falls back to PDF metadata when the result looks broken.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +2 -0
  2. demo_ui.py +53 -38
  3. pdf_pipeline.py +16 -5
README.md CHANGED
@@ -21,6 +21,8 @@ and writes the poster as standalone HTML; the Space renders it to PNG.
21
  - Model: RL checkpoint-240, BF16, no quantization, 16K context.
22
  - Generation runs in ZeroGPU steps of up to 120 s; a full poster usually takes 3–6 steps and the
23
  demo chains them automatically. Sign in to Hugging Face to use your GPU quota.
 
 
24
  - The examples on the page were generated by the same checkpoint with the default instruction and settings.
25
  - Research demo: scanned PDFs are not supported, uploads are temporary, and every statement,
26
  number and caption should be checked before a poster is used.
 
21
  - Model: RL checkpoint-240, BF16, no quantization, 16K context.
22
  - Generation runs in ZeroGPU steps of up to 120 s; a full poster usually takes 3–6 steps and the
23
  demo chains them automatically. Sign in to Hugging Face to use your GPU quota.
24
+ - API: call `/analyze` once (PDF + settings), then `/step` repeatedly until the status starts with
25
+ `Done`, `Paused` or `Stopped`; each step is its own request so the ZeroGPU token stays valid.
26
  - The examples on the page were generated by the same checkpoint with the default instruction and settings.
27
  - Research demo: scanned PDFs are not supported, uploads are temporary, and every statement,
28
  number and caption should be checked before a poster is used.
demo_ui.py CHANGED
@@ -14,7 +14,7 @@ from poster_render import render
14
  ROOT = Path(__file__).resolve().parent
15
  ARTIFACTS = Path(tempfile.mkdtemp(prefix='rap_demo_'))
16
  WINDOW = 16384
17
- MAX_CHUNKS = 8
18
 
19
  CSS = '''
20
  .gradio-container {max-width: 1360px !important; margin: 0 auto;}
@@ -93,51 +93,59 @@ def build(model, processor):
93
  del inputs, output
94
  return result
95
 
96
- def generate_loop(p, gallery, progress):
97
- """ZeroGPU chunks until the HTML is complete; yields (state, status, png, html, zip, gallery, spec, raw, continue-button)."""
98
- tokens = ''
99
- for k in range(1, MAX_CHUNKS + 1):
100
- progress(min(.15 + .1 * k, .9), desc=f'Generating poster HTML (step {k})…')
101
- try:
102
- r = gpu_chunk(p)
103
- except Exception as e: # noqa: BLE001 (quota exhausted, timeout, …)
104
- yield (p, f"Paused after step {k - 1}: {str(e)[:200]}", None, None, None, gallery, p['spec'], p['raw'],
105
- gr.update(visible=bool(p.get('continuation_ids'))))
106
- return
107
- p = dict(p, continuation_ids=r['ids'], raw=r['raw'], done=r['done'])
108
- tokens = f"{len(r['ids']):,} tokens · {r['new_tokens'] / max(r['seconds'], .01):.0f} tok/s"
109
- if r['complete'] or p['done']:
110
- break
111
- yield (p, f"Step {k} done · {tokens} · continuing…", None, None, None, gallery, p['spec'], p['raw'], gr.update(visible=False))
112
- progress(.95, desc='Rendering…')
113
- try:
114
- png, html, archive, meta = render(p['raw'], p)
115
- except Exception as e: # noqa: BLE001
116
- yield (p, f"The model stopped before a complete poster ({str(e)[:160]}).", None, None, None, gallery, p['spec'], p['raw'],
117
- gr.update(visible=not p.get('done')))
118
- return
119
- note = '' if meta['complete_html'] else ' · output incomplete'
120
- yield (dict(p, done=True), f"Done · {tokens}{note}", png, fileout(html), fileout(archive), gallery, p['spec'], p['raw'], gr.update(visible=False))
121
 
122
- def run(pdf, instruction, canvas, accent, columns, custom_spec, progress=gr.Progress()):
123
- """One click: parse the PDF, then generate until the poster is complete."""
124
  if not pdf:
125
  raise gr.Error('Upload a paper PDF first.')
126
- progress(.05, desc='Reading the paper (LayoutLMv3 on CPU)…')
127
  try:
128
  p = analyze(pdf, instruction, canvas, accent, columns, custom_spec)
129
  except (ValueError, RuntimeError) as e:
130
  raise gr.Error(str(e)) from None
131
- gallery = [(a['thumb'], f"{a['id']} · page {a['page']}") for a in p['assets']]
132
- yield (p, f"Parsed **{p['title'][:90]}** · {len(p['assets'])} figures/tables found · generating…",
133
- None, None, None, gallery, p['spec'], '', gr.update(visible=False))
134
- yield from generate_loop(p, gallery, progress)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- def cont(p, progress=gr.Progress()):
137
  if not p or p.get('done'):
138
  raise gr.Error('Nothing to continue. Click Generate poster.')
139
- gallery = [(a['thumb'], f"{a['id']} · page {a['page']}") for a in p['assets']]
140
- yield from generate_loop(p, gallery, progress)
141
 
142
  examples = load_examples()
143
  def pick_example(evt: gr.SelectData):
@@ -188,8 +196,15 @@ def build(model, processor):
188
  '[Source and third-party notices](https://huggingface.co/spaces/VincentHancoder/RAP/tree/main).')
189
  state = gr.State()
190
  outs = [state, status, poster, html_download, bundle, gallery, spec_view, code, cont_btn]
191
- run_btn.click(run, [pdf, instruction, canvas, accent, columns, custom_spec], outs, api_name='generate', concurrency_limit=1)
192
- cont_btn.click(cont, [state], outs, api_name='continue', concurrency_limit=1)
 
 
 
 
 
 
 
193
  if examples:
194
  ex_gallery.select(pick_example, None, [pdf, instruction, canvas, accent, columns, status, poster, html_download, bundle], api_name=False)
195
  return demo, ARTIFACTS
 
14
  ROOT = Path(__file__).resolve().parent
15
  ARTIFACTS = Path(tempfile.mkdtemp(prefix='rap_demo_'))
16
  WINDOW = 16384
17
+ MAX_STEPS = 10 # ZeroGPU steps of <=120 s per run
18
 
19
  CSS = '''
20
  .gradio-container {max-width: 1360px !important; margin: 0 auto;}
 
93
  del inputs, output
94
  return result
95
 
96
+ HIDE = gr.update(value=None, visible=False)
97
+
98
+ def gallery_of(p):
99
+ return [(a['thumb'], f"{a['id']} · page {a['page']}") for a in p['assets']]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ def analyze_evt(pdf, instruction, canvas, accent, columns, custom_spec, progress=gr.Progress()):
102
+ """Request 1 of a run: parse the PDF on CPU. GPU steps run in their own requests (see step)."""
103
  if not pdf:
104
  raise gr.Error('Upload a paper PDF first.')
105
+ progress(.1, desc='Reading the paper (LayoutLMv3 on CPU, up to a few minutes)…')
106
  try:
107
  p = analyze(pdf, instruction, canvas, accent, columns, custom_spec)
108
  except (ValueError, RuntimeError) as e:
109
  raise gr.Error(str(e)) from None
110
+ p = dict(p, step=0, paused=False)
111
+ return (p, f"Parsed **{p['title'][:90]}** · {len(p['assets'])} figures/tables found · generating (step 1)…",
112
+ None, HIDE, HIDE, gallery_of(p), p['spec'], '', gr.update(visible=False))
113
+
114
+ def step(p, progress=gr.Progress()):
115
+ """One ZeroGPU step per request: the ZeroGPU proxy token is tied to the request and expires after a few minutes,
116
+ so a single long request (parse + several 120 s steps) fails with 'Expired ZeroGPU proxy token'."""
117
+ if not p or p.get('done') or p.get('paused'):
118
+ return tuple(gr.skip() for _ in range(9))
119
+ k = p.get('step', 0) + 1
120
+ gallery = gallery_of(p)
121
+ if k > MAX_STEPS:
122
+ return (dict(p, paused=True), f"Stopped after {MAX_STEPS} steps without a complete poster.", None, HIDE, HIDE,
123
+ gallery, p['spec'], p['raw'], gr.update(visible=True))
124
+ progress(min(.1 + .12 * k, .9), desc=f'Generating poster HTML (step {k})…')
125
+ try:
126
+ r = gpu_chunk(p)
127
+ except Exception as e: # noqa: BLE001 (quota exhausted, timeout, …)
128
+ return (dict(p, paused=True), f"Paused after step {k - 1}: {str(e)[:200]}", None, HIDE, HIDE,
129
+ gallery, p['spec'], p['raw'], gr.update(visible=bool(p.get('continuation_ids'))))
130
+ p = dict(p, continuation_ids=r['ids'], raw=r['raw'], done=r['done'], step=k)
131
+ tokens = f"{len(r['ids']):,} tokens · {r['new_tokens'] / max(r['seconds'], .01):.0f} tok/s"
132
+ if not (r['complete'] or p['done']):
133
+ return (p, f"Step {k} done · {tokens} · continuing (step {k + 1})…", None, HIDE, HIDE,
134
+ gallery, p['spec'], p['raw'], gr.update(visible=False))
135
+ progress(.95, desc='Rendering…')
136
+ try:
137
+ png, html, archive, meta = render(p['raw'], p)
138
+ except Exception as e: # noqa: BLE001
139
+ return (dict(p, paused=True), f"The model stopped before a complete poster ({str(e)[:160]}).", None, HIDE, HIDE,
140
+ gallery, p['spec'], p['raw'], gr.update(visible=not p.get('done')))
141
+ note = '' if meta['complete_html'] else ' · output incomplete'
142
+ return (dict(p, done=True), f"Done · {tokens}{note}", png, fileout(html), fileout(archive),
143
+ gallery, p['spec'], p['raw'], gr.update(visible=False))
144
 
145
+ def resume(p):
146
  if not p or p.get('done'):
147
  raise gr.Error('Nothing to continue. Click Generate poster.')
148
+ return dict(p, paused=False)
 
149
 
150
  examples = load_examples()
151
  def pick_example(evt: gr.SelectData):
 
196
  '[Source and third-party notices](https://huggingface.co/spaces/VincentHancoder/RAP/tree/main).')
197
  state = gr.State()
198
  outs = [state, status, poster, html_download, bundle, gallery, spec_view, code, cont_btn]
199
+ # Chain: one request for parsing, then one request per GPU step (fresh ZeroGPU token each time).
200
+ # API clients: call /analyze once, then /step until the status starts with "Done", "Paused" or "Stopped".
201
+ ev = run_btn.click(analyze_evt, [pdf, instruction, canvas, accent, columns, custom_spec], outs, api_name='analyze',
202
+ concurrency_limit=1)
203
+ for k in range(MAX_STEPS):
204
+ ev = ev.then(step, [state], outs, api_name='step' if k == 0 else False, concurrency_limit=1)
205
+ ev = cont_btn.click(resume, [state], [state], api_name=False)
206
+ for _ in range(MAX_STEPS):
207
+ ev = ev.then(step, [state], outs, api_name=False, concurrency_limit=1)
208
  if examples:
209
  ex_gallery.select(pick_example, None, [pdf, instruction, canvas, accent, columns, status, poster, html_download, bundle], api_name=False)
210
  return demo, ARTIFACTS
pdf_pipeline.py CHANGED
@@ -68,11 +68,22 @@ def prepare(pdf, job, instruction, canvas, accent, columns, custom_spec=''):
68
  chosen=select_for_prompt(registry['assets'],[],16,{'figure':10,'table':5,'formula':3})
69
  with fitz.open(pdf) as doc:
70
  first=doc[0].get_text('dict')
71
- spans=[s for b in first['blocks'] if 'lines' in b for l in b['lines']
72
- if abs(l.get('dir',(1,0))[1])<.1 for s in l['spans']
73
- if s['text'].strip() and not re.search(r'arxiv:|preprint|under review',s['text'],re.I)]
74
- largest=max((s['size'] for s in spans),default=0)
75
- title=clean_text(' '.join(s['text'] for s in spans if s['size']>=largest*.85 and s['bbox'][1]<doc[0].rect.height*.45))[:400]
 
 
 
 
 
 
 
 
 
 
 
76
  authors=clean_text(doc.metadata.get('author',''))[:500]
77
  asset_lines=[];assets=[]
78
  for a in chosen:
 
68
  chosen=select_for_prompt(registry['assets'],[],16,{'figure':10,'table':5,'formula':3})
69
  with fitz.open(pdf) as doc:
70
  first=doc[0].get_text('dict')
71
+ lines=[[s for s in l['spans'] if s['text'].strip() and not re.search(r'arxiv:|preprint|under review',s['text'],re.I)]
72
+ for b in first['blocks'] if 'lines' in b for l in b['lines'] if abs(l.get('dir',(1,0))[1])<.1]
73
+ lines=[l for l in lines if l]
74
+ largest=max((s['size'] for l in lines for s in l),default=0)
75
+ # whole lines whose biggest span is title-sized: keeps small-caps titles (ICLR) intact instead of 'B F G L'
76
+ def join_line(l): # small-caps titles come as 'B'+'AYESIAN' spans: no space when the spans touch
77
+ out=''
78
+ for prev,s in zip([None]+l[:-1],l):
79
+ gap=s['bbox'][0]-prev['bbox'][2] if prev else 0
80
+ out+=('' if prev and gap<s['size']*.15 and not out.endswith(' ') and not s['text'].startswith(' ') else ' ')+s['text']
81
+ return out
82
+ title=clean_text(' '.join(join_line(l) for l in lines if max(s['size'] for s in l)>=largest*.85
83
+ and l[0]['bbox'][1]<doc[0].rect.height*.45))[:400]
84
+ meta_title=clean_text(doc.metadata.get('title',''))
85
+ if meta_title and len(meta_title)>8 and (len(title)<8 or len(title.split())>3 and sum(len(w) for w in title.split())/len(title.split())<2.5):
86
+ title=meta_title[:400]
87
  authors=clean_text(doc.metadata.get('author',''))[:500]
88
  asset_lines=[];assets=[]
89
  for a in chosen: