diff --git a/insta-lab/app/card_renderer.py b/insta-lab/app/card_renderer.py new file mode 100644 index 0000000..5bbfc71 --- /dev/null +++ b/insta-lab/app/card_renderer.py @@ -0,0 +1,100 @@ +"""Jinja → HTML → Playwright headless screenshot.""" + +import asyncio +import hashlib +import json +import logging +import os +import tempfile +from typing import List + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from playwright.async_api import async_playwright + +from .config import CARDS_DIR, CARD_TEMPLATE_DIR +from . import db + +logger = logging.getLogger(__name__) + + +def _resolve_template_dir() -> str: + """Prefer config CARD_TEMPLATE_DIR if it exists; else fall back to in-repo templates/.""" + if os.path.isdir(CARD_TEMPLATE_DIR): + return CARD_TEMPLATE_DIR + return os.path.join(os.path.dirname(__file__), "templates") + + +def _env() -> Environment: + return Environment( + loader=FileSystemLoader(_resolve_template_dir()), + autoescape=select_autoescape(["html", "j2"]), + ) + + +def _slate_dir(slate_id: int) -> str: + out = os.path.join(CARDS_DIR, str(slate_id)) + os.makedirs(out, exist_ok=True) + return out + + +def _build_pages(slate: dict) -> List[dict]: + cover = json.loads(slate["cover_copy"] or "{}") + bodies = json.loads(slate["body_copies"] or "[]") + cta = json.loads(slate["cta_copy"] or "{}") + accent = cover.get("accent_color") or "#0F62FE" + pages: List[dict] = [] + pages.append({ + "page_type": "cover", "page_no": 1, "total_pages": 10, + "headline": cover.get("headline", ""), "body": cover.get("body", ""), + "accent_color": accent, "cta": "", + }) + for i, b in enumerate(bodies[:8]): + pages.append({ + "page_type": "body", "page_no": i + 2, "total_pages": 10, + "headline": b.get("headline", ""), "body": b.get("body", ""), + "accent_color": accent, "cta": "", + }) + pages.append({ + "page_type": "cta", "page_no": 10, "total_pages": 10, + "headline": cta.get("headline", ""), "body": cta.get("body", ""), + "accent_color": accent, "cta": cta.get("cta", ""), + }) + return pages + + +async def render_slate(slate_id: int, template: str = "default/card.html.j2") -> List[str]: + slate = db.get_card_slate(slate_id) + if not slate: + raise ValueError(f"slate {slate_id} not found") + env = _env() + tmpl = env.get_template(template) + pages = _build_pages(slate) + out_dir = _slate_dir(slate_id) + paths: List[str] = [] + + async with async_playwright() as p: + browser = await p.chromium.launch() + try: + ctx = await browser.new_context(viewport={"width": 1080, "height": 1350}) + page = await ctx.new_page() + for spec in pages: + html_str = tmpl.render(**spec) + with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(html_str) + html_path = f.name + try: + await page.goto(f"file://{html_path}", wait_until="networkidle") + out_path = os.path.join(out_dir, f"{spec['page_no']:02d}.png") + await page.screenshot(path=out_path, full_page=False, omit_background=False) + with open(out_path, "rb") as fp: + file_hash = hashlib.md5(fp.read()).hexdigest() + db.add_card_asset(slate_id, spec["page_no"], out_path, file_hash) + paths.append(out_path) + finally: + try: + os.unlink(html_path) + except OSError: + pass + finally: + await browser.close() + return paths diff --git a/insta-lab/app/templates/default/card.html.j2 b/insta-lab/app/templates/default/card.html.j2 new file mode 100644 index 0000000..836c3cb --- /dev/null +++ b/insta-lab/app/templates/default/card.html.j2 @@ -0,0 +1,55 @@ + + +
+ + + + +{{ body }}
+