콘텐츠로 이동
quickthumb
전체 문서

Shorts / 세로형 커버

잘린 사진을 보여 주는 기울어진 휴대전화와 "Turn long videos into Shorts"라는 문구가 있는 세로형 커버

레이아웃 전체를 JSON으로 작성한 1080×1920 커버입니다. Python은 이 파일을 읽어 렌더링만 합니다. LLM이나 템플릿 시스템으로 레이아웃을 생성할 때도 같은 방식으로 quickthumb를 사용할 수 있습니다.

예제를 실행하려면 uv를 설치하고 저장소를 복제하세요.

git clone https://github.com/sjquant/quickthumb
cd quickthumb
uv run python examples/shorts_cover_agent.py

스크립트는 examples/shorts_cover_agent.png를 기록하며 같은 이름의 기존 파일을 덮어씁니다.

구성 방식

  • 같은 사진을 두 군데에 씁니다. 한 곳은 가로 16:9로 자르고, 다른 곳은 휴대전화 화면 안에 넣습니다. "fit": "cover"가 각 프레임 모양에 맞게 사진을 잘라 줍니다.
  • 휴대전화는 두 레이어입니다. 어둡고 둥근 사각형이 본체이고 위의 사진이 화면입니다. 두 레이어와 화면의 라벨 세 개가 모두 "rotation": 3을 사용하므로 함께 기울어집니다.
  • 경로는 저장소 루트를 기준으로 합니다. 예를 들어 assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg입니다. 렌더링 전에 스크립트가 저장소 루트로 이동하므로 어느 디렉터리에서도 실행할 수 있습니다.
  • 배경에 약간의 입자를 더했습니다. 고정 seed가 있는 grain 효과로 렌더링할 때마다 같은 질감을 만듭니다.

명세

