JaceWei commited on
Commit
ddc016b
·
1 Parent(s): 09f6b85
Files changed (3) hide show
  1. app.py +8 -19
  2. pipeline.py +85 -15
  3. posterbuilder/convert.py +20 -11
app.py CHANGED
@@ -139,22 +139,10 @@ def _prepare_workspace(logs):
139
  # ---------------------
140
  # Helpers for new features (post-processing)
141
  # ---------------------
142
- def _parse_rgb(s):
143
- """Accepts '94,46,145' / '94 46 145' / '[94,46,145]' / '(94, 46, 145)' and returns (r,g,b) or None."""
144
- if s is None:
145
- return None
146
- if isinstance(s, (tuple, list)) and len(s) == 3:
147
- vals = s
148
- else:
149
- nums = re.findall(r"\d+", str(s))
150
- if len(nums) < 3:
151
- return None
152
- vals = nums[:3]
153
  try:
154
- r, g, b = (int(vals[0]), int(vals[1]), int(vals[2]))
155
- if any(v < 0 or v > 255 for v in (r, g, b)):
156
- return None
157
- return (r, g, b)
158
  except Exception:
159
  return None
160
 
@@ -504,7 +492,10 @@ iface = gr.Interface(
504
  file_types=["image"],
505
  ),
506
  gr.File(label="🧩 Optional: Conference Logo (replaces right_logo.png)", file_count="single", file_types=["image"]),
507
- gr.Textbox(label="🎨 Optional: Theme RGB (e.g., 94,46,145)", placeholder="94,46,145"),
 
 
 
508
  ],
