브랜드 세이프티
광고 차단은 광고 소재 하나를 대상으로 하는 프로그래매틱 브랜드 세이프티 심사입니다. 소재와 자연어 정책 규칙 목록(선택)을 보내면, 서비스가 결정적인 block / allow 판정과 함께 걸린 GARM 카테고리, 일치한 규칙을 반환합니다. 동기 호출 한 번으로 끝나며 폴링이나 콜백이 없습니다.
POST /api/v1/ad-blocking — multipart 업로드, 응답에 판정이 담깁니다.
POST /api/v1.1/ad-blocking — 광고 레지스트리의 ad_id를 담은 JSON 본문을 보냅니다.
GET /api/v1.1/ad-blocking — 타임아웃으로 놓친 결과를 되찾습니다.
AI가 생성한 근거, 결정적인 판정, 그리고 직접 검토가 필요한 지점을 설명합니다.
심사 방식
심사 요청은 동기 호출 한 번 안에서 다음을 수행합니다.
- 소재를 저장하고, 필요하면 분석기에 맞게 트랜스코딩합니다.
- 오디오와 키프레임을 추출합니다.
- 멀티모달 분석을 한 번 실행해 11개 GARM 브랜드 세이프티 카테고리와 모든 정책 규칙을 함께 평가합니다. 정책 규칙은 팀에 저장된 차단 목록과 요청에 담긴 임시
rules입니다. - 근거로부터 코드에서 판정을 계산합니다. GARM 카테고리가 하나 이상 걸렸거나 또는 정책 규칙이 하나 이상 일치한 경우에만
block_decision이block입니다. 그 외에는allow입니다. - 결과를 저장하고 반환합니다.
설계 시 고려할 특성은 다음과 같습니다.
- 동기 방식. 일반적인 지연 시간은 몇 초이며, 대부분 멀티모달 분석에 걸립니다.
- 결정적인 판정. 추론은 모델이 하지만, 판 정은
garm과blocklist.hits로부터 코드에서 계산합니다. 따라서 판정은 함께 반환되는 근거와 항상 일치합니다. 근거는 AI 출력이라 어느 방향으로든 틀릴 수 있습니다.block은 "검토 대기"로 다루고, 중요한 건에는 반드시 사람이 개입하세요. 데이터 처리를 참고하세요. - 페일 클로즈드. 분석 자체에서 오류가 나도
block_decision: "block"과screening_failed: true를 담은200을 받습니다. 따라서block_decision만 보고 분기하는 연동도 소재를 송출하지 않고 보류합니다. - GARM은 항상 평가합니다. 규칙이 없어도 마찬가지입니다. 규칙은 그 위에 더하는 정책입니다.
- 길이 상한. 소재는 약 120초로 제한됩니다.
업로드한 파일 심사
POST /api/v1/ad-blocking은 multipart/form-data를 받습니다.
filefilebodyrequired광고 영상 파일(mp4 / mov / ...)이며, 최대 ~120초입니다.
titlestringbody감사 기록과 목록에 표시할 제목입니다.
rulesstringbody자연어로 작성한 임시 정책 규칙으로, 쉼표나 줄바꿈으로 구분합니다. 팀에 저장된 차단 목록(대시보드 Ad Blocking 탭에서 관리)에 더해 평가합니다.
- cURL
- Python
curl -X POST https://video.pyler.ai/api/v1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-F "file=@./ads/crunchy-sticks-q4.mp4" \
-F "title=Crunchy Sticks Q4 brand campaign" \
-F "rules=no on-screen consumption of alcohol, no minors holding weapons"
import os, requests
with open("./ads/crunchy-sticks-q4.mp4", "rb") as f:
resp = requests.post(
"https://video.pyler.ai/api/v1/ad-blocking",
headers={"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"},
files={"file": ("crunchy-sticks-q4.mp4", f, "video/mp4")},
data={
"title": "Crunchy Sticks Q4 brand campaign",
"rules": "no on-screen consumption of alcohol, no minors holding weapons",
},
timeout=120,
)
resp.raise_for_status()
verdict = resp.json()
응답 — 200 OK:
{
"id": "9f3c2a4d-0000-0000-0000-000000000000",
"ad_id": null,
"title": "Crunchy Sticks Q4 brand campaign",
"duration_ms": 18000,
"one_line": "Chocolate-stick snack; friends share boxes at a party.",
"garm": [
{
"category": "illegal_drugs_alcohol_tobacco_vaping",
"reasoning": "Bar scene at 0:12 shows a beer pour and a toast."
}
],
"blocklist": {
"rules": [
"no on-screen consumption of alcohol",
"no minors holding weapons"
],
"hits": [
{
"rule": "no on-screen consumption of alcohol",
"matched": true,
"evidence": "Characters drink beer at 0:14."
}
]
},
"block_decision": "block",
"explanation": "BLOCKED — GARM illegal_drugs_alcohol_tobacco_vaping; blocklist rule 'no on-screen consumption of alcohol'. Bar scene shows pour and toast.",
"screening_failed": false,
"created_at": "2026-06-16T10:23:01Z"
}
업로드한 파일로 실행한 심사는 ad_id: null입니다. 이 값은 아래 v1.1 경로에서만 채워집니다.
등록된 소재 심사(v1.1)
소재가 이미 광고 레지스트리에 있다면 다시 업로드할 필요가 없습니다. POST /api/v1.1/ad-blocking은 ad_id(status: ready여야 함)와 선택적 rules를 담은 JSON 본문을 받습니다. 코덱 정규화와 오디오 트랙 분석을 포함한 심사 파이프라인은 파일 경로와 같으므로, 같은 소재는 어느 경로로든 같은 판정을 받습니다. 판정 형태도 같으며 ad_id만 채워집니다.
ad_idstringbodyrequiredstatus: ready인 등록된 광고 소재 UUID입니다.
rulesstring | nullbody선택적 임시 정책 규칙으로, 쉼표나 줄바꿈으로 구분합니다.
- cURL
- Python
curl -X POST https://video.pyler.ai/api/v1.1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"ad_id\": \"$AD_ID\", \"rules\": \"no on-screen consumption of alcohol\"}"
import os, requests
resp = requests.post(
"https://video.pyler.ai/api/v1.1/ad-blocking",
headers={"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"},
json={"ad_id": os.environ["AD_ID"], "rules": "no on-screen consumption of alcohol"},
timeout=120,
)
resp.raise_for_status()
verdict = resp.json()
| HTTP | 발생 조건 |
|---|---|
200 | 판정을 반환했습니다. |
404 | 알 수 없는 ad_id, 다른 팀 소유, 또는 유효하지 않은 UUID입니다(존재 여부가 드러나지 않도록 잘못된 형식의 id도 404로 정규화합니다). |
409 | 소재가 status: ready가 아닙니다. |
/api/v1.1은 등록된 ad_id를 입력으로 받는 경로를 위해 /api/v1과 나란히 있는 별도 버전 루트입니다. 두 루트 모두 같은 API 키를 받습니다. v1 multipart 경로는 바뀌지 않았습니다.
지난 심사 결과 조회
모든 심사 결과는 저장되므로, 클라이언트 타임아웃으로 응답을 놓쳐도 작업이 사라지지 않습니다. GET /api/v1.1/ad-blocking은 팀의 결과를 최신순으로 나열합니다. 다시 제출하지 말고 ad_id로 필터링해 놓친 결과를 가져오세요.
| 쿼리 | 타입 | 비고 |
|---|---|---|
ad_id | UUID | 선택입니다. 알 수 없는 id는 빈 목록을 반환하고, UUID가 아닌 값은 422를 반환합니다. |
limit | integer | 1~200, 기본값 50입니다. |
offset | integer | 기본값 0입니다. |
curl "https://video.pyler.ai/api/v1.1/ad-blocking?ad_id=$AD_ID" \
-H "Authorization: Bearer $VUF_API_KEY"
{
"items": [
{
"id": "1a2b3c4d-0000-0000-0000-000000000000",
"ad_id": "c54eff39-0000-0000-0000-000000000000",
"block_decision": "allow",
"screening_failed": false,
"created_at": "2026-06-16T10:25:14Z"
}
],
"total": 1
}
항목에는 아래의 전체 판정 형태가 담깁니다. 업로드한 파일로 실행한 심사(v1 multipart 경로)는 ad_id: null이라 이 방법으로 조회할 수 없습니다. ad_id는 광고 레지스트리에 등록한 소재에만 지정하세요. 지난 심사 결과는 대시보드의 Ad Blocking 탭에서도 볼 수 있습니다.
판정 형태
두 심사 경로와 조회는 모두 같은 AdBlockingResult를 반환합니다.
idstringrequired서버가 발급한 이 심사의 id입니다.
ad_idstring | null심사한 등록 소재입니다. 파일 업로드는 null입니다. GET /api/v1.1/ad-blocking?ad_id=...에 사용해 결과를 다시 찾을 수 있습니다.
titlestring | null표시용 제목입니다. 요청의 title, 없으면 파일 이름입니다.
duration_msinteger | null소재 길이(밀리초)입니다.
one_linestring | null광고 내용을 한 줄로 요약합니다.
garmobject[]걸린 GARM 카테고리이며, 문제가 없으면 빈 배열입니다. 각 항목은 { category, reasoning }입니다. category는 아래 표의 고정 슬러그라 분기에 써도 안전합니다. reasoning은 판단의 근거가 된 단서와 발생 시점을 인용합니다(예: "Bar scene at 0:12").
blocklistobject{ rules, hits }입니다. rules는 평가한 전체 규칙(팀 차단 목록 + 임시 규칙)입니다. hits는 [{ rule, matched, evidence }]이며, matched: true인 항목이 걸린 규칙입니다.
block_decisionstringdefault: allow판정입니다. garm이 비어 있지 않거나 blocklist.hits[i].matched 중 하나라도 true이면 block, 그 외에는 allow입니다.
explanationstring | null걸린 카테고리와 규칙을 앞에 붙인 사람이 읽기 위한 설명입니다. 표시용이며 파싱용이 아닙니다. 분기는 garm과 blocklist.hits로 하세요.
screening_failedbooleandefault: false심사 자체가 실패해 block이 콘텐츠 판정이 아닌 예방 조치(페일 클로즈드)일 때 true입니다. 호출을 재시도하세요. 최종 사용자에게 실제 차단처럼 보여 주면 안 됩니다.
created_atstringrequiredISO 8601 UTC 타임스탬프입니다.
설명 텍스트가 아니라 screening_failed로 분기하세요. 실제 차단과 실패한 심사 모두 BLOCKED —로 시작합니다.
GARM 카테고리
심사기는 11개 GARM Brand Safety Floor 카테고리를 평가합니다. 슬러그는 고정이며, 제목은 GARM 공식 명칭입니다.
| 슬러그 | GARM 제목 |
|---|---|
adult_and_explicit_sexual_content | Adult & Explicit Sexual Content |
arms_and_ammunition | Arms & Ammunition |
crime_human_rights_violations | Crime & Harmful Acts, Human Rights Violations |
death_injury_or_military_conflict | Death, Injury, or Military Conflict |
online_piracy | Online Piracy |
hate_speech_and_acts_of_aggression | Hate Speech & Acts of Aggression |
obscenity_and_profanity | Obscenity & Profanity |
illegal_drugs_alcohol_tobacco_vaping | Illegal Drugs / Tobacco / E-cigarettes / Vaping / Alcohol |
spam_or_harmful_content | Spam or Harmful Content |
terrorism | Terrorism |
debated_sensitive_social_issue | Debated Sensitive Social Issue |
광고 차단의 garm 항목에는 카테고리와 추론만 담기며, 심사 판정에는 심각도 수준이나 장면 구간이 없습니다. 심각도(level: high / medium / low)와 time_ranges는 색인 생성, 검색, GET /api/v1/ads/{ad_id}/analysis가 반환하는 영상 측 garm_video 항목에 있습니다. 색인 생성 측 분류 체계는 12개 카테고리입니다(misinformation 추가). 분류 체계와 판정을 참고하세요.
레시피: 제출하고 판정에 따라 조치하기
일반적인 연동 흐름은 심사하고, block_decision으로 분기하고, 차단되면 설명을 사람에게 넘기는 것입니다.
- 소재 심사
파일을 업로드하거나(v1) 등록된
ad_id를 넘기고(v1.1), 임시rules를 함께 보냅니다. - 먼저 `screening_failed` 확인
true이면block은 예방 조치입니다. 마감 시간 안에서 한 번 재시도하고, 콘텐츠 차단으로 표시하지 마세요. - `block_decision`으로 분기
allow→ 송출할 수 있습니다.block→explanation,garm[].category슬러그, 일치한 규칙을 첨부해 검토 대기로 둡니다.
- cURL
- Python
response=$(curl -s -X POST https://video.pyler.ai/api/v1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-F "file=@./ads/new-creative.mp4" \
-F "title=spring-2026-launch" \
-F "rules=no lingerie or swimwear modeling, no on-screen consumption of alcohol")
if [ "$(echo "$response" | jq -r .screening_failed)" = "true" ]; then
echo "Screening failed — retry"; exit 1
fi
if [ "$(echo "$response" | jq -r .block_decision)" = "block" ]; then
echo "BLOCKED — hold for human review"
echo "$response" | jq -r .explanation
echo "GARM:"; echo "$response" | jq -r '.garm[].category'
echo "Matched rules:"; echo "$response" | jq -r '.blocklist.hits[] | select(.matched) | .rule'
else
echo "Cleared for delivery"
fi
import os, requests
def screen(path: str, rules: str) -> dict:
with open(path, "rb") as f:
resp = requests.post(
"https://video.pyler.ai/api/v1/ad-blocking",
headers={"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"},
files={"file": (os.path.basename(path), f, "video/mp4")},
data={"title": "spring-2026-launch", "rules": rules},
timeout=120,
)
resp.raise_for_status()
return resp.json()
rules = "no lingerie or swimwear modeling, no on-screen consumption of alcohol"
verdict = screen("./ads/new-creative.mp4", rules)
if verdict["screening_failed"]:
verdict = screen("./ads/new-creative.mp4", rules) # one deliberate retry
if verdict["screening_failed"]:
raise RuntimeError("screening unavailable; do not auto-serve")
if verdict["block_decision"] == "block":
matched = [h["rule"] for h in verdict["blocklist"]["hits"] if h["matched"]]
hold_for_review(
verdict["id"],
verdict["explanation"],
garm=[g["category"] for g in verdict["garm"]],
matched_rules=matched,
)
else:
enqueue_for_delivery(verdict["id"])
allow 응답은 다음과 같습니다.
{
"id": "1a2b3c4d-0000-0000-0000-000000000000",
"ad_id": null,
"title": "spring-2026-launch",
"duration_ms": 15000,
"one_line": "Unbranded running-shoe ad; runner on a coastal road at sunrise.",
"garm": [],
"blocklist": { "rules": ["no on-screen consumption of alcohol"], "hits": [] },
"block_decision": "allow",
"explanation": "ALLOWED — no GARM or blocklist violation. Standard product showcase with voiceover.",
"screening_failed": false,
"created_at": "2026-06-16T10:25:14Z"
}
타임아웃과 재시도
타임아웃은 실패가 아니라 결과를 알 수 없는 상태입니다. 요청이 수락되면 연결이 유지되는지와 관계없이 심사는 끝까지 실행되고 결과가 저장됩니다. 잃는 것은 응답뿐이므로 광고가 심사되지 않았다고 가정하지 마세요.
-
권장 클라이언트 타임아웃: 120초. 심사는 보통 몇 초 안에 반환됩니다. 이 상한은 드문 지연에 대비한 값입니다.
-
5분 넘게 올려도 효과가 없습니다. 5분 동안 데이터를 내보내지 않은 요청은
504로 닫힙니다. 스트리밍 엔드포인트(/api/v1/ads/match,/api/v1/videos/{video_id}/scenes/stream)는 계속 데이터를 내보내므로 더 오래 실행될 수 있지만, 조용한 요청은 5분 시점에 끝납니다. -
다시 제출하지 말고 복구하세요. v1.1 심사라면 다시 심사하는 대신
ad_id로 결과를 조회하세요. 비용이 적고 원래 판정을 반환합니다.curl "https://video.pyler.ai/api/v1.1/ad-blocking?ad_id=$AD_ID" \-H "Authorization: Bearer $VUF_API_KEY"같은
ad_id를 다시 심사할 수는 있지만 멱등하지 않습니다. 다시 실행되고 두 번째 결과가 기록됩니다. -
백오프. 전체 마감 시간 안에서 한두 번 재시도하면 충분합니다. 재시도는 다른 요청에 쓰일 심사 처리량을 차지하며, 요청 한도가 적용될 수 있습니다. 사용량과 한도를 참고하세요.
재시도할 오류는 다음과 같습니다.
| HTTP | 의미 | 재시도 |
|---|---|---|
400 | 잘못된 업로드입니다. 파일 누락, 분석 불가, 또는 길이 초과입니다(v1 경로). | 아니요 |
401 / 403 | 키가 없거나 유효하지 않거나, 공개 API 루트 밖에서 키를 사용했습니다. | 아니요 |
404 | 알 수 없거나 잘못된 형식의 ad_id, 또는 다른 팀 소유입니다. | 아니요 |
409 | 소재가 ready가 아닙니다. | 아니요 |
422 | 본문 또는 쿼리 검증에 실패했습니다(예: 조회 시 UUID가 아닌 ad_id). | 아니요 |
500 / 502 / 503 / 504 | 업스트림 또는 엣지 오류입니다. | 예 |
| 네트워크 오류, 클라이언트 타임아웃 | 응답을 받지 못했습니다. | 예, 조회 후 |
"아니요" 행은 결정적입니다. 같은 요청은 같은 방식으로 실패하므로 입력을 고치세요. block_decision: "block"과 screening_failed: true를 담은 200도 오류 경로입니다. 시스템이 페일 클로즈드로 동작하도록 판정 형태로 반환할 뿐이니 한 번 재시도하세요. 모든 오류 본문은 오류에서 설명하는 { "detail", "code" } 형식을 따릅니다.
콘텐츠 측 브랜드 세이프티
광고 심사는 소재를 판정합니다. 광고가 함께 노출되는 콘텐츠에는 색인 생성 결과를 사용하세요. GET /api/v1/videos/{video_id}의 영상 단위 garm_video 항목, GET /api/v1/videos/{video_id}/scenes의 장면 단위 garm_categories, 그리고 검색과 영상 목록의 garm_min_level 필터가 있습니다.