{
  "kind": "canvas",
  "width": 1080,
  "height": 1920,
  "layers": [
    {
      "type": "background",
      "color": "#F5F5F7",
      "effects": [
        { "type": "grain", "intensity": 0.006, "monochrome": true, "seed": 9 }
      ]
    },
    {
      "type": "text",
      "content": "JSON WORKFLOW  /  01",
      "font": "Roboto",
      "size": 24,
      "color": "#6E6E73",
      "position": ["8%", "7%"],
      "weight": 700,
      "letter_spacing": 3
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["8%", "10%"],
      "width": 908,
      "height": 1,
      "color": "#1D1D1F26"
    },
    {
      "type": "text",
      "content": [
        { "text": "TURN LONG VIDEOS\nINTO ", "color": "#1D1D1F", "weight": 700 },
        { "text": "SHORTS.", "color": "#0066CC", "weight": 700 }
      ],
      "font": "Roboto",
      "size": 102,
      "position": ["8%", "16%"],
      "max_width": "84%",
      "line_height": 1.0,
      "letter_spacing": -2
    },
    {
      "type": "text",
      "content": "JSON in.\nCover out.",
      "font": "Roboto",
      "size": 42,
      "color": "#6E6E73",
      "position": ["8%", "43%"],
      "line_height": 1.18
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["0%", "50%"],
      "width": 1080,
      "height": 720,
      "color": "#0066CC"
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["77%", "56%"],
      "width": 430,
      "height": 850,
      "color": "#0A0A0A",
      "border_radius": 50,
      "rotation": 3,
      "align": "center",
      "effects": [
        { "type": "stroke", "width": 1, "color": "#D2D2D7" },
        { "type": "shadow", "offset_x": 0, "offset_y": 22, "color": "#00000038", "blur_radius": 32 }
      ]
    },
    {
      "type": "image",
      "path": "assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg",
      "position": ["77%", "56%"],
      "width": 376,
      "height": 728,
      "fit": "cover",
      "rotation": 3,
      "align": "center",
      "border_radius": 34,
      "effects": [
        { "type": "filter", "brightness": 0.9, "contrast": 1.04, "saturation": 0.72, "blur": 0 }
      ]
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["8%", "64%"],
      "width": 430,
      "height": 242,
      "color": "#FFFFFF14",
      "border_radius": 18,
      "effects": [{ "type": "stroke", "width": 2, "color": "#FFFFFF" }]
    },
    {
      "type": "text",
      "content": "16:9  SOURCE",
      "font": "Roboto",
      "size": 24,
      "color": "#FFFFFF",
      "position": ["8%", "60%"],
      "weight": 700,
      "letter_spacing": 2
    },
    {
      "type": "image",
      "path": "assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg",
      "position": ["8%", "64%"],
      "width": 430,
      "height": 242,
      "fit": "cover",
      "border_radius": 18,
      "effects": [
        { "type": "filter", "brightness": 0.76, "contrast": 1.08, "saturation": 0.58, "blur": 0 }
      ]
    },
    {
      "type": "text",
      "content": "TO  /  9:16 × 3",
      "font": "Roboto",
      "size": 40,
      "color": "#FFFFFF",
      "position": ["8%", "80%"],
      "weight": 700,
      "letter_spacing": 2
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["77%", "47%"],
      "width": 330,
      "height": 2,
      "color": "#FFFFFFCC",
      "rotation": 3,
      "align": "center"
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["77%", "58%"],
      "width": 330,
      "height": 2,
      "color": "#FFFFFFCC",
      "rotation": 3,
      "align": "center"
    },
    {
      "type": "text",
      "content": "01  HOOK",
      "font": "Roboto",
      "size": 18,
      "color": "#FFFFFF",
      "position": ["65%", "41%"],
      "weight": 700,
      "letter_spacing": 2,
      "rotation": 3,
      "effects": [{ "type": "shadow", "offset_x": 0, "offset_y": 2, "color": "#000000CC", "blur_radius": 5 }]
    },
    {
      "type": "text",
      "content": "02  CUT",
      "font": "Roboto",
      "size": 18,
      "color": "#FFFFFF",
      "position": ["65%", "52%"],
      "weight": 700,
      "letter_spacing": 2,
      "rotation": 3,
      "effects": [{ "type": "shadow", "offset_x": 0, "offset_y": 2, "color": "#000000CC", "blur_radius": 5 }]
    },
    {
      "type": "text",
      "content": "03  CAPTION",
      "font": "Roboto",
      "size": 18,
      "color": "#FFFFFF",
      "position": ["65%", "63%"],
      "weight": 700,
      "letter_spacing": 2,
      "rotation": 3,
      "effects": [{ "type": "shadow", "offset_x": 0, "offset_y": 2, "color": "#000000CC", "blur_radius": 5 }]
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["8%", "88%"],
      "width": 908,
      "height": 1,
      "color": "#1D1D1F26"
    },
    {
      "type": "text",
      "content": [
        { "text": "AGENT EMITS JSON", "color": "#1D1D1F", "weight": 700 },
        { "text": "  /  QUICKTHUMB RENDERS", "color": "#6E6E73", "weight": 500 }
      ],
      "font": "Roboto",
      "size": 28,
      "position": ["8%", "91%"],
      "max_width": "84%"
    }
  ]
}

스크립트

"""
JSON-first vertical cover example.

This script simulates the workflow where an AI agent emits a quickthumb JSON spec
and the application renders it with `Canvas.from_json(...)`.
"""

import os

from quickthumb import Canvas

FILE_DIR = os.path.dirname(__file__)
REPO_DIR = os.path.abspath(os.path.join(FILE_DIR, ".."))
SPEC_PATH = os.path.join(FILE_DIR, "shorts_cover_agent.json")
OUTPUT_PATH = os.path.join(FILE_DIR, "shorts_cover_agent.png")

os.environ["QUICKTHUMB_FONT_DIR"] = os.path.join(REPO_DIR, "assets", "fonts")
os.environ["QUICKTHUMB_DEFAULT_FONT"] = "Roboto"

with open(SPEC_PATH, encoding="utf-8") as f:
    json_spec = f.read()

previous_cwd = os.getcwd()

try:
    # The checked-in JSON spec uses repo-relative asset paths to stay easy to read.
    os.chdir(REPO_DIR)
    Canvas.from_json(json_spec).render(OUTPUT_PATH)
finally:
    os.chdir(previous_cwd)

print(f"✓ JSON-first Shorts cover created: {OUTPUT_PATH}")
print(f"  Spec source: {SPEC_PATH}")

Canvas.from_json()은 그리기 전에 명세를 검증하므로 형식이 잘못되면 해당 필드를 가리키는 ValidationError가 발생합니다. 사용 가능한 전체 필드는 JSON Schema와 AI 작업 흐름을 참고하세요.