22
33Writes config.PAPERS_PATH: a list of {id, title, authors, abstract, pdf_link, forum_link,
44keywords, tldr, area, decision, track, site} in listing order. CVF publishes no keywords,
5- TL;DR, primary area or presentation type, so those fields stay empty and decision is "other".
5+ TL;DR, primary area or presentation type. Official conference program pages supply oral and
6+ spotlight assignments; every remaining paper uses config.DEFAULT_PRESENTATION.
67"""
8+ import argparse
79import json
810import os
911import re
1012import sys
1113import threading
1214import time
15+ import unicodedata
1316from concurrent .futures import ThreadPoolExecutor , as_completed
1417from urllib .parse import urljoin
1518
@@ -82,6 +85,74 @@ def parse_paper_html(html, url):
8285 return to_record (title , authors , abstract , urljoin (url , pdf_a ["href" ]))
8386
8487
88+ def normalize_match_text (value ):
89+ """Normalize typography while retaining the words used to identify a paper."""
90+ value = unicodedata .normalize ("NFKC" , value ).casefold ()
91+ value = re .sub (r"[‐‑‒–—−]" , "-" , value )
92+ return " " .join (re .sub (r"[^\w]+" , " " , value ).split ())
93+
94+
95+ def parse_presentation_html (html ):
96+ """Return title and ordered authors from a CVPR program event page."""
97+ soup = BeautifulSoup (html , "html.parser" )
98+ entries = []
99+ for card in soup .select (".event-card" ):
100+ title = card .select_one ("h3.event-title a" )
101+ authors = card .select_one (".event-speakers" )
102+ if title is not None :
103+ entries .append ({
104+ "title" : " " .join (title .get_text (" " , strip = True ).split ()),
105+ "authors" : " " .join (authors .get_text (" " , strip = True ).split ()) if authors else "" ,
106+ })
107+ return entries
108+
109+
110+ def fetch_presentations ():
111+ listings = {}
112+ for presentation , url in config .PRESENTATION_URLS .items ():
113+ resp = requests .get (url , timeout = 30 )
114+ resp .raise_for_status ()
115+ listings [presentation ] = parse_presentation_html (resp .text )
116+ if not listings [presentation ]:
117+ raise ValueError (f"no { presentation } papers found at { url } " )
118+ return listings
119+
120+
121+ def enrich_presentations (papers , listings ):
122+ """Assign configured presentation types by title, with ordered authors as fallback."""
123+ by_title = {}
124+ by_authors = {}
125+ for index , paper in enumerate (papers ):
126+ by_title .setdefault (normalize_match_text (paper ["title" ]), []).append (index )
127+ by_authors .setdefault (normalize_match_text (paper ["authors" ]), []).append (index )
128+
129+ assigned = {}
130+ for presentation , entries in listings .items ():
131+ seen = set ()
132+ for entry in entries :
133+ matches = by_title .get (normalize_match_text (entry ["title" ]), [])
134+ if len (matches ) != 1 and entry ["authors" ]:
135+ matches = by_authors .get (normalize_match_text (entry ["authors" ]), [])
136+ if len (matches ) != 1 :
137+ raise ValueError (
138+ f"{ presentation } paper did not match one record: { entry ['title' ]!r} "
139+ )
140+ index = matches [0 ]
141+ if index in seen :
142+ raise ValueError (f"duplicate { presentation } paper: { entry ['title' ]!r} " )
143+ if index in assigned :
144+ raise ValueError (
145+ f"paper has both { assigned [index ]} and { presentation } presentations: "
146+ f"{ papers [index ]['title' ]!r} "
147+ )
148+ seen .add (index )
149+ assigned [index ] = presentation
150+
151+ for index , paper in enumerate (papers ):
152+ paper ["decision" ] = assigned .get (index , config .DEFAULT_PRESENTATION )
153+ return {presentation : len (entries ) for presentation , entries in listings .items ()}
154+
155+
85156def fetch_with_retry (href ):
86157 url = config .CVF_URL + href
87158 delay = 1.0
@@ -99,7 +170,7 @@ def fetch_with_retry(href):
99170 return url , {"__error__" : str (last_exc )}
100171
101172
102- def main ():
173+ def scrape_all ():
103174 print (f"Fetching listing: { config .LISTING_URL } " )
104175 hrefs = fetch_listing_hrefs ()
105176 n_expected = len (hrefs )
@@ -133,6 +204,31 @@ def main():
133204 # Listing order, not id order: the committed embeddings follow this order.
134205 papers = [results_by_href [href ] for href in hrefs ]
135206
207+ return papers
208+
209+
210+ def main ():
211+ parser = argparse .ArgumentParser (description = __doc__ .splitlines ()[0 ])
212+ parser .add_argument (
213+ "--enrich-presentations" ,
214+ action = "store_true" ,
215+ help = "update presentation types in the committed paper data without rescraping papers" ,
216+ )
217+ args = parser .parse_args ()
218+
219+ if args .enrich_presentations :
220+ with open (config .PAPERS_PATH , encoding = "utf-8" ) as f :
221+ papers = json .load (f )
222+ else :
223+ papers = scrape_all ()
224+
225+ print ("Fetching presentation types from the official conference program." )
226+ counts = enrich_presentations (papers , fetch_presentations ())
227+ counts [config .DEFAULT_PRESENTATION ] = sum (
228+ paper ["decision" ] == config .DEFAULT_PRESENTATION for paper in papers
229+ )
230+ print ("Presentation types: " + ", " .join (f"{ key } ={ value } " for key , value in counts .items ()))
231+
136232 assert len ({p ["id" ] for p in papers }) == len (papers ), "duplicate paper ids"
137233 for p in papers :
138234 assert p ["title" ], f"empty title for paper: { p } "
0 commit comments