Spaces:
Running
Running
| import gradio as gr | |
| import subprocess, shutil, os, zipfile, datetime, sys, time, uuid, stat, re | |
| from pathlib import Path | |
| import base64 | |
| # ===================== | |
| # Version guard | |
| # ===================== | |
| def _ensure_versions(): | |
| import importlib, subprocess, sys | |
| def get_version(pkg): | |
| try: | |
| m = importlib.import_module(pkg) | |
| return getattr(m, "__version__", "0") | |
| except Exception: | |
| return "0" | |
| try: | |
| from packaging.version import Version | |
| except ImportError: | |
| # 安装packaging,确保下面版本比较能用 | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "packaging"]) | |
| from packaging.version import Version | |
| # 检查 huggingface_hub | |
| hub_ver = get_version("huggingface_hub") | |
| hv = Version(hub_ver) | |
| required_min = Version("0.24.0") | |
| required_max = Version("1.0.0") | |
| hub_ok = required_min <= hv < required_max | |
| if not hub_ok: | |
| print(f"[INFO] huggingface_hub=={hub_ver} not in range " | |
| f"[{required_min}, {required_max}), reinstalling...") | |
| subprocess.check_call([ | |
| sys.executable, "-m", "pip", "install", | |
| "huggingface-hub==0.27.1", | |
| "transformers==4.48.0", | |
| "--force-reinstall", "--no-deps" | |
| ]) | |
| else: | |
| print(f"[INFO] huggingface_hub version OK: {hub_ver}") | |
| _ensure_versions() | |
| # ===================== | |
| # Paths (read-only repo root; DO NOT write here) | |
| # ===================== | |
| ROOT = Path(__file__).resolve().parent | |
| RUNS_DIR = ROOT / "runs" # all per-run workspaces live here | |
| RUNS_DIR.mkdir(parents=True, exist_ok=True) | |
| TIMEOUT_SECONDS = 1800 # 30 minutes | |
| RETENTION_HOURS = 1 # auto-clean runs older than N hours | |
| DEFAULT_RIGHT_LOGO_PATH = ROOT / "template" / "logos" / "right_logo.png" | |
| # --------------------- | |
| # Utils | |
| # --------------------- | |
| def _now_str(): | |
| return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') | |
| def _write_logs(log_path: Path, logs): | |
| try: | |
| log_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(log_path, "w", encoding="utf-8") as f: | |
| f.write("\n".join(logs)) | |
| except Exception: | |
| pass | |
| def _find_ui_header_logos(root: Path): | |
| """Return list of Paths for UI header logos in order camel → tvg → waterloo. | |
| Prefer template/logos; fallback to assets folders; if none match, return up to 3 images. | |
| """ | |
| preferred = root / "template" / "logos" | |
| # Desired visual order (left → right) with CAMEL at rightmost: | |
| names = ["tvg", "waterloo", "camel"] | |
| found = [] | |
| # Try preferred dir first with explicit names | |
| try: | |
| if preferred.exists(): | |
| allp = list(preferred.iterdir()) | |
| for key in names: | |
| for p in allp: | |
| if p.is_file() and p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"} and key in p.name.lower(): | |
| found.append(p) | |
| break | |
| except Exception: | |
| pass | |
| # If not all found, search broader locations | |
| if len(found) < 3: | |
| cand_dirs = [ | |
| root / "assets", | |
| root / "assets" / "logos", | |
| root / "Paper2Poster" / "assets", | |
| root / "Paper2Poster" / "assets" / "logos", | |
| root / "paper2poster" / "assets", | |
| root / "paper2poster" / "assets" / "logos", | |
| ] | |
| imgs = [] | |
| for d in cand_dirs: | |
| try: | |
| if d.exists(): | |
| for p in d.iterdir(): | |
| if p.is_file() and p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}: | |
| imgs.append(p) | |
| except Exception: | |
| continue | |
| for key in names: | |
| if any(key in str(fp).lower() for fp in found): | |
| continue | |
| for p in imgs: | |
| if key in p.name.lower(): | |
| found.append(p) | |
| break | |
| # Fallback: fill up to 3 | |
| if not found and imgs: | |
| found = imgs[:3] | |
| return found | |
| def _ui_header_logos_html(): | |
| """Return an HTML div with base64-embedded logos to avoid broken /file routes. | |
| Logos are fixed at top-right, slightly inset, with larger spacing. | |
| """ | |
| import base64 | |
| logos = _find_ui_header_logos(ROOT) | |
| if not logos: | |
| return "" | |
| parts = [] | |
| for p in logos: | |
| try: | |
| b = p.read_bytes() | |
| b64 = base64.b64encode(b).decode("utf-8") | |
| mime = "image/png" if p.suffix.lower() == ".png" else "image/jpeg" | |
| src = f"data:{mime};base64,{b64}" | |
| name = p.name.lower() | |
| href = None | |
| if "camel" in name: | |
| href = "https://www.camel-ai.org/" | |
| elif "tvg" in name or "torr" in name: | |
| href = "https://torrvision.com/index.html" | |
| elif "waterloo" in name: | |
| href = "https://uwaterloo.ca/" | |
| img = f"<img src='{src}' alt='{p.stem}' style='height:44px;width:auto;object-fit:contain;display:block;cursor:pointer'>" | |
| parts.append(f"<a href='{href}' target='_blank' rel='noopener'>{img}</a>" if href else img) | |
| except Exception: | |
| continue | |
| if not parts: | |
| return "" | |
| # Fixed-position header bar at top-right; moved further left and larger spacing | |
| imgs = "".join(parts) | |
| return ( | |
| "<style>" | |
| "#p2p-logo-bar{position:fixed;top:12px;right:140px;display:flex;gap:24px;" | |
| "align-items:center;z-index:9999}" | |
| "#p2p-logo-bar a{display:block;line-height:0}" | |
| "@media (max-width: 768px){#p2p-logo-bar img{height:32px}}" | |
| "</style>" | |
| "<div id='p2p-logo-bar'>" + imgs + "</div>" | |
| ) | |
| def _default_conf_logo_path(): | |
| """Pick a default conference logo to preview. | |
| Prefer Paper2Poster/assets/neurips.png, else template/logos/right_logo.png, | |
| else the first of detected header logos. | |
| """ | |
| try: | |
| prefer_assets = ROOT / "Paper2Poster" / "assets" / "neurips.png" | |
| if prefer_assets.exists(): | |
| return prefer_assets | |
| pref = ROOT / "template" / "logos" / "right_logo.png" | |
| if pref.exists(): | |
| return pref | |
| logos = _find_ui_header_logos(ROOT) | |
| for p in logos: | |
| if p and p.exists(): | |
| return p | |
| except Exception: | |
| pass | |
| return None | |
| ## Removed sanitizer per request: do not mutate user-generated TeX | |
| def _on_rm_error(func, path, exc_info): | |
| # fix "PermissionError: [Errno 13] Permission denied" for readonly files | |
| os.chmod(path, stat.S_IWRITE) | |
| func(path) | |
| def _copytree(src: Path, dst: Path, symlinks=True, ignore=None): | |
| if dst.exists(): | |
| shutil.rmtree(dst, onerror=_on_rm_error) | |
| shutil.copytree(src, dst, symlinks=symlinks, ignore=ignore) | |
| def _safe_copy(src: Path, dst: Path): | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, dst) | |
| def _cleanup_old_runs(max_age_hours=12): | |
| try: | |
| now = datetime.datetime.now().timestamp() | |
| for run_dir in RUNS_DIR.iterdir(): | |
| try: | |
| if not run_dir.is_dir(): | |
| continue | |
| mtime = run_dir.stat().st_mtime | |
| age_h = (now - mtime) / 3600.0 | |
| if age_h > max_age_hours: | |
| shutil.rmtree(run_dir, onerror=_on_rm_error) | |
| except Exception: | |
| continue | |
| except Exception: | |
| pass | |
| def _prepare_workspace(logs): | |
| """Create isolated per-run workspace and copy needed code/assets into it.""" | |
| run_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:8] | |
| work_dir = RUNS_DIR / run_id | |
| work_dir.mkdir(parents=True, exist_ok=True) | |
| # Per-run log & zip path | |
| log_path = work_dir / "run.log" | |
| zip_path = work_dir / "output.zip" | |
| logs.append(f"🧩 New workspace: {work_dir.relative_to(ROOT)} (run_id={run_id})") | |
| # Copy code/assets that do file IO so they are run-local (avoid shared writes) | |
| # Keep copies as cheap as possible (symlinks=True when supported) | |
| needed_dirs = ["posterbuilder", "Paper2Poster"] | |
| for d in needed_dirs: | |
| src = ROOT / d | |
| if src.exists(): | |
| _copytree(src, work_dir / d, symlinks=True) | |
| logs.append(f" ↪ copied {d}/ → runs/{run_id}/{d}/ (symlink where possible)") | |
| # template/ optional | |
| tmpl = ROOT / "template" | |
| if tmpl.exists(): | |
| _copytree(tmpl, work_dir / "template", symlinks=True) | |
| logs.append(" ↪ copied template/") | |
| # pipeline.py must live inside workspace so that ROOT_DIR=work_dir | |
| _safe_copy(ROOT / "pipeline.py", work_dir / "pipeline.py") | |
| # Create standard IO dirs in workspace | |
| (work_dir / "input" / "pdf").mkdir(parents=True, exist_ok=True) | |
| (work_dir / "input" / "logo").mkdir(parents=True, exist_ok=True) | |
| (work_dir / "posterbuilder" / "latex_proj").mkdir(parents=True, exist_ok=True) | |
| return run_id, work_dir, log_path, zip_path | |
| # --------------------- | |
| # Helpers for new features (post-processing) | |
| # --------------------- | |
| def _parse_rgb(val): | |
| """Return (R, G, B) as ints in [0,255] from '#RRGGBB', 'rgb(...)', 'rgba(...)', 'r,g,b', [r,g,b], or (r,g,b).""" | |
| if val is None: | |
| return None | |
| import re | |
| def clamp255(x): | |
| try: | |
| return max(0, min(255, int(round(float(x))))) | |
| except Exception: | |
| return None | |
| s = str(val).strip() | |
| # list/tuple | |
| if isinstance(val, (list, tuple)) and len(val) >= 3: | |
| r, g, b = [clamp255(val[0]), clamp255(val[1]), clamp255(val[2])] | |
| if None not in (r, g, b): | |
| return (r, g, b) | |
| # hex: #RGB or #RRGGBB | |
| if s.startswith("#"): | |
| hx = s[1:].strip() | |
| if len(hx) == 3: | |
| hx = "".join(c*2 for c in hx) | |
| if len(hx) == 6 and re.fullmatch(r"[0-9A-Fa-f]{6}", hx): | |
| return tuple(int(hx[i:i+2], 16) for i in (0, 2, 4)) | |
| # rgb/rgba(...) | |
| m = re.match(r"rgba?\(\s*([^)]+)\)", s, flags=re.IGNORECASE) | |
| if m: | |
| parts = [p.strip() for p in m.group(1).split(",")] | |
| if len(parts) >= 3: | |
| def to_int(p): | |
| if p.endswith("%"): | |
| # percentage to 0-255 | |
| return clamp255(float(p[:-1]) * 255.0 / 100.0) | |
| return clamp255(p) | |
| r, g, b = to_int(parts[0]), to_int(parts[1]), to_int(parts[2]) | |
| if None not in (r, g, b): | |
| return (r, g, b) | |
| # 'r,g,b' | |
| if "," in s: | |
| parts = [p.strip() for p in s.split(",")] | |
| if len(parts) >= 3: | |
| def to_int(p): | |
| if p.endswith("%"): | |
| return clamp255(float(p[:-1]) * 255.0 / 100.0) | |
| return clamp255(p) | |
| r, g, b = to_int(parts[0]), to_int(parts[1]), to_int(parts[2]) | |
| if None not in (r, g, b): | |
| return (r, g, b) | |
| return None | |
| def _apply_meeting_logo(OUTPUT_DIR: Path, meeting_logo_file, logs): | |
| """Replace output/poster_latex_proj/logos/right_logo.png if meeting_logo_file provided.""" | |
| if not meeting_logo_file: | |
| return False | |
| def _ensure_right_logo_default(OUTPUT_DIR: Path, logs): | |
| """If no right_logo.png exists in output poster project, copy a default NeurIPS logo. | |
| Looks for Paper2Poster/assets/neurips.png first, else template/logos/right_logo.png. | |
| Returns True if a logo was written. | |
| """ | |
| try: | |
| logos_dir = OUTPUT_DIR / "poster_latex_proj" / "logos" | |
| target = logos_dir / "right_logo.png" | |
| if target.exists(): | |
| return False | |
| # Preferred default | |
| prefer_assets = ROOT / "Paper2Poster" / "assets" / "neurips.png" | |
| fallback_tpl = ROOT / "template" / "logos" / "right_logo.png" | |
| src = None | |
| if prefer_assets.exists(): | |
| src = prefer_assets | |
| elif fallback_tpl.exists(): | |
| src = fallback_tpl | |
| if src is None: | |
| logs.append("⚠️ No default right_logo source found (neurips.png or template right_logo.png).") | |
| return False | |
| logos_dir.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, target) | |
| logs.append(f"🏷️ Applied default conference logo → {target.relative_to(OUTPUT_DIR)}") | |
| return True | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed to apply default right_logo: {e}") | |
| return False | |
| logos_dir = OUTPUT_DIR / "poster_latex_proj" / "logos" | |
| target = logos_dir / "right_logo.png" | |
| try: | |
| logos_dir.mkdir(parents=True, exist_ok=True) | |
| # Try to convert to PNG for safety | |
| try: | |
| from PIL import Image | |
| img = Image.open(meeting_logo_file.name) | |
| # preserve alpha if available | |
| if img.mode not in ("RGB", "RGBA"): | |
| img = img.convert("RGBA") | |
| img.save(target, format="PNG") | |
| logs.append(f"🖼️ Meeting logo converted to PNG and saved → {target.relative_to(OUTPUT_DIR)}") | |
| except Exception as e: | |
| # Fallback: raw copy with .png name | |
| shutil.copy(meeting_logo_file.name, target) | |
| logs.append(f"🖼️ Meeting logo copied (no conversion) → {target.relative_to(OUTPUT_DIR)} (note: ensure it's a valid PNG).") | |
| return True | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed to apply meeting logo: {e}") | |
| return False | |
| def _apply_theme_rgb(OUTPUT_DIR: Path, rgb_tuple, logs): | |
| if not rgb_tuple: | |
| return False | |
| tex_path = OUTPUT_DIR / "poster_latex_proj" / "poster_output.tex" | |
| if not tex_path.exists(): | |
| logs.append(f"⚠️ Theme RGB skipped: {tex_path.relative_to(OUTPUT_DIR)} not found.") | |
| return False | |
| try: | |
| content = tex_path.read_text(encoding="utf-8") | |
| r, g, b = rgb_tuple | |
| name_pattern = r"(?:nipspurple|neuripspurple|themecolor)" | |
| rgb_pat = rf"(\\definecolor\{{{name_pattern}\}}\{{RGB\}}\{{)\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(\}})" | |
| def repl_rgb(m): | |
| return f"{m.group(1)}{r},{g},{b}{m.group(2)}" | |
| new_content, n = re.subn(rgb_pat, repl_rgb, content, flags=re.MULTILINE) | |
| if n == 0: | |
| hexval = f"{r:02X}{g:02X}{b:02X}" | |
| html_pat = rf"(\\definecolor\{{{name_pattern}\}}\{{HTML\}}\{{)[0-9A-Fa-f]{{6}}(\}})" | |
| def repl_html(m): | |
| return f"{m.group(1)}{hexval}{m.group(2)}" | |
| new_content, n = re.subn(html_pat, repl_html, content, flags=re.MULTILINE) | |
| if n > 0: | |
| tex_path.write_text(new_content, encoding="utf-8") | |
| logs.append(f"🎨 Theme color updated to RGB {{{r},{g},{b}}}") | |
| return True | |
| else: | |
| logs.append("ℹ️ No \\definecolor target found.") | |
| return False | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed to update theme RGB: {e}") | |
| return False | |
| def _apply_left_logo(OUTPUT_DIR: Path, logo_files, logs): | |
| """ | |
| Use the first institutional logo uploaded by the user: | |
| - Copy it into output/poster_latex_proj/logos/ as left_logo.<ext> | |
| - Replace 'logos/left_logo.png' in poster_output.tex with the proper file extension | |
| Does NOT convert formats. Simply renames and rewrites the tex reference. | |
| """ | |
| if not logo_files: | |
| logs.append("ℹ️ No institutional logo uploaded.") | |
| return False | |
| if isinstance(logo_files, (list, tuple)) and len(logo_files) > 1: | |
| logs.append("Multiple institutional logos uploaded.") | |
| return False | |
| # Single file case | |
| f = logo_files[0] if isinstance(logo_files, (list, tuple)) else logo_files | |
| if not f: | |
| logs.append("ℹ️ No institutional logo uploaded.") | |
| return False | |
| ext = Path(f.name).suffix or ".png" # fallback to .png if no extension | |
| logos_dir = OUTPUT_DIR / "poster_latex_proj" / "logos" | |
| tex_path = OUTPUT_DIR / "poster_latex_proj" / "poster_output.tex" | |
| try: | |
| logos_dir.mkdir(parents=True, exist_ok=True) | |
| dst = logos_dir / f"left_logo{ext}" | |
| shutil.copy(f.name, dst) | |
| logs.append(f"🏷️ Institutional logo copied to: {dst.relative_to(OUTPUT_DIR)}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed to copy institutional logo: {e}") | |
| return False | |
| if not tex_path.exists(): | |
| logs.append("⚠️ poster_output.tex not found, cannot replace left_logo path.") | |
| return False | |
| try: | |
| text = tex_path.read_text(encoding="utf-8") | |
| old = "logos/left_logo.png" | |
| new = f"logos/left_logo{ext}" | |
| if old in text: | |
| tex_path.write_text(text.replace(old, new), encoding="utf-8") | |
| logs.append(f"🛠️ Replaced left_logo.png → left_logo{ext} in poster_output.tex") | |
| return True | |
| # Fallback (covers weird spacing or macro variations) | |
| import re | |
| pattern = r"(logos/left_logo)\.png" | |
| new_text, n = re.subn(pattern, r"\1" + ext, text) | |
| if n > 0: | |
| tex_path.write_text(new_text, encoding="utf-8") | |
| logs.append(f"🛠️ Replaced left_logo.png → left_logo{ext} (regex fallback)") | |
| return True | |
| logs.append("ℹ️ No left_logo.png reference found in poster_output.tex.") | |
| return False | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed to modify poster_output.tex: {e}") | |
| return False | |
| def render_overleaf_button(overleaf_b64): | |
| if not overleaf_b64: | |
| return "" | |
| html = f""" | |
| <form action="https://www.overleaf.com/docs" method="post" target="_blank"> | |
| <input type="hidden" name="snip_uri" value="data:application/zip;base64,{overleaf_b64}"> | |
| <input type="hidden" name="engine" value="xelatex"> | |
| <button style=" | |
| background:#4CAF50;color:white;padding:8px 14px; | |
| border:none;border-radius:6px;cursor:pointer; margin-top:8px; | |
| "> | |
| 🚀 Open in Overleaf | |
| </button> | |
| </form> | |
| """ | |
| return html | |
| def _get_tectonic_bin(logs): | |
| """Return a usable path to the tectonic binary. Try PATH/common paths; if not found, download to runs/_bin.""" | |
| import shutil as _sh, tarfile, urllib.request, os | |
| # 1) existing in PATH or common dirs | |
| cands = [ | |
| "tectonic", | |
| str(Path("/usr/local/bin/tectonic")), | |
| str(Path("/usr/bin/tectonic")), | |
| os.path.expanduser("~/.local/bin/tectonic"), | |
| str((RUNS_DIR / "_bin" / "tectonic").resolve()), | |
| ] | |
| for c in cands: | |
| if _sh.which(c) or Path(c).exists(): | |
| return c | |
| # 2) download to runs/_bin | |
| try: | |
| url = "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%400.15.0/tectonic-0.15.0-x86_64-unknown-linux-gnu.tar.gz" | |
| bin_dir = RUNS_DIR / "_bin" | |
| bin_dir.mkdir(parents=True, exist_ok=True) | |
| tgz = bin_dir / "tectonic.tar.gz" | |
| logs.append("⬇️ Downloading tectonic binary …") | |
| with urllib.request.urlopen(url, timeout=60) as resp, open(tgz, "wb") as out: | |
| out.write(resp.read()) | |
| with tarfile.open(tgz, "r:gz") as tf: | |
| tf.extractall(path=bin_dir) | |
| # find binary | |
| tbin = None | |
| for p in bin_dir.rglob("tectonic"): | |
| if p.is_file() and os.access(p, os.X_OK): | |
| tbin = p | |
| break | |
| if not tbin: | |
| # make executable if needed | |
| for p in bin_dir.rglob("tectonic"): | |
| try: | |
| p.chmod(0o755) | |
| tbin = p | |
| break | |
| except Exception: | |
| continue | |
| if tbin: | |
| logs.append(f"✅ Tectonic ready at {tbin}") | |
| return str(tbin) | |
| logs.append("⚠️ Tectonic binary not found after download.") | |
| except Exception as e: | |
| logs.append(f"⚠️ Tectonic download failed: {e}") | |
| return None | |
| def _compile_poster_pdf(OUTPUT_DIR: Path, logs): | |
| """ | |
| Compile output/poster_latex_proj/poster_output.tex into a PDF using an | |
| available TeX engine. Prefer 'tectonic', then 'lualatex', then 'xelatex', | |
| then 'latexmk'. Returns Path to the PDF or None. | |
| """ | |
| try: | |
| proj_dir = OUTPUT_DIR / "poster_latex_proj" | |
| tex_path = proj_dir / "poster_output.tex" | |
| if not tex_path.exists(): | |
| logs.append(f"⚠️ LaTeX source not found: {tex_path.relative_to(OUTPUT_DIR)}") | |
| return None | |
| # Clean old PDFs | |
| for cand in (proj_dir / "poster_output.pdf", proj_dir / "poster.pdf"): | |
| try: | |
| if cand.exists(): | |
| cand.unlink() | |
| except Exception: | |
| pass | |
| import shutil as _sh | |
| import subprocess as _sp | |
| def _has(bin_name): | |
| return _sh.which(bin_name) is not None | |
| # Most-tolerant: prefer latexmk with XeLaTeX and force (-f), then XeLaTeX, then LuaLaTeX | |
| pretex = r"\nonstopmode\scrollmode\makeatletter\let\pgf@error\pgf@warning\let\GenericError\GenericWarning\let\PackageError\PackageWarning\makeatother" | |
| if _has("latexmk"): | |
| cmd = [ | |
| "latexmk", "-pdf", "-pdflatex=xelatex", "-f", | |
| "-interaction=nonstopmode", "-file-line-error", | |
| f"-pretex={pretex}", | |
| tex_path.name, | |
| ] | |
| logs.append("▶ Compiling with latexmk (-pdf -pdflatex=xelatex -f, pretex demote errors) …") | |
| elif _has("xelatex"): | |
| # Inject pretex macros via direct input to engine (no file mutation) | |
| injected = pretex + f"\\input{{{tex_path.name}}}" | |
| cmd = ["xelatex", "-interaction=nonstopmode", "-file-line-error", injected] | |
| logs.append("▶ Compiling with xelatex (pretex injected) …") | |
| elif _has("lualatex"): | |
| injected = pretex + f"\\input{{{tex_path.name}}}" | |
| cmd = ["lualatex", "-interaction=nonstopmode", "-file-line-error", injected] | |
| logs.append("▶ Compiling with lualatex (pretex injected) …") | |
| else: | |
| logs.append("⚠️ No TeX engine found (latexmk/xelatex/lualatex). Skipping PDF compile.") | |
| return None | |
| import os as _os | |
| _env = _os.environ.copy() | |
| # Ensure TeX can find local theme/fonts across project tree | |
| texinputs = _env.get("TEXINPUTS", "") | |
| search = _os.pathsep.join([ | |
| str(proj_dir), str(proj_dir) + "//", | |
| str(proj_dir.parent), str(proj_dir.parent) + "//", | |
| ]) | |
| _env["TEXINPUTS"] = search + _os.pathsep + texinputs | |
| proc = _sp.run(cmd, cwd=str(proj_dir), stdout=_sp.PIPE, stderr=_sp.STDOUT, text=True, env=_env) | |
| if proc.stdout: | |
| logs.append(proc.stdout[-4000:]) | |
| # Accept PDF even if return code is non-zero (be tolerant) | |
| for out_name in ("poster_output.pdf", "poster.pdf", tex_path.stem + ".pdf"): | |
| out_path = proj_dir / out_name | |
| if out_path.exists(): | |
| if proc.returncode != 0: | |
| logs.append(f"⚠️ Compile returned code {proc.returncode}, but PDF exists; using it.") | |
| else: | |
| logs.append(f"✅ PDF generated → {out_path.relative_to(OUTPUT_DIR)}") | |
| return out_path | |
| if proc.returncode != 0: | |
| logs.append(f"❌ PDF compile failed with code {proc.returncode} and no PDF produced.") | |
| return None | |
| logs.append("⚠️ PDF not found after compile.") | |
| return None | |
| except Exception as e: | |
| logs.append(f"⚠️ PDF compile error: {e}") | |
| return None | |
| def _pdf_to_iframe_html(pdf_path: Path, width="100%", height="900px") -> str: | |
| try: | |
| b = pdf_path.read_bytes() | |
| b64 = base64.b64encode(b).decode("utf-8") | |
| return ( | |
| f"<div style='border:1px solid #ddd;border-radius:8px;overflow:hidden'>" | |
| f"<embed type='application/pdf' width='{width}' height='{height}' src='data:application/pdf;base64,{b64}'></embed>" | |
| f"</div>" | |
| ) | |
| except Exception: | |
| return "" | |
| def _pdf_to_iframe_file(pdf_path: Path, width="100%", height="900px") -> str: | |
| try: | |
| from urllib.parse import quote | |
| p = str(pdf_path) | |
| src = f"/file={quote(p)}" | |
| return ( | |
| f"<div style='border:1px solid #ddd;border-radius:8px;overflow:hidden'>" | |
| f"<iframe src='{src}' width='{width}' height='{height}' style='border:0'></iframe>" | |
| f"</div>" | |
| ) | |
| except Exception: | |
| return "" | |
| def _pdf_to_image_first_page(pdf_path: Path, out_dir: Path, logs) -> Path | None: | |
| try: | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out_path = out_dir / "poster_preview.png" | |
| # Try PyMuPDF | |
| try: | |
| import fitz # PyMuPDF | |
| doc = fitz.open(str(pdf_path)) | |
| if doc.page_count == 0: | |
| return None | |
| page = doc.load_page(0) | |
| mat = fitz.Matrix(2, 2) | |
| pix = page.get_pixmap(matrix=mat, alpha=False) | |
| pix.save(str(out_path)) | |
| return out_path if out_path.exists() else None | |
| except Exception as e: | |
| logs.append(f"⚠️ PyMuPDF render failed: {e}") | |
| # Fallback: pypdfium2 | |
| try: | |
| import pypdfium2 as pdfium | |
| pdf = pdfium.PdfDocument(str(pdf_path)) | |
| if len(pdf) == 0: | |
| return None | |
| page = pdf[0] | |
| bitmap = page.render(scale=2).to_pil() | |
| bitmap.save(out_path) | |
| return out_path if out_path.exists() else None | |
| except Exception as e: | |
| logs.append(f"⚠️ pypdfium2 render failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ image preview failed: {e}") | |
| return None | |
| def preview_image_from_pdf(pdf_file): | |
| try: | |
| path = pdf_file | |
| if hasattr(pdf_file, 'name'): | |
| path = pdf_file.name | |
| if not path: | |
| return None | |
| p = Path(path) | |
| logs = [] | |
| img = _pdf_to_image_first_page(p, p.parent, logs) | |
| return str(img) if img and img.exists() else None | |
| except Exception: | |
| return None | |
| def _compile_tex_to_pdf(tex_path: Path, logs): | |
| """Generic TeX compile helper for a .tex file. Returns Path to PDF or None.""" | |
| try: | |
| proj_dir = tex_path.parent | |
| import shutil as _sh, subprocess as _sp | |
| def _has(bin_name): | |
| return _sh.which(bin_name) is not None | |
| # Most-tolerant: latexmk first with pretex demoting errors; fallbacks inject pretex, too | |
| pretex = r"\nonstopmode\scrollmode\makeatletter\let\pgf@error\pgf@warning\let\GenericError\GenericWarning\let\PackageError\PackageWarning\makeatother" | |
| if _has("latexmk"): | |
| cmd = [ | |
| "latexmk", "-pdf", "-pdflatex=xelatex", "-f", | |
| "-interaction=nonstopmode", "-file-line-error", | |
| f"-pretex={pretex}", | |
| tex_path.name, | |
| ] | |
| logs.append("▶ Compiling with latexmk (-pdf -pdflatex=xelatex -f, pretex demote errors) …") | |
| elif _has("xelatex"): | |
| injected = pretex + f"\\input{{{tex_path.name}}}" | |
| cmd = ["xelatex", "-interaction=nonstopmode", "-file-line-error", injected] | |
| logs.append("▶ Compiling with xelatex (pretex injected) …") | |
| elif _has("lualatex"): | |
| injected = pretex + f"\\input{{{tex_path.name}}}" | |
| cmd = ["lualatex", "-interaction=nonstopmode", "-file-line-error", injected] | |
| logs.append("▶ Compiling with lualatex (pretex injected) …") | |
| else: | |
| logs.append("⚠️ No TeX engine found (xelatex/lualatex/latexmk).") | |
| return None | |
| import os as _os | |
| _env = _os.environ.copy() | |
| texinputs = _env.get("TEXINPUTS", "") | |
| search = _os.pathsep.join([ | |
| str(proj_dir), str(proj_dir) + "//", | |
| str(proj_dir.parent), str(proj_dir.parent) + "//", | |
| ]) | |
| _env["TEXINPUTS"] = search + _os.pathsep + texinputs | |
| proc = _sp.run(cmd, cwd=str(proj_dir), stdout=_sp.PIPE, stderr=_sp.STDOUT, text=True, env=_env) | |
| if proc.stdout: | |
| logs.append(proc.stdout[-4000:]) | |
| guess = proj_dir / (tex_path.stem + ".pdf") | |
| if proc.returncode != 0: | |
| # Be tolerant: if a PDF was produced despite errors, use it. | |
| if guess.exists(): | |
| logs.append(f"⚠️ Compile returned code {proc.returncode}, but PDF exists; using it.") | |
| return guess | |
| logs.append(f"❌ PDF compile failed with code {proc.returncode} and no PDF produced.") | |
| return None | |
| return guess if guess.exists() else None | |
| except Exception as e: | |
| logs.append(f"⚠️ PDF compile error: {e}") | |
| return None | |
| def _ensure_left_logo_or_disable(OUTPUT_DIR: Path, logs): | |
| """If no left_logo.* exists in logos/, comment out \logoleft line in poster_output.tex.""" | |
| tex_path = OUTPUT_DIR / "poster_latex_proj" / "poster_output.tex" | |
| logos_dir = OUTPUT_DIR / "poster_latex_proj" / "logos" | |
| try: | |
| if not tex_path.exists(): | |
| return False | |
| # any left_logo.* present? | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if has_left: | |
| return False | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| new_txt = re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\\logoleft in poster_output.tex.") | |
| return True | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed left_logo fallback: {e}") | |
| return False | |
| def debug_compile(): | |
| # Disabled minimal LaTeX debug; use the two pipeline-zip tests instead. | |
| return "<div style='color:#555'>Minimal debug disabled. Use 'Test repo output.zip' or 'Test last pipeline zip'.</div>" | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and return preview HTML + PDF path.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>" | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Ensure local fonts present in project and override theme to use them | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_proj/fonts/") | |
| # Copy repository theme .sty files into project root so they take precedence | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_proj/") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| # Copy repository theme .sty files into both root and the .tex dir so they take precedence | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_proj/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| # Append overrides into theme file(s) | |
| for sty in work_zip_dir.rglob("beamerthemegemini.sty"): | |
| with open(sty, "a", encoding="utf-8") as fh: | |
| fh.write("\n% === overrides inserted for local fonts ===\n") | |
| fh.write("\\makeatletter\\@ifpackageloaded{fontspec}{%\n") | |
| fh.write("\\IfFileExists{fonts/Raleway/Raleway-Regular.ttf}{\\renewfontfamily\\Raleway[Path=fonts/Raleway/, UprightFont=*-Regular, ItalicFont=*-Italic, BoldFont=*-Bold, BoldItalicFont=*-BoldItalic, Ligatures=TeX]{Raleway}}{}\n") | |
| fh.write("\\IfFileExists{fonts/Lato/Lato-Regular.ttf}{\\renewfontfamily\\Lato[Path=fonts/Lato/, UprightFont=*-Light, ItalicFont=*-LightItalic, BoldFont=*-Regular, BoldItalicFont=*-Italic, Ligatures=TeX]{Lato}\\setsansfont{Lato}[Path=fonts/Lato/, UprightFont=*-Light, ItalicFont=*-LightItalic, BoldFont=*-Regular, BoldItalicFont=*-Italic]}{}\n") | |
| fh.write("}{ }\\makeatother\n") | |
| logs.append(f"🛠️ Appended local font overrides in {sty.relative_to(work_zip_dir)}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| # Use served file path to avoid data: URI issues | |
| html = _pdf_preview_html(pdf_path, height="700px") | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>", None | |
| # Disable logoleft if missing; also ensure local fonts and override theme to use them | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| # Copy repo-local fonts into the zip project under ./fonts/, then append overrides into theme | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_upload/fonts/") | |
| for sty in work_zip_dir.rglob("beamerthemegemini.sty"): | |
| with open(sty, "a", encoding="utf-8") as fh: | |
| fh.write("\n% === overrides inserted for local fonts ===\n") | |
| fh.write("\\makeatletter\\@ifpackageloaded{fontspec}{%\n") | |
| fh.write("\\IfFileExists{fonts/Raleway/Raleway-Regular.ttf}{\\renewfontfamily\\Raleway[Path=fonts/Raleway/, UprightFont=*-Regular, ItalicFont=*-Italic, BoldFont=*-Bold, BoldItalicFont=*-BoldItalic, Ligatures=TeX]{Raleway}}{}\n") | |
| fh.write("\\IfFileExists{fonts/Lato/Lato-Regular.ttf}{\\renewfontfamily\\Lato[Path=fonts/Lato/, UprightFont=*-Light, ItalicFont=*-LightItalic, BoldFont=*-Regular, BoldItalicFont=*-Italic, Ligatures=TeX]{Lato}\\setsansfont{Lato}[Path=fonts/Lato/, UprightFont=*-Light, ItalicFont=*-LightItalic, BoldFont=*-Regular, BoldItalicFont=*-Italic]}{}\n") | |
| fh.write("}{ }\\makeatother\n") | |
| logs.append(f"🛠️ Appended local font overrides in {sty.relative_to(work_zip_dir)}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>" | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>" | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>" | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_output_zip(): | |
| """Compile the repo-root output.zip (a real LaTeX project) and preview the resulting PDF.""" | |
| # Stage repo output.zip to runs/<id>/output.zip to follow pipeline layout, then delegate | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>", | |
| None, | |
| ) | |
| logs = [f"🐞 Stage(repo zip) at {_now_str()}"] | |
| _, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| try: | |
| shutil.copy2(zip_path, ZIP_PATH) | |
| logs.append(f"📦 Staged repo output.zip → runs/{WORK_DIR.name}/output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| except Exception as e: | |
| logs.append(f"❌ Failed staging output.zip: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Failed to stage output.zip</div>" | |
| return debug_compile_last_pipeline_zip() | |
| logs = [f"🐞 Debug(real) at {_now_str()}"] | |
| zip_path = ROOT / "output.zip" | |
| if not zip_path.exists(): | |
| return ( | |
| "<div style='color:#b00'><b>output.zip not found at repo root.</b></div>" | |
| + f"<div>Expected at: {zip_path}</div>" | |
| ) | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_proj" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append("Unzipping output.zip → zip_proj/") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(zip_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate poster_output.tex (fallback to poster.tex) | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| # fallback: any .tex | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in output.zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in output.zip</div>", None | |
| # If left_logo missing, disable \logoleft | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in zip project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def _find_last_pipeline_zip(): | |
| try: | |
| candidates = [] | |
| for d in RUNS_DIR.iterdir(): | |
| try: | |
| if d.is_dir(): | |
| z = d / "output.zip" | |
| if z.exists(): | |
| candidates.append((z.stat().st_mtime, z)) | |
| except Exception: | |
| pass | |
| if not candidates: | |
| return None | |
| candidates.sort(key=lambda x: x[0], reverse=True) | |
| return candidates[0][1] | |
| except Exception: | |
| return None | |
| def debug_compile_last_pipeline_zip(): | |
| """Find the most recent runs/*/output.zip from pipeline, compile, and return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(last-pipeline-zip) at {_now_str()}"] | |
| last_zip = _find_last_pipeline_zip() | |
| if not last_zip: | |
| repo_zip = ROOT / "output.zip" | |
| if repo_zip.exists(): | |
| try: | |
| _, W, L, Z = _prepare_workspace(logs) | |
| shutil.copy2(repo_zip, Z) | |
| logs.append(f"📦 Auto-staged repo output.zip → runs/{W.name}/output.zip") | |
| last_zip = Z | |
| except Exception as e: | |
| logs.append(f"❌ Auto-stage failed: {e}") | |
| return "<div style='color:#b00'>No recent pipeline output.zip found and auto-stage failed.</div>" | |
| else: | |
| return "<div style='color:#b00'>No recent pipeline output.zip found under runs/.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_last" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| logs.append(f"Workspace: runs/{WORK_DIR.name}") | |
| logs.append(f"Using: {last_zip}") | |
| # Extract zip | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(last_zip, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Locate tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in last pipeline zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in last pipeline zip</div>", None | |
| # Ensure local fonts and theme precedence (same as other debug path) | |
| try: | |
| src_fonts = ROOT / "template" / "fonts" | |
| dst_fonts = work_zip_dir / "fonts" | |
| if src_fonts.exists(): | |
| for root_dir, dirs, files in os.walk(src_fonts): | |
| rel = Path(root_dir).relative_to(src_fonts) | |
| out_dir = dst_fonts / rel | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for fn in files: | |
| if fn.lower().endswith((".ttf", ".otf")): | |
| shutil.copy2(Path(root_dir)/fn, out_dir/fn) | |
| logs.append("📁 Copied local fonts → zip_last/fonts/") | |
| # Copy repository theme .sty next to tex and at root | |
| try: | |
| tmpl_dir = ROOT / "template" | |
| for sty in tmpl_dir.glob("*.sty"): | |
| shutil.copy2(sty, work_zip_dir / sty.name) | |
| shutil.copy2(sty, tex_path.parent / sty.name) | |
| logs.append("📄 Copied template/*.sty → zip_last/ and tex dir") | |
| except Exception as e: | |
| logs.append(f"⚠️ Copy sty failed: {e}") | |
| except Exception as e: | |
| logs.append(f"⚠️ Local font setup failed: {e}") | |
| # Compile to PDF | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile last pipeline zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return html, str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| def debug_compile_uploaded_zip(zip_file): | |
| """Compile an uploaded poster zip (user-provided); return preview HTML + PDF path.""" | |
| logs = [f"🐞 Debug(upload) at {_now_str()}"] | |
| if not zip_file: | |
| return "<div style='color:#b00'>Please upload a .zip file first.</div>", None | |
| # Prepare workspace | |
| run_id, WORK_DIR, LOG_PATH, _ = _prepare_workspace(logs) | |
| work_zip_dir = WORK_DIR / "zip_upload" | |
| work_zip_dir.mkdir(parents=True, exist_ok=True) | |
| # Save uploaded zip | |
| up_path = work_zip_dir / "input.zip" | |
| try: | |
| shutil.copy(zip_file.name, up_path) | |
| except Exception as e: | |
| logs.append(f"❌ save upload failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Save upload failed.</div>", None | |
| # Extract | |
| try: | |
| import zipfile as _zf | |
| with _zf.ZipFile(up_path, 'r') as zf: | |
| zf.extractall(work_zip_dir) | |
| except Exception as e: | |
| logs.append(f"❌ unzip failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>Unzip failed.</div>", None | |
| # Find tex | |
| tex_path = None | |
| for name in ("poster_output.tex", "poster.tex"): | |
| cand = list(work_zip_dir.rglob(name)) | |
| if cand: | |
| tex_path = cand[0] | |
| break | |
| if tex_path is None: | |
| cand = list(work_zip_dir.rglob("*.tex")) | |
| if cand: | |
| tex_path = cand[0] | |
| if tex_path is None: | |
| logs.append("❌ No .tex file found in uploaded zip") | |
| _write_logs(LOG_PATH, logs) | |
| return "<div style='color:#b00'>No .tex found in uploaded zip</div>" | |
| # Disable logoleft if missing | |
| try: | |
| logos_dir = tex_path.parent / "logos" | |
| has_left = False | |
| if logos_dir.exists(): | |
| for p in logos_dir.iterdir(): | |
| if p.is_file() and p.stem == "left_logo": | |
| has_left = True | |
| break | |
| if not has_left: | |
| txt = tex_path.read_text(encoding="utf-8") | |
| if "\\logoleft" in txt: | |
| import re as _re | |
| new_txt = _re.sub(r"^\\\s*logoleft\s*\{.*?\}\s*$", lambda m: "%" + m.group(0), txt, flags=_re.MULTILINE) | |
| if new_txt != txt: | |
| tex_path.write_text(new_txt, encoding="utf-8") | |
| logs.append("ℹ️ No left_logo found; disabled \\logoleft in uploaded project.") | |
| except Exception as e: | |
| logs.append(f"⚠️ left_logo adjust failed: {e}") | |
| # Compile | |
| pdf_path = _compile_tex_to_pdf(tex_path, logs) | |
| if not pdf_path or not pdf_path.exists(): | |
| logs.append("❌ Failed to compile uploaded zip PDF.") | |
| _write_logs(LOG_PATH, logs) | |
| return ( | |
| "<div style='color:#b00'><b>Compile failed.</b></div>" | |
| + "<pre style='white-space:pre-wrap;background:#f7f7f8;padding:8px;border-radius:6px'>" | |
| + "\n".join(logs) | |
| + "</pre>", | |
| None, | |
| ) | |
| try: | |
| b64 = base64.b64encode(pdf_path.read_bytes()).decode("utf-8") | |
| open_tab = f"<a target='_blank' rel='noopener' href='data:application/pdf;base64,{b64}'>Open PDF in new tab</a>" | |
| html = ( | |
| f"<div style='margin-bottom:8px'>{open_tab}</div>" | |
| + _pdf_to_iframe_html(pdf_path, height="700px") | |
| ) | |
| _write_logs(LOG_PATH, logs) | |
| return "", str(pdf_path) | |
| except Exception as e: | |
| logs.append(f"⚠️ preview failed: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| return f"<div>Compiled but preview failed: {e}</div>", None | |
| # ===================== | |
| # Gradio pipeline function (ISOLATED) | |
| # ===================== | |
| def run_pipeline(arxiv_url, pdf_file, openai_key, logo_files, meeting_logo_file, theme_rgb): | |
| _cleanup_old_runs(RETENTION_HOURS) | |
| start_time = datetime.datetime.now() | |
| logs = [f"🚀 Starting pipeline at {_now_str()}"] | |
| # --- Prepare per-run workspace --- | |
| run_id, WORK_DIR, LOG_PATH, ZIP_PATH = _prepare_workspace(logs) | |
| INPUT_DIR = WORK_DIR / "input" | |
| OUTPUT_DIR = WORK_DIR / "output" | |
| LOGO_DIR = INPUT_DIR / "logo" | |
| POSTER_LATEX_DIR = WORK_DIR / "posterbuilder" / "latex_proj" | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| # ====== Validation: must upload LOGO ====== | |
| if logo_files is None: | |
| logo_files = [] | |
| if not isinstance(logo_files, (list, tuple)): | |
| logo_files = [logo_files] | |
| logo_files = [f for f in logo_files if f] | |
| # if len(logo_files) == 0: | |
| # msg = "❌ You must upload at least one institutional logo (multiple allowed)." | |
| # logs.append(msg) | |
| # _write_logs(LOG_PATH, logs) | |
| # yield "\n".join(logs), "", None, "" | |
| # return | |
| # Save logos into run-local dir | |
| for item in LOGO_DIR.iterdir(): | |
| if item.is_file(): | |
| item.unlink() | |
| saved_logo_paths = [] | |
| for lf in logo_files: | |
| p = LOGO_DIR / Path(lf.name).name | |
| shutil.copy(lf.name, p) | |
| saved_logo_paths.append(p) | |
| logs.append(f"🏷️ Saved {len(saved_logo_paths)} logo file(s) → {LOGO_DIR.relative_to(WORK_DIR)}") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| # ====== Handle uploaded PDF (optional) ====== | |
| pdf_path = None | |
| if pdf_file: | |
| pdf_dir = INPUT_DIR / "pdf" | |
| pdf_dir.mkdir(parents=True, exist_ok=True) | |
| pdf_path = pdf_dir / Path(pdf_file.name).name | |
| shutil.copy(pdf_file.name, pdf_path) | |
| logs.append(f"📄 Uploaded PDF → {pdf_path.relative_to(WORK_DIR)}") | |
| # For pipeline Step 1.5 compatibility: also copy to input/paper.pdf | |
| canonical_pdf = INPUT_DIR / "paper.pdf" | |
| shutil.copy(pdf_file.name, canonical_pdf) | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| # ====== Validate input source ====== | |
| if not arxiv_url and not pdf_file: | |
| msg = "❌ Please provide either an arXiv link or upload a PDF file (choose one)." | |
| logs.append(msg) | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| # ====== Build command (run INSIDE workspace) ====== | |
| cmd = [ | |
| sys.executable, "pipeline.py", | |
| "--model_name_t", "gpt-5", | |
| "--model_name_v", "gpt-5", | |
| "--result_dir", "output", | |
| "--paper_latex_root", "input/latex_proj", | |
| "--openai_key", openai_key, | |
| "--gemini_key", "##", | |
| "--logo_dir", str(LOGO_DIR) # run-local logo dir | |
| ] | |
| if arxiv_url: | |
| cmd += ["--arxiv_url", arxiv_url] | |
| # (Keep pdf via input/paper.pdf; pipeline will read it if exists) | |
| logs.append("\n======= REAL-TIME LOG =======") | |
| logs.append(f"cwd = runs/{WORK_DIR.name}") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| # ====== Run with REAL-TIME streaming, inside workspace ====== | |
| try: | |
| process = subprocess.Popen( | |
| cmd, | |
| cwd=str(WORK_DIR), | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| bufsize=1, | |
| universal_newlines=True, | |
| ) | |
| except Exception as e: | |
| msg = f"❌ Pipeline failed to start: {e}" | |
| logs.append(msg) | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| last_yield = time.time() | |
| try: | |
| while True: | |
| # Timeout guard | |
| if (datetime.datetime.now() - start_time).total_seconds() > TIMEOUT_SECONDS: | |
| logs.append("❌ Pipeline timed out (30 min limit). Killing process…") | |
| try: | |
| process.kill() | |
| except Exception: | |
| pass | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| line = process.stdout.readline() | |
| if line: | |
| print(line, end="") # echo to Space logs | |
| logs.append(line.rstrip("\n")) | |
| _write_logs(LOG_PATH, logs) | |
| now = time.time() | |
| if now - last_yield >= 0.3: | |
| last_yield = now | |
| yield "\n".join(logs), "", None, None, "" | |
| elif process.poll() is not None: | |
| break | |
| else: | |
| time.sleep(0.05) | |
| return_code = process.wait() | |
| logs.append(f"\nProcess finished with code {return_code}") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| if return_code != 0: | |
| logs.append("❌ Process exited with non-zero status. See logs above.") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| except Exception as e: | |
| logs.append(f"❌ Error during streaming: {e}") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| finally: | |
| try: | |
| if process.stdout: | |
| process.stdout.close() | |
| except Exception: | |
| pass | |
| # ====== Check output ====== | |
| has_output = False | |
| try: | |
| if OUTPUT_DIR.exists(): | |
| for _ in OUTPUT_DIR.iterdir(): | |
| has_output = True | |
| break | |
| except FileNotFoundError: | |
| has_output = False | |
| if not has_output: | |
| msg = "❌ No output generated. Please check logs above." | |
| logs.append(msg) | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| return | |
| # ====== NEW: Post-processing (optional features) ====== | |
| # 1) Optional meeting logo replacement; if not provided, apply default NeurIPS | |
| applied_logo = _apply_meeting_logo(OUTPUT_DIR, meeting_logo_file, logs) | |
| if not applied_logo: | |
| _ensure_right_logo_default(OUTPUT_DIR, logs) | |
| # 2) Optional theme color update | |
| rgb_tuple = _parse_rgb(theme_rgb) | |
| if theme_rgb and not rgb_tuple: | |
| logs.append(f"⚠️ Ignored Theme RGB input '{theme_rgb}': expected like '94,46,145'.") | |
| applied_rgb = _apply_theme_rgb(OUTPUT_DIR, rgb_tuple, logs) if rgb_tuple else False | |
| # 3) Optional institutional logo -> left_logo.<ext> | |
| _apply_left_logo(OUTPUT_DIR, logo_files, logs) | |
| _ensure_left_logo_or_disable(OUTPUT_DIR, logs) | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), "", None, None, "" | |
| # ====== Compile PDF (for download + image preview) ====== | |
| pdf_html = "" | |
| compiled_pdf_file = None | |
| try: | |
| pdf_path = _compile_poster_pdf(OUTPUT_DIR, logs) | |
| if pdf_path and pdf_path.exists(): | |
| # Prefer file-served iframe to avoid large data: URIs and browser blocks | |
| pdf_html = _pdf_to_iframe_file(pdf_path) | |
| compiled_pdf_file = str(pdf_path) | |
| logs.append("🖨️ PDF compiled (image preview available).") | |
| except Exception as e: | |
| logs.append(f"⚠️ PDF compile skipped: {e}") | |
| # ====== Zip output (run-local) ====== | |
| try: | |
| target_dir = OUTPUT_DIR / "poster_latex_proj" | |
| if not target_dir.exists(): | |
| logs.append("❌ poster_latex_proj folder not found") | |
| else: | |
| with zipfile.ZipFile(ZIP_PATH, 'w', zipfile.ZIP_DEFLATED) as zipf: | |
| for root, dirs, files in os.walk(target_dir): | |
| for file in files: | |
| file_path = Path(root) / file | |
| arcname = file_path.relative_to(target_dir) # only relative to subfolder | |
| zipf.write(file_path, arcname=arcname) | |
| logs.append(f"✅ Zipped poster_latex_proj → {ZIP_PATH.relative_to(WORK_DIR)}") | |
| except Exception as e: | |
| logs.append(f"❌ Failed to create zip: {e}") | |
| # ====== Prepare Overleaf base64 payload (optional) ====== | |
| overleaf_zip_b64 = "" | |
| try: | |
| with open(ZIP_PATH, "rb") as f: | |
| overleaf_zip_b64 = base64.b64encode(f.read()).decode("utf-8") | |
| logs.append("🔗 Prepared Overleaf base64 payload") | |
| except Exception as e: | |
| logs.append(f"⚠️ Failed Overleaf payload: {e}") | |
| end_time = datetime.datetime.now() | |
| dur = (end_time - start_time).seconds | |
| logs.append(f"🏁 Completed at {_now_str()} (Duration: {dur}s)") | |
| logs.append(f"🆔 run_id = {WORK_DIR.name}") | |
| _write_logs(LOG_PATH, logs) | |
| yield "\n".join(logs), ( | |
| pdf_html | |
| ), ( | |
| compiled_pdf_file | |
| ), ( | |
| str(ZIP_PATH) if ZIP_PATH.exists() else None | |
| ), render_overleaf_button(overleaf_zip_b64) | |
| def debug_compile(): | |
| # Minimal debug disabled to simplify UI. | |
| return "<div style='color:#555'>Minimal debug disabled. Use 'Test repo output.zip' or 'Test last pipeline zip'.</div>" | |
| # ===================== | |
| # Gradio UI | |
| # ===================== | |
| with gr.Blocks(title="🎓 Paper2Poster") as iface: | |
| # Title | |
| gr.Markdown("# 🎓 Paper2Poster") | |
| gr.Markdown(""" | |
| [Paper](https://arxiv.org/abs/2505.21497) | [GitHub](https://github.com/Paper2Poster/Paper2Poster) | [Project Page](https://paper2poster.github.io/) | |
| **TL;DR:** Upload your paper and get an auto-generated poster. | |
| Please be patient — each paper takes about 8–10 minutes to process. | |
| This work, developed in collaboration with [TVG@Oxford](https://torrvision.com/index.html) and [UWaterloo](https://uwaterloo.ca/), has been accepted to [NeurIPS 2025 D&B](https://neurips.cc/). The framework builds upon 🐪 [CAMEL-ai](https://github.com/camel-ai/camel). | |
| """, elem_id="intro-md") | |
| # Top-right logos (camel, tvg, waterloo) if available | |
| gr.HTML(_ui_header_logos_html()) | |
| # Note: CAMEL line merged into the Markdown above to keep it on the same line. | |
| # -------- Input box -------- | |
| with gr.Row(): | |
| # ========== LEFT: INPUT ========== | |
| with gr.Column(scale=1): | |
| with gr.Accordion("Input", open=True): | |
| arxiv_in = gr.Textbox(label="📘 ArXiv URL (choose one)", placeholder="https://arxiv.org/abs/2505.xxxxx") | |
| pdf_in = gr.File(label="📄 Upload PDF (choose one)") | |
| key_in = gr.Textbox(label="🔑 OpenAI API Key", placeholder="sk-...", type="password") | |
| inst_logo_in = gr.File( | |
| label="🏷️ Institutional Logo (optional, multiple allowed)", | |
| file_count="multiple", | |
| file_types=["image"], | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| conf_logo_in = gr.File( | |
| label="🧩 Optional: Conference Logo (defaults to NeurIPS logo)", | |
| file_count="single", | |
| file_types=["image"], | |
| ) | |
| with gr.Column(): | |
| _conf_path = _default_conf_logo_path() | |
| conf_preview = gr.Image( | |
| value=str(_conf_path) if _conf_path else None, | |
| label="Default conference logo preview", | |
| interactive=False, | |
| ) | |
| theme_in = gr.ColorPicker(label="🎨 Theme Color (optional)", value="#5E2E91") | |
| run_btn = gr.Button("🚀 Run", variant="primary") | |
| # ========== RIGHT: OUTPUT ========== | |
| with gr.Column(scale=1): | |
| with gr.Accordion("Output", open=True): | |
| # Preview on top | |
| img_out = gr.Image(label="🖼️ Poster (Image Preview)", interactive=False) | |
| # Logs in the middle (keep compact height) | |
| logs_out = gr.Textbox(label="🧾 Logs", lines=10, max_lines=20) | |
| # Downloads at bottom | |
| pdf_out = gr.HTML(label="📄 Poster (PDF Preview)", visible=False) | |
| with gr.Row(): | |
| pdf_file_out = gr.File(label="📄 Download Poster (PDF)", interactive=False, visible=True) | |
| zip_out = gr.File(label="📦 Download Results (.zip)", interactive=False, visible=True) | |
| gr.Markdown("The ZIP can be uploaded to Overleaf and compiled with XeLaTeX.") | |
| overleaf_out = gr.HTML(label="Open in Overleaf") | |
| # Debug (hidden) | |
| debug_zip_btn= gr.Button("🐞 Test repo output.zip", variant="secondary", visible=False) | |
| debug_zip_out= gr.HTML(label="🐞 Real Output Preview", visible=False) | |
| debug_zip_img= gr.Image(label="🐞 Real Output Image", interactive=False, visible=False) | |
| debug_zip_pdfpath = gr.Textbox(visible=False) | |
| debug_last_btn= gr.Button("🐞 Test last pipeline zip", variant="secondary", visible=False) | |
| debug_last_out= gr.HTML(label="🐞 Last Pipeline Preview", visible=False) | |
| debug_last_img= gr.Image(label="🐞 Last Output Image", interactive=False, visible=False) | |
| debug_last_pdfpath = gr.Textbox(visible=False) | |
| _run_evt = run_btn.click( | |
| fn=run_pipeline, | |
| inputs=[arxiv_in, pdf_in, key_in, inst_logo_in, conf_logo_in, theme_in], | |
| outputs=[logs_out, pdf_out, pdf_file_out, zip_out, overleaf_out], | |
| ) | |
| _run_evt.then(fn=preview_image_from_pdf, inputs=[pdf_file_out], outputs=[img_out]) | |
| _dz = debug_zip_btn.click(fn=debug_compile_output_zip, inputs=[], outputs=[debug_zip_out, debug_zip_pdfpath]) | |
| _dz.then(fn=preview_image_from_pdf, inputs=[debug_zip_pdfpath], outputs=[debug_zip_img]) | |
| _dl = debug_last_btn.click(fn=debug_compile_last_pipeline_zip, inputs=[], outputs=[debug_last_out, debug_last_pdfpath]) | |
| _dl.then(fn=preview_image_from_pdf, inputs=[debug_last_pdfpath], outputs=[debug_last_img]) | |
| if __name__ == "__main__": | |
| iface.launch(server_name="0.0.0.0", server_port=7860) | |