Siyuan Hu commited on
Commit
3398479
Β·
1 Parent(s): b447602

feat(space): PDF compile + preview + debug; runtime to gradio; packages.txt with tectonic

Browse files
Files changed (3) hide show
  1. app.py +177 -18
  2. packages.txt +1 -0
  3. runtime.yaml +3 -1
app.py CHANGED
@@ -354,6 +354,143 @@ def render_overleaf_button(overleaf_b64):
354
  """
355
  return html
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  # =====================
358
  # Gradio pipeline function (ISOLATED)
359
  # =====================
@@ -371,7 +508,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
371
  POSTER_LATEX_DIR = WORK_DIR / "posterbuilder" / "latex_proj"
372
 
373
  _write_logs(LOG_PATH, logs)
374
- yield "\n".join(logs), None
375
 
376
  # ====== Validation: must upload LOGO ======
377
  if logo_files is None:
@@ -384,7 +521,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
384
  # msg = "❌ You must upload at least one institutional logo (multiple allowed)."
385
  # logs.append(msg)
386
  # _write_logs(LOG_PATH, logs)
387
- # yield "\n".join(logs), None
388
  # return
389
 
390
  # Save logos into run-local dir
@@ -398,7 +535,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
398
  saved_logo_paths.append(p)
399
  logs.append(f"🏷️ Saved {len(saved_logo_paths)} logo file(s) β†’ {LOGO_DIR.relative_to(WORK_DIR)}")
400
  _write_logs(LOG_PATH, logs)
401
- yield "\n".join(logs), None
402
 
403
  # ====== Handle uploaded PDF (optional) ======
404
  pdf_path = None
@@ -413,14 +550,14 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
413
  canonical_pdf = INPUT_DIR / "paper.pdf"
414
  shutil.copy(pdf_file.name, canonical_pdf)
415
  _write_logs(LOG_PATH, logs)
416
- yield "\n".join(logs), None
417
 
418
  # ====== Validate input source ======
419
  if not arxiv_url and not pdf_file:
420
  msg = "❌ Please provide either an arXiv link or upload a PDF file (choose one)."
421
  logs.append(msg)
422
  _write_logs(LOG_PATH, logs)
423
- yield "\n".join(logs), None
424
  return
425
 
426
  # ====== Build command (run INSIDE workspace) ======
@@ -441,7 +578,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
441
  logs.append("\n======= REAL-TIME LOG =======")
442
  logs.append(f"cwd = runs/{WORK_DIR.name}")
443
  _write_logs(LOG_PATH, logs)
444
- yield "\n".join(logs), None
445
 
446
  # ====== Run with REAL-TIME streaming, inside workspace ======
447
  try:
@@ -458,7 +595,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
458
  msg = f"❌ Pipeline failed to start: {e}"
459
  logs.append(msg)
460
  _write_logs(LOG_PATH, logs)
461
- yield "\n".join(logs), None
462
  return
463
 
464
  last_yield = time.time()
@@ -472,7 +609,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
472
  except Exception:
473
  pass
474
  _write_logs(LOG_PATH, logs)
475
- yield "\n".join(logs), None
476
  return
477
 
478
  line = process.stdout.readline()
@@ -483,7 +620,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
483
  now = time.time()
484
  if now - last_yield >= 0.3:
485
  last_yield = now
486
- yield "\n".join(logs), None
487
  elif process.poll() is not None:
488
  break
489
  else:
@@ -492,18 +629,18 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
492
  return_code = process.wait()
493
  logs.append(f"\nProcess finished with code {return_code}")
494
  _write_logs(LOG_PATH, logs)
495
- yield "\n".join(logs), None
496
 
497
  if return_code != 0:
498
  logs.append("❌ Process exited with non-zero status. See logs above.")
499
  _write_logs(LOG_PATH, logs)
500
- yield "\n".join(logs), None
501
  return
502
 
503
  except Exception as e:
504
  logs.append(f"❌ Error during streaming: {e}")
505
  _write_logs(LOG_PATH, logs)
506
- yield "\n".join(logs), None
507
  return
508
  finally:
509
  try:
@@ -526,7 +663,7 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
526
  msg = "❌ No output generated. Please check logs above."
527
  logs.append(msg)
528
  _write_logs(LOG_PATH, logs)
529
- yield "\n".join(logs), None
530
  return
531
 
532
  # ====== NEW: Post-processing (optional features) ======
@@ -541,13 +678,24 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
541
 
542
  # 3) Optional institutional logo -> left_logo.<ext>
543
  _apply_left_logo(OUTPUT_DIR, logo_files, logs)
 
544
 
545
  _write_logs(LOG_PATH, logs)
546
- yield "\n".join(logs), None
547
 
548
 
549
  _write_logs(LOG_PATH, logs)
550
- yield "\n".join(logs), None
 
 
 
 
 
 
 
 
 
 
551
 
552
  # ====== Zip output (run-local) ======
553
  try:
@@ -584,7 +732,9 @@ def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file,
584
 
585
  _write_logs(LOG_PATH, logs)
586
  yield "\n".join(logs), (
587
- str(ZIP_PATH) if ZIP_PATH.exists() else None
 
 
588
  ), render_overleaf_button(overleaf_zip_b64)
589
 
590
 
@@ -639,14 +789,23 @@ The framework builds upon [CAMEL-ai](https://github.com/camel-ai/camel).
639
  with gr.Column(scale=1):
640
  with gr.Accordion("Output", open=True):
641
  logs_out = gr.Textbox(label="🧾 Logs (8–10 minutes)", lines=30, max_lines=50)
 
642
  zip_out = gr.File(
643
  label="πŸ“¦ Download Results (.zip)",
644
  info="πŸ“Œ After uploading the ZIP to Overleaf, please select **XeLaTeX** as the compile engine.",
645
- )
 
 
 
646
  run_btn.click(
647
  fn=run_pipeline,
648
  inputs=[arxiv_in, pdf_in, key_in, inst_logo_in, conf_logo_in, theme_in],
649
- outputs=[logs_out, zip_out],
 
 
 
 
 
650
  )
651
 
652
  if __name__ == "__main__":
 
354
  """
