109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
"""Jinja → HTML → Playwright headless screenshot."""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
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()
|
|
|
|
# template 파일이 없으면 default로 폴백 (INSTA_DEFAULT_THEME가 import 안 된 theme이면 안전)
|
|
template_full = Path(_resolve_template_dir()) / template
|
|
if not template_full.exists():
|
|
logger.warning("Template '%s' 없음 → 'default/card.html.j2'로 폴백", template)
|
|
template = "default/card.html.j2"
|
|
|
|
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
|