509
  outputs=[
510
  gr.Textbox(label="🧾 Logs (8~10 minutes)", lines=30, max_lines=50),
@@ -512,9 +503,7 @@ iface = gr.Interface(
512
  ],
513
  title="🎓 Paper2Poster",
514
  description="""
515
- paper(https://arxiv.org/abs/2505.21497) | [GitHub](https://github.com/Paper2Poster/Paper2Poster) | project page (https://paper2poster.github.io/)
516
-
517
- # Paper2Poster
518
 
519
  Upload a paper, generate a poster for you.
520
  Each paper takes approximately **8–10 minutes**.
 
139
  # ---------------------
140
  # Helpers for new features (post-processing)
141
  # ---------------------
142
+ def _parse_rgb(hex):
 
 
 
 
 
 
 
 
 
 
143
  try:
144
+ hex = hex.lstrip('#')
145
+ return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4))
 
 
146
  except Exception:
147
  return None
148
 
 
492
  file_types=["image"],
493
  ),
494
  gr.File(label="🧩 Optional: Conference Logo (replaces right_logo.png)", file_count="single", file_types=["image"]),
495
+ gr.ColorPicker(
496
+ label="🎨 Theme Color (optional)",
497
+ value="#5E2E91" # default purple (94,46,145)
498
+ ),
499
  ],
500
  outputs=[
501
  gr.Textbox(label="🧾 Logs (8~10 minutes)", lines=30, max_lines=50),
 
503
  ],
504
  title="🎓 Paper2Poster",
505
  description="""
506
+ [paper](https://arxiv.org/abs/2505.21497) | [GitHub](https://github.com/Paper2Poster/Paper2Poster) | [project page] (https://paper2poster.github.io/)
 
 
507
 
508
  Upload a paper, generate a poster for you.
509
  Each paper takes approximately **8–10 minutes**.
pipeline.py CHANGED
@@ -21,9 +21,6 @@ P2P_ROOT = ROOT_DIR / "Paper2Poster"
21
  PB_ROOT = ROOT_DIR / "posterbuilder"
22
  sys.path.append(str(P2P_ROOT))
23
 
24
- print(f"🔒 Workspace ROOT_DIR = {ROOT_DIR}")
25
- print(f"🔒 This run is isolated under: {ROOT_DIR.resolve()}")
26
-
27
  def copy_folder(src_dir, dst_dir):
28
  src_dir = Path(src_dir)
29
  dst_dir = Path(dst_dir)
@@ -43,7 +40,7 @@ def safe_copy(src: Path, dst: Path):
43
  dst.parent.mkdir(parents=True, exist_ok=True)
44
  shutil.copy2(src, dst)
45
 
46
- def str2list(s):
47
  return [int(x) for x in s.split(',')]
48
 
49
  def run_paper2poster_content_build():
@@ -99,7 +96,13 @@ def _list_logo_files(logo_dir: Path):
99
  return files
100
 
101
  def _compose_logos_horizontally(logo_paths, out_path: Path, box_w=2000, box_h=476, gap=16):
102
- # (same as your original; omitted comments for brevity)
 
 
 
 
 
 
103
  imgs = []
104
  for p in logo_paths:
105
  p = Path(p)
@@ -109,27 +112,35 @@ def _compose_logos_horizontally(logo_paths, out_path: Path, box_w=2000, box_h=47
109
  if n == 0:
110
  raise RuntimeError("No logo images found.")
111
 
 
112
  widths = [im.width for im in imgs]
113
  heights = [im.height for im in imgs]
114
  sum_w = sum(widths)
115
  if sum_w <= 0:
116
  raise RuntimeError("All logo images have zero width.")
117
 
 
 
118
  total_gap = max(0, gap * (n - 1))
119
  if box_w <= total_gap:
120
  raise ValueError(f"box_w({box_w}) too small vs total gaps({total_gap}). Increase box_w or reduce gap.")
121
  s = (box_w - total_gap) / float(sum_w)
122
 
 
123
  resized = []
124
  scaled_widths = []
 
125
  for im, w, h in zip(imgs, widths, heights):
126
  nw = max(1, int(round(w * s)))
127
  nh = max(1, int(round(h * s)))
128
  resized.append(im.resize((nw, nh), Image.LANCZOS))
129
  scaled_widths.append(nw)
 
130
 
 
131
  current_sum_w = sum(scaled_widths)
132
  diff = (box_w - total_gap) - current_sum_w
 
133
  if diff != 0:
134
  order = sorted(range(n), key=lambda i: scaled_widths[i], reverse=(diff > 0))
135
  idx = 0
@@ -144,13 +155,14 @@ def _compose_logos_horizontally(logo_paths, out_path: Path, box_w=2000, box_h=47
144
  remaining -= 1
145
  idx += 1
146
 
 
147
  total_w = sum(scaled_widths) + total_gap
148
  assert total_w == box_w, f"width pack mismatch: got {total_w}, expect {box_w}"
149
  canvas_w = box_w
150
- canvas_h = max(im.height for im in resized)
151
 
152
- from PIL import Image as PILImage
153
- canvas = PILImage.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
154
  cur_x = 0
155
  for idx, im in enumerate(resized):
156
  y = (canvas_h - im.height) // 2
@@ -159,8 +171,13 @@ def _compose_logos_horizontally(logo_paths, out_path: Path, box_w=2000, box_h=47
159
  if idx != n - 1:
160
  cur_x += gap
161
 
 
162
  canvas.save(out_path, format="PNG")
163
- print(f" 🧩 Logos composed (width-locked) → {out_path.relative_to(ROOT_DIR)} (n={n}, final_size={canvas_w}x{canvas_h})")
 
 
 
 
164
 
165
  if __name__ == '__main__':
166
  parser = argparse.ArgumentParser(description='Paper2Video Generation Pipeline')
@@ -179,11 +196,11 @@ if __name__ == '__main__':
179
  args = parser.parse_args()
180
  print("start")
181
 
182
- # env
183
  os.environ["OPENAI_API_KEY"] = args.openai_key
184
  os.environ["GEMINI_API_KEY"] = args.gemini_key
185
 
186
- # clean and create run-local output
187
  output_dir = ROOT_DIR / "output"
188
  if output_dir.exists():
189
  print(f" 🧹 Clearing old output directory: {output_dir.relative_to(ROOT_DIR)}")
@@ -193,7 +210,9 @@ if __name__ == '__main__':
193
  (output_dir / "slide_imgs").mkdir(parents=True, exist_ok=True)
194
  print(" ✅ Created subfolders: latex_proj / poster_latex_proj / slide_imgs")
195
 
196
- # Step 0: arXiv download (same as your original)
 
 
197
  try:
198
  if args.arxiv_url:
199
  import requests, tarfile
@@ -236,13 +255,59 @@ if __name__ == '__main__':
236
  except Exception as e:
237
  print(f"❌ Step 0 failed: {e}")
238
 
239
- # Step 1.5: content build
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  try:
241
  run_paper2poster_content_build()
242
  except Exception as e:
243
  print(f"❌ Step 1.5 failed: {e}")
244
 
245
- # Step 2: build poster
 
 
246
  try:
247
  print("🧩 Step 2: Building poster ...")
248
  build_poster()
@@ -250,7 +315,9 @@ if __name__ == '__main__':
250
  except Exception as e:
251
  print(f"❌ Step 2 failed: {e}")
252
 
253
- # Step 3: export latex & apply template & logos
 
 
254
  try:
255
  src_lp = PB_ROOT / "latex_proj"
256
  dst_lp = ROOT_DIR / "output" / "poster_latex_proj"
@@ -277,13 +344,16 @@ if __name__ == '__main__':
277
  print("⚠️ template directory not found, skipping Step 3.5.")
278
 
279
  logos_out_dir = dst_lp / "logos"
 
280
  left_logo_path = logos_out_dir / "left_logo.png"
281
 
282
  if len(logo_files) == 1:
 
283
  im = Image.open(logo_files[0]).convert("RGBA")
284
  im.save(left_logo_path, format="PNG")
285
  print(f"🖼️ Single logo saved → {left_logo_path.relative_to(ROOT_DIR)}")
286
  else:
 
287
  _compose_logos_horizontally(logo_files, left_logo_path, box_w=2000, box_h=476, gap=16)
288
 
289
  print("✅ Step 3 done.")
 
21
  PB_ROOT = ROOT_DIR / "posterbuilder"
22
  sys.path.append(str(P2P_ROOT))
23
 
 
 
 
24
  def copy_folder(src_dir, dst_dir):
25
  src_dir = Path(src_dir)
26
  dst_dir = Path(dst_dir)
 
40
  dst.parent.mkdir(parents=True, exist_ok=True)
41
  shutil.copy2(src, dst)
42
 
43
+ def str2list(s):
44
  return [int(x) for x in s.split(',')]
45
 
46
  def run_paper2poster_content_build():
 
96
  return files
97
 
98
  def _compose_logos_horizontally(logo_paths, out_path: Path, box_w=2000, box_h=476, gap=16):
99
+ """
100
+ 宽度为硬约束:输出图像宽度必为 box_w(默认 2000px)。
101
+ 多 logo 按比例统一缩放,拼接后刚好占满 box_w(包含间距)。
102
+ 高度由比例自然决定,可能 < box_h,也可能 > box_h(甚至 > 2*box_h),不会再二次压缩。
103
+ 透明背景,输出 PNG。
104
+ """
105
+ # 读取图片
106
  imgs = []
107
  for p in logo_paths:
108
  p = Path(p)
 
112
  if n == 0:
113
  raise RuntimeError("No logo images found.")
114
 
115
+ # 原始总宽度(不含 gap);拼接总宽 = sum(w_i) + gap*(n-1)
116
  widths = [im.width for im in imgs]
117
  heights = [im.height for im in imgs]
118
  sum_w = sum(widths)
119
  if sum_w <= 0:
120
  raise RuntimeError("All logo images have zero width.")
121
 
122
+ # 计算统一缩放比例,使:sum(w_i * s) + gap*(n-1) == box_w
123
+ # => s = (box_w - gap*(n-1)) / sum_w
124
  total_gap = max(0, gap * (n - 1))
125
  if box_w <= total_gap:
126
  raise ValueError(f"box_w({box_w}) too small vs total gaps({total_gap}). Increase box_w or reduce gap.")
127
  s = (box_w - total_gap) / float(sum_w)
128
 
129
+ # 按统一比例缩放(四舍五入到整数像素,避免累计误差)
130
  resized = []
131
  scaled_widths = []
132
+ scaled_heights = []
133
  for im, w, h in zip(imgs, widths, heights):
134
  nw = max(1, int(round(w * s)))
135
  nh = max(1, int(round(h * s)))
136
  resized.append(im.resize((nw, nh), Image.LANCZOS))
137
  scaled_widths.append(nw)
138
+ scaled_heights.append(nh)
139
 
140
+ # 由于整数取整,可能出现总宽 != box_w - total_gap;对若干图微调 1px 以精确对齐
141
  current_sum_w = sum(scaled_widths)
142
  diff = (box_w - total_gap) - current_sum_w
143
+ # 按从宽到窄/从大到小顺序均匀分配像素误差
144
  if diff != 0:
145
  order = sorted(range(n), key=lambda i: scaled_widths[i], reverse=(diff > 0))
146
  idx = 0
 
155
  remaining -= 1
156
  idx += 1
157
 
158
+ # 计算最终尺寸
159
  total_w = sum(scaled_widths) + total_gap
160
  assert total_w == box_w, f"width pack mismatch: got {total_w}, expect {box_w}"
161
  canvas_w = box_w
162
+ canvas_h = max(im.height for im in resized) # 高度由比例自然决定(可能 > 2*box_h)
163
 
164
+ # 画布 & 居中摆放(垂直方向居中)
165
+ canvas = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
166
  cur_x = 0
167
  for idx, im in enumerate(resized):
168
  y = (canvas_h - im.height) // 2
 
171
  if idx != n - 1:
172
  cur_x += gap
173
 
174
+ # out_path.parent.mkdir(parents=True, exist_ok=True)
175
  canvas.save(out_path, format="PNG")
176
+ print(f" 🧩 Logos composed (width-locked) → {out_path.relative_to(ROOT_DIR)} "
177
+ f"(n={n}, final_size={canvas_w}x{canvas_h})")
178
+
179
+
180
+
181
 
182
  if __name__ == '__main__':
183
  parser = argparse.ArgumentParser(description='Paper2Video Generation Pipeline')
 
196
  args = parser.parse_args()
197
  print("start")
198
 
199
+ # ✅ 使用传入的 key 设置环境变量
200
  os.environ["OPENAI_API_KEY"] = args.openai_key
201
  os.environ["GEMINI_API_KEY"] = args.gemini_key
202
 
203
+ # 清空 output
204
  output_dir = ROOT_DIR / "output"
205
  if output_dir.exists():
206
  print(f" 🧹 Clearing old output directory: {output_dir.relative_to(ROOT_DIR)}")
 
210
  (output_dir / "slide_imgs").mkdir(parents=True, exist_ok=True)
211
  print(" ✅ Created subfolders: latex_proj / poster_latex_proj / slide_imgs")
212
 
213
+ # ================
214
+ # Step 0: Download from arXiv
215
+ # ================
216
  try:
217
  if args.arxiv_url:
218
  import requests, tarfile
 
255
  except Exception as e:
256
  print(f"❌ Step 0 failed: {e}")
257
 
258
+ # =========================
259
+ # Step 1: Slide Generation
260
+ # =========================
261
+ # try:
262
+ # print("🧩 Step 1: Generating Slides ...")
263
+ # slide_latex_path = path.join(args.paper_latex_root, "slides.tex")
264
+ # slide_image_dir = path.join(args.result_dir, 'slide_imgs')
265
+ # os.makedirs(slide_image_dir, exist_ok=True)
266
+
267
+ # start_time = time.time()
268
+ # prompt_path = "./Paper2Video/src/prompts/slide_beamer_prompt.txt"
269
+
270
+ # if args.if_tree_search:
271
+ # usage_slide, beamer_path = latex_code_gen(
272
+ # prompt_path=prompt_path,
273
+ # tex_dir=args.paper_latex_root,
274
+ # beamer_save_path=slide_latex_path,
275
+ # model_config_ll=get_agent_config(args.model_name_t),
276
+ # model_config_vl=get_agent_config(args.model_name_v),
277
+ # beamer_temp_name=args.beamer_templete_prompt
278
+ # )
279
+ # else:
280
+ # paper_latex_path = path.join(args.paper_latex_root, "main.tex")
281
+ # usage_slide = latex_code_gen(
282
+ # prompt_path=prompt_path,
283
+ # tex_dir=args.paper_latex_root,
284
+ # tex_path=paper_latex_path,
285
+ # beamer_save_path=slide_latex_path,
286
+ # model_config=get_agent_config(args.model_name_t)
287
+ # )
288
+ # beamer_path = slide_latex_path
289
+
290
+ # if not os.path.exists(beamer_path):
291
+ # raise FileNotFoundError(f"❌ Beamer PDF not found: {beamer_path}")
292
+
293
+ # slide_imgs = convert_from_path(beamer_path, dpi=400)
294
+ # for i, img in enumerate(slide_imgs):
295
+ # img.save(path.join(slide_image_dir, f"{i+1}.png"))
296
+ # print("✅ Step 1 done.")
297
+ # except Exception as e:
298
+ # print(f"❌ Step 1 failed: {e}")
299
+
300
+ # =========================
301
+ # Step 1.5: Poster2Poster 内容生成
302
+ # =========================
303
  try:
304
  run_paper2poster_content_build()
305
  except Exception as e:
306
  print(f"❌ Step 1.5 failed: {e}")
307
 
308
+ # =========================
309
+ # Step 2: Build Poster
310
+ # =========================
311
  try:
312
  print("🧩 Step 2: Building poster ...")
313
  build_poster()
 
315
  except Exception as e:
316
  print(f"❌ Step 2 failed: {e}")
317
 
318
+ # =========================
319
+ # Step 3: 导出 latex_proj & 处理 LOGO & 应用 template
320
+ # =========================
321
  try:
322
  src_lp = PB_ROOT / "latex_proj"
323
  dst_lp = ROOT_DIR / "output" / "poster_latex_proj"
 
344
  print("⚠️ template directory not found, skipping Step 3.5.")
345
 
346
  logos_out_dir = dst_lp / "logos"
347
+ # logos_out_dir.mkdir(parents=True, exist_ok=True)
348
  left_logo_path = logos_out_dir / "left_logo.png"
349
 
350
  if len(logo_files) == 1:
351
+ # 单图:拷贝并转成 PNG(以确保一致)
352
  im = Image.open(logo_files[0]).convert("RGBA")
353
  im.save(left_logo_path, format="PNG")
354
  print(f"🖼️ Single logo saved → {left_logo_path.relative_to(ROOT_DIR)}")
355
  else:
356
+ # 多图:拼接
357
  _compose_logos_horizontally(logo_files, left_logo_path, box_w=2000, box_h=476, gap=16)
358
 
359
  print("✅ Step 3 done.")
posterbuilder/convert.py CHANGED
@@ -82,30 +82,39 @@ def fix_latex_escaped_commands(s: str) -> str:
82
  s = s.replace("\\}", "}")
83
  return s
84
 
 
 
85
  def escape_text(s: str) -> str:
86
  if not s:
87
  return ""
88
 
89
- # ✅ 保护 $...$ 内的内容
90
- math = []
91
- def save_math(m):
92
- math.append(m.group(0))
93
- return f"__MATH{len(math)-1}__"
 
 
94
 
95
- s = re.sub(r"\${1,2}.*?\${1,2}", save_math, s)
96
 
97
- rep = {"&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_",
98
- "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}"}
 
 
 
 
99
  for k, v in rep.items():
100
  s = s.replace(k, v)
101
 
102
- # ✅ 恢复 math
103
- for i, block in enumerate(math):
104
- s = s.replace(f"__MATH{i}__", block)
105
 
106
  return s
107
 
108
 
 
109
  def soft_wrap_title_for_logo(title: str, first_limit=68, next_limit=72) -> str:
110
  if not title or len(title) <= first_limit: return title
111
  def break_at(s: str, limit: int):
 
82
  s = s.replace("\\}", "}")
83
  return s
84
 
85
+ import re
86
+
87
  def escape_text(s: str) -> str:
88
  if not s:
89
  return ""
90
 
91
+ # ✅ 提取并暂存 math block(不产生 MATH 字符)
92
+ math_blocks = []
93
+
94
+ def store_math(m):
95
+ math_blocks.append(m.group(0))
96
+ # 用对象占位,避免出现标记文本
97
+ return f"\0{len(math_blocks)-1}\0" # 不会出现在普通文本里
98
 
99
+ s = re.sub(r"\${1,2}.*?\${1,2}", store_math, s)
100
 
101
+ # 执行普通字符转义
102
+ rep = {
103
+ "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#",
104
+ "_": r"\_", "{": r"\{", "}": r"\}",
105
+ "~": r"\textasciitilde{}", "^": r"\textasciicircum{}",
106
+ }
107
  for k, v in rep.items():
108
  s = s.replace(k, v)
109
 
110
+ # ✅ 恢复 math(无 MATH 字符产生)
111
+ for i, block in enumerate(math_blocks):
112
+ s = s.replace(f"\0{i}\0", block)
113
 
114
  return s
115
 
116
 
117
+
118
  def soft_wrap_title_for_logo(title: str, first_limit=68, next_limit=72) -> str:
119
  if not title or len(title) <= first_limit: return title
120
  def break_at(s: str, limit: int):