355
  return html
356
 
357
+ def _compile_poster_pdf(OUTPUT_DIR: Path, logs):
358
+ """
359
+ Compile output/poster_latex_proj/poster_output.tex into a PDF using an
360
+ available TeX engine. Prefer 'tectonic', then 'lualatex', then 'xelatex',
361
+ then 'latexmk'. Returns Path to the PDF or None.
362
+ """
363
+ try:
364
+ proj_dir = OUTPUT_DIR / "poster_latex_proj"
365
+ tex_path = proj_dir / "poster_output.tex"
366
+ if not tex_path.exists():
367
+ logs.append(f"⚠️ LaTeX source not found: {tex_path.relative_to(OUTPUT_DIR)}")
368
+ return None
369
+
370
+ # Clean old PDFs
371
+ for cand in (proj_dir / "poster_output.pdf", proj_dir / "poster.pdf"):
372
+ try:
373
+ if cand.exists():
374
+ cand.unlink()
375
+ except Exception:
376
+ pass
377
+
378
+ import shutil as _sh
379
+ import subprocess as _sp
380
+
381
+ def _has(bin_name):
382
+ return _sh.which(bin_name) is not None
383
+
384
+ if _has("tectonic"):
385
+ cmd = ["tectonic", tex_path.name]
386
+ logs.append("β–Ά Compiling with tectonic …")
387
+ elif _has("lualatex"):
388
+ cmd = ["lualatex", "-interaction=nonstopmode", tex_path.name]
389
+ logs.append("β–Ά Compiling with lualatex …")
390
+ elif _has("xelatex"):
391
+ cmd = ["xelatex", "-interaction=nonstopmode", tex_path.name]
392
+ logs.append("β–Ά Compiling with xelatex …")
393
+ elif _has("latexmk"):
394
+ cmd = ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path.name]
395
+ logs.append("β–Ά Compiling with latexmk …")
396
+ else:
397
+ logs.append("⚠️ No TeX engine found (tectonic/lualatex/xelatex/latexmk). Skipping PDF compile.")
398
+ return None
399
+
400
+ proc = _sp.run(cmd, cwd=str(proj_dir), stdout=_sp.PIPE, stderr=_sp.STDOUT, text=True)
401
+ if proc.stdout:
402
+ logs.append(proc.stdout[-4000:])
403
+ if proc.returncode != 0:
404
+ logs.append(f"❌ PDF compile failed with code {proc.returncode}.")
405
+ return None
406
+
407
+ # Find produced PDF
408
+ for out_name in ("poster_output.pdf", "poster.pdf", tex_path.stem + ".pdf"):
409
+ out_path = proj_dir / out_name
410
+ if out_path.exists():
411
+ logs.append(f"βœ… PDF generated β†’ {out_path.relative_to(OUTPUT_DIR)}")
412
+ return out_path
413
+
414
+ logs.append("⚠️ PDF not found after compile.")
415
+ return None
416
+ except Exception as e:
417
+ logs.append(f"⚠️ PDF compile error: {e}")
418
+ return None
419
+
420
+ def _pdf_to_iframe_html(pdf_path: Path, width="100%", height="900px") -> str:
421
+ try:
422
+ b = pdf_path.read_bytes()
423
+ b64 = base64.b64encode(b).decode("utf-8")
424
+ return (
425
+ f"<div style='border:1px solid #ddd;border-radius:8px;overflow:hidden'>"
426
+ f"<embed type='application/pdf' width='{width}' height='{height}' src='data:application/pdf;base64,{b64}'></embed>"
427
+ f"<div style='padding:8px'><a download='poster.pdf' href='data:application/pdf;base64,{b64}'>⬇️ Download PDF</a></div>"
428
+ f"</div>"
429
+ )
430
+ except Exception:
431
+ return ""
432
+
433
+ def _compile_tex_to_pdf(tex_path: Path, logs):
434
+ """Generic TeX compile helper for a .tex file. Returns Path to PDF or None."""
435
+ try:
436
+ proj_dir = tex_path.parent
437
+ import shutil as _sh, subprocess as _sp
438
+ def _has(bin_name):
439
+ return _sh.which(bin_name) is not None
440
+ if _has("tectonic"):
441
+ cmd = ["tectonic", tex_path.name]
442
+ logs.append("β–Ά Compiling with tectonic …")
443
+ elif _has("lualatex"):
444
+ cmd = ["lualatex", "-interaction=nonstopmode", tex_path.name]
445
+ logs.append("β–Ά Compiling with lualatex …")
446
+ elif _has("xelatex"):
447
+ cmd = ["xelatex", "-interaction=nonstopmode", tex_path.name]
448
+ logs.append("β–Ά Compiling with xelatex …")
449
+ elif _has("latexmk"):
450
+ cmd = ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path.name]
451
+ logs.append("β–Ά Compiling with latexmk …")
452
+ else:
453
+ logs.append("⚠️ No TeX engine found.")
454
+ return None
455
+ proc = _sp.run(cmd, cwd=str(proj_dir), stdout=_sp.PIPE, stderr=_sp.STDOUT, text=True)
456
+ if proc.stdout:
457
+ logs.append(proc.stdout[-4000:])
458
+ if proc.returncode != 0:
459
+ logs.append(f"❌ PDF compile failed with code {proc.returncode}.")
460
+ return None
461
+ guess = proj_dir / (tex_path.stem + ".pdf")
462
+ return guess if guess.exists() else None
463
+ except Exception as e:
464
+ logs.append(f"⚠️ PDF compile error: {e}")
465
+ return None
466
+
467
+ def _ensure_left_logo_or_disable(OUTPUT_DIR: Path, logs):
468
+ """If no left_logo.* exists in logos/, comment out \logoleft line in poster_output.tex."""
469
+ tex_path = OUTPUT_DIR / "poster_latex_proj" / "poster_output.tex"
470
+ logos_dir = OUTPUT_DIR / "poster_latex_proj" / "logos"
471
+ try:
472
+ if not tex_path.exists():
473
+ return False
474
+ # any left_logo.* present?
475
+ has_left = False
476
+ if logos_dir.exists():
477
+ for p in logos_dir.iterdir():
478
+ if p.is_file() and p.stem == "left_logo":
479
+ has_left = True
480
+ break
481
+ if has_left:
482
+ return False
483
+ txt = tex_path.read_text(encoding="utf-8")
484
+ if "\\logoleft" in txt:
485
+ new_txt = re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=re.MULTILINE)
486
+ if new_txt != txt:
487
+ tex_path.write_text(new_txt, encoding="utf-8")
488
+ logs.append("ℹ️ No left_logo found; disabled \\\logoleft in poster_output.tex.")
489
+ return True
490
+ except Exception as e:
491
+ logs.append(f"⚠️ Failed left_logo fallback: {e}")
492
+ return False
493
+
494
  # =====================
