feat(insta-lab): 슬레이트 zip 패키지 다운로드 API (10 PNG + caption.txt)

GET /api/insta/slates/{slate_id}/package 엔드포인트 추가.
렌더된 card_assets PNG들 + suggested_caption + hashtags를
단일 zip으로 번들해 StreamingResponse 반환.
hashtags JSON 문자열/리스트 방어 파싱 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:58:32 +09:00
parent bb0280274e
commit 3a9d6e986e
2 changed files with 77 additions and 1 deletions

View File

@@ -1,14 +1,16 @@
"""FastAPI entrypoint for insta-lab."""
import asyncio
import io
import json
import logging
import os
import zipfile
from typing import Optional
from fastapi import FastAPI, HTTPException, BackgroundTasks, Body, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
from _shared.access_log import install as install_access_log
@@ -247,6 +249,35 @@ def get_asset(slate_id: int, page: int):
return FileResponse(match["file_path"], media_type="image/png")
@app.get("/api/insta/slates/{slate_id}/package")
def download_package(slate_id: int):
slate = db.get_card_slate(slate_id)
if not slate:
raise HTTPException(404, "slate not found")
assets = sorted(db.list_card_assets(slate_id), key=lambda a: a["page_index"])
if not assets:
raise HTTPException(409, "아직 렌더된 카드가 없습니다")
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
for a in assets:
fp = a["file_path"]
if os.path.exists(fp):
z.write(fp, arcname=f"{a['page_index']:02d}.png")
caption = (slate.get("suggested_caption") or "").strip()
tags = slate.get("hashtags") or []
if isinstance(tags, str):
try:
tags = json.loads(tags)
except Exception:
tags = []
caption_full = caption + ("\n\n" + " ".join(tags) if tags else "")
z.writestr("caption.txt", caption_full)
buf.seek(0)
return StreamingResponse(buf, media_type="application/zip", headers={
"Content-Disposition": f'attachment; filename="insta_slate_{slate_id}.zip"'
})
@app.delete("/api/insta/slates/{slate_id}")
def delete_slate(slate_id: int):
if not db.get_card_slate(slate_id):