"""RAP demo UI: PDF -> assets -> model HTML -> rendered poster. Pipeline unchanged; UI redesigned 2026-10-15."""
import json
import re
import tempfile
import time
from pathlib import Path
import gradio as gr
import torch
import spaces
from pdf_pipeline import prepare, chat, CANVASES, DEFAULT_INSTRUCTION
from paper_sections import budget_body
from poster_render import render
ROOT = Path(__file__).resolve().parent
ARTIFACTS = Path(tempfile.mkdtemp(prefix='rap_demo_'))
WINDOW = 16384
MAX_STEPS = 10 # chained ZeroGPU steps per run
GPU_LADDER = (600, 300, 120, 60) # requested step length; ZeroGPU refuses what the visitor's quota cannot cover
QUOTA_HINT = (' — this 27B BF16 model needs the full 96 GB ZeroGPU (2x quota) and a poster takes roughly 15–25 min of quota. '
'Daily ZeroGPU quota: PRO 40 min, free account 5 min, anonymous 2 min, so sign in with a PRO account to finish a poster.')
CSS = '''
#hero .links { margin: 14px 0 0; font-size: 14px; opacity: .92 } #hero .links a { color: #fff; text-decoration: underline; text-underline-offset: 3px }
.gradio-container {max-width: 1360px !important; margin: 0 auto;}
#hero {padding: 26px 30px 22px; border-radius: 16px; margin-bottom: 6px;
background: linear-gradient(120deg, #1e1b4b 0%, #4c3d99 60%, #6d5bd0 100%); color: #fff;}
#hero h1 {margin: 0 0 6px; font-size: 30px; letter-spacing: .2px; color: #fff;}
#hero p {margin: 0; color: #e6e2ff; font-size: 15px;}
#hero a {color: #fff; text-decoration: underline;}
.badge {display: inline-block; margin: 10px 8px 0 0; padding: 3px 10px; border-radius: 999px;
background: rgba(255,255,255,.14); font-size: 12px; color: #fff;}
#status {min-height: 24px; font-size: 14px;}
#poster img {border-radius: 10px; box-shadow: 0 10px 30px rgba(20,16,60,.12);}
#examples .thumbnail-item {border-radius: 10px;}
footer {display: none !important;}
'''
def load_examples():
"""examples//: meta.json, paper.pdf, poster.png, poster.html, prompt.json (the exact benchmark prompt),
thumbnails/ (what the model sees) and assets/ (full-resolution crops for rendering)."""
import hashlib
out = []
for d in sorted((ROOT / 'examples').glob('*/')):
meta = d / 'meta.json'
if not meta.exists() or not (d / 'poster.png').exists() or not (d / 'paper.pdf').exists():
continue
m = json.loads(meta.read_text(encoding='utf-8'))
out.append({'dir': d, 'title': m.get('title', d.name), 'short': m.get('short', m.get('title', d.name))[:40], 'venue': m.get('venue', ''),
'paper_id': m.get('paper_id', d.name), 'instruction': m.get('instruction', DEFAULT_INSTRUCTION), 'spec': m.get('spec', ''),
'canvas': m.get('canvas', 'Landscape · 3:2'), 'columns': m.get('columns', 3), 'accent': m.get('accent', '#584287'),
'prompt': d / 'prompt.json' if (d / 'prompt.json').exists() else None,
'pdf_sha': hashlib.sha256((d / 'paper.pdf').read_bytes()).hexdigest()})
return out
def fileout(path):
return gr.update(value=str(path), visible=True) if path else gr.update(value=None, visible=False)
def build(model, processor):
def match_example(pdf, instruction, custom_spec):
"""The exact benchmark prompt is used when the PDF is one of the examples and instruction + spec are unchanged."""
import hashlib
try:
sha = hashlib.sha256(Path(pdf).read_bytes()).hexdigest()
except OSError:
return None
for ex in examples:
if ex['prompt'] and ex['pdf_sha'] == sha and instruction.strip() == ex['instruction'].strip() and custom_spec.strip() == ex['spec'].strip():
return ex
return None
def analyze_example(ex):
"""Reproduce a benchmark case: the stored prompt (paper text, asset list, thumbnails, instruction, layout spec)."""
d = ex['dir']; pr = json.loads((d / 'prompt.json').read_text(encoding='utf-8'))
system, user = pr['messages'][0]['content'], pr['messages'][1]['content']
parts = user.split(''); images = [str(d / im) for im in pr['images']]
content = []
for k, text in enumerate(parts):
if text: content.append({'type': 'text', 'text': text})
if k < len(images): content.append({'type': 'image', 'image': images[k]})
messages = [{'role': 'system', 'content': [{'type': 'text', 'text': system}]}, {'role': 'user', 'content': content}]
assets = []
for im in pr['images']:
aid = Path(im).stem; f = d / 'assets' / f'{aid}.png'
kind = aid.rsplit('_', 1)[0]
assets.append({'id': aid, 'kind': kind, 'file': str(f if f.exists() else d / im), 'thumb': str(d / im), 'page': None})
w, h = (int(v) for v in re.match(r'(\d+)x(\d+)', ex['canvas']).groups()) if re.match(r'\d+x\d+', ex['canvas']) else CANVASES.get(ex['canvas'], (2400, 1600))
job = Path(tempfile.mkdtemp(prefix='example_', dir=ARTIFACTS))
return {'title': ex['title'], 'authors': '', 'assets': assets, 'instruction': ex['instruction'], 'spec': ex['spec'], 'canvas': [w, h],
'messages_pre': messages, 'job': str(job), 'example': ex['paper_id'], 'sections': [], 'body': ''}
def analyze(pdf, instruction, canvas, accent, columns, custom_spec):
ex = match_example(pdf, instruction, custom_spec)
if ex is not None:
p = analyze_example(ex)
inputs = processor.apply_chat_template(chat(p), tokenize=True, add_generation_prompt=True, enable_thinking=False, return_dict=True, return_tensors='pt')
p['input_tokens'] = int(inputs['input_ids'].shape[1]); p['continuation_ids'] = []; p['raw'] = ''; p['done'] = False
return p
job = Path(tempfile.mkdtemp(prefix='paper_', dir=ARTIFACTS))
p = prepare(pdf, job, instruction, canvas, accent, columns, custom_spec)
budget = 22000
while True:
inputs = processor.apply_chat_template(chat(p), tokenize=True, add_generation_prompt=True,
enable_thinking=False, return_dict=True, return_tensors='pt')
n = inputs['input_ids'].shape[1]
if n <= 10000:
break
if budget <= 3500:
raise ValueError('The paper and its figures exceed the demo input budget.')
budget = max(3500, int(budget * .8))
p['body'], _ = budget_body(p['sections'], total=budget, abstract_missing=True)
p['input_tokens'] = n; p['continuation_ids'] = []; p['raw'] = ''; p['done'] = False
return p
def gpu_seconds(p):
return int(p.get('gpu_seconds', 120))
@spaces.GPU(size='xlarge', duration=gpu_seconds) # 27B BF16 needs the full 96 GB GPU (2x quota)
def gpu_chunk(p):
budget = max(30, gpu_seconds(p) - 90) # leave room for moving the weights to the GPU and the prefill
inputs = processor.apply_chat_template(chat(p), tokenize=True, add_generation_prompt=True,
enable_thinking=False, return_dict=True, return_tensors='pt')
prefix = p.get('continuation_ids', [])
if prefix:
inputs['input_ids'] = torch.cat([inputs['input_ids'], torch.tensor([prefix], dtype=torch.long)], dim=1)
inputs['attention_mask'] = torch.ones_like(inputs['input_ids'])
length = int(inputs['input_ids'].shape[1])
remaining = WINDOW - length
if remaining <= 0:
raise ValueError('The 16K context budget has been exhausted.')
inputs = inputs.to('cuda')
start = time.monotonic()
with torch.inference_mode():
output = model.generate(**inputs, max_new_tokens=min(8192, remaining), max_time=budget, do_sample=False,
use_cache=True, temperature=None, top_p=None, top_k=None, repetition_penalty=1.1)
new_ids = output[0, length:].detach().cpu().tolist()
all_ids = prefix + new_ids
text = processor.decode(all_ids, skip_special_tokens=True)
eos = model.generation_config.eos_token_id
eos = eos if isinstance(eos, list) else [eos]
complete = bool(re.search(r'