495
  # Gradio pipeline function (ISOLATED)
496
  # =====================
 
508
  POSTER_LATEX_DIR = WORK_DIR / "posterbuilder" / "latex_proj"
509
 
510
  _write_logs(LOG_PATH, logs)
511
+ yield "\n".join(logs), "", None, ""
512
 
513
  # ====== Validation: must upload LOGO ======
514
  if logo_files is None:
 
521
  # msg = "❌ You must upload at least one institutional logo (multiple allowed)."
522
  # logs.append(msg)
523
  # _write_logs(LOG_PATH, logs)
524
+ # yield "\n".join(logs), "", None, ""
525
  # return
526
 
527
  # Save logos into run-local dir
 
535
  saved_logo_paths.append(p)
536
  logs.append(f"🏷️ Saved {len(saved_logo_paths)} logo file(s) β†’ {LOGO_DIR.relative_to(WORK_DIR)}")
537
  _write_logs(LOG_PATH, logs)
538
+ yield "\n".join(logs), "", None, ""
539
 
540
  # ====== Handle uploaded PDF (optional) ======
541
  pdf_path = None
 
550
  canonical_pdf = INPUT_DIR / "paper.pdf"
551
  shutil.copy(pdf_file.name, canonical_pdf)
552
  _write_logs(LOG_PATH, logs)
