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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user