553
+ yield "\n".join(logs), "", None, ""
554
 
555
  # ====== Validate input source ======
556
  if not arxiv_url and not pdf_file:
557
  msg = "❌ Please provide either an arXiv link or upload a PDF file (choose one)."
558
  logs.append(msg)
559
  _write_logs(LOG_PATH, logs)
560
+ yield "\n".join(logs), "", None, ""
561
  return
562
 
563
  # ====== Build command (run INSIDE workspace) ======
 
578
  logs.append("\n======= REAL-TIME LOG =======")
579
  logs.append(f"cwd = runs/{WORK_DIR.name}")
580
  _write_logs(LOG_PATH, logs)
581
+ yield "\n".join(logs), "", None, ""
582
 
583
  # ====== Run with REAL-TIME streaming, inside workspace ======
584
  try:
 
595
  msg = f"❌ Pipeline failed to start: {e}"
596
  logs.append(msg)
597
  _write_logs(LOG_PATH, logs)
598
+ yield "\n".join(logs), "", None, ""
599
  return
600
 
601
  last_yield = time.time()
 
609
  except Exception:
610
  pass
611
  _write_logs(LOG_PATH, logs)
612
+ yield "\n".join(logs), "", None, ""
613
  return
614
 
615
  line = process.stdout.readline()
 
620
  now = time.time()
621
  if now - last_yield >= 0.3:
622
  last_yield = now
623
+ yield "\n".join(logs), "", None, ""
624
  elif process.poll() is not None:
625
  break
626
  else:
 
629
  return_code = process.wait()
630
  logs.append(f"\nProcess finished with code {return_code}")
631
  _write_logs(LOG_PATH, logs)
632
+ yield "\n".join(logs), "", None, ""
633
 
634
  if return_code != 0:
635
  logs.append("❌ Process exited with non-zero status. See logs above.")
636
  _write_logs(LOG_PATH, logs)
637
+ yield "\n".join(logs), "", None, ""
638
  return
639
 
640
  except Exception as e:
641
  logs.append(f"❌ Error during streaming: {e}")
642
  _write_logs(LOG_PATH, logs)
643
+ yield "\n".join(logs), "", None, ""
644
  return
645
  finally:
646
  try:
 
663
  msg = "❌ No output generated. Please check logs above."
664
  logs.append(msg)
665
  _write_logs(LOG_PATH, logs)
666
+ yield "\n".join(logs), "", None, ""
667
  return
668
 
669
  # ====== NEW: Post-processing (optional features) ======
 
678
 
679
  # 3) Optional institutional logo -> left_logo.<ext>
680
  _apply_left_logo(OUTPUT_DIR, logo_files, logs)
681
+ _ensure_left_logo_or_disable(OUTPUT_DIR, logs)
682
 
683
  _write_logs(LOG_PATH, logs)
684
+ yield "\n".join(logs), "", None, ""
685
 
686
 
687
  _write_logs(LOG_PATH, logs)
688
+ yield "\n".join(logs), "", None, ""
689
+
690
+ # ====== Compile PDF (for inline preview) ======
691
+ pdf_html = ""
692
+ try:
693
+ pdf_path = _compile_poster_pdf(OUTPUT_DIR, logs)
694
+ if pdf_path and pdf_path.exists():
695
+ pdf_html = _pdf_to_iframe_html(pdf_path)
696
+ logs.append("πŸ–¨οΈ PDF ready for preview in UI.")
697
+ except Exception as e:
698
+ logs.append(f"⚠️ PDF compile/preview skipped: {e}")
699
 
700
  # ====== Zip output (run-local) ======
701
  try:
 
732
 
733
  _write_logs(LOG_PATH, logs)
734
  yield "\n".join(logs), (
735
+ pdf_html
736
+ ), (
737
+ str(ZIP_PATH) if ZIP_PATH.exists() else None
738
  ), render_overleaf_button(overleaf_zip_b64)
739
 
740
 
 
789
  with gr.Column(scale=1):
790
  with gr.Accordion("Output", open=True):
791
  logs_out = gr.Textbox(label="🧾 Logs (8–10 minutes)", lines=30, max_lines=50)
792
+ pdf_out = gr.HTML(label="πŸ“„ Poster (PDF Preview)")
793
  zip_out = gr.File(
794
  label="πŸ“¦ Download Results (.zip)",
795
  info="πŸ“Œ After uploading the ZIP to Overleaf, please select **XeLaTeX** as the compile engine.",
796
+ )
797
+ overleaf_out = gr.HTML(label="Open in Overleaf")
798
+ debug_btn = gr.Button("🐞 Debug PDF", variant="secondary")
799
+ debug_out = gr.HTML(label="🐞 Debug Preview")
800
  run_btn.click(
801
  fn=run_pipeline,
802
  inputs=[arxiv_in, pdf_in, key_in, inst_logo_in, conf_logo_in, theme_in],
803
+ outputs=[logs_out, pdf_out, zip_out, overleaf_out],
804
+ )
805
+ debug_btn.click(
806
+ fn=debug_compile,
807
+ inputs=[],
808
+ outputs=[debug_out],
809
  )
810
 
811
  if __name__ == "__main__":
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ tectonic
runtime.yaml CHANGED
@@ -1 +1,3 @@
1
- sdk: docker
 
 
 
1
+ sdk: gradio
2
+ app_file: app.py
3
+ python_version: 3.10