빠른 시작
사진 파일을 multipart/form-data의 image 필드에 담아 보냅니다. 키가 없어도 됩니다.
curl
curl -sS https://wonjun12-aeye-backend.hf.space/api/analyze \
-F "image=@photo.jpg;filename=image.jpg"
Python (requests)
import time
import requests
BASE = "https://wonjun12-aeye-backend.hf.space"
def analyze(path, api_key=None):
headers = {"X-API-Key": api_key} if api_key else {}
for attempt in range(2):
with open(path, "rb") as f:
# 파일 이름도 판정에 쓰일 수 있어 중립적인 이름으로 보냅니다
r = requests.post(f"{BASE}/api/analyze", headers=headers,
files={"image": ("image.jpg", f)}, timeout=120)
wait = int(r.headers.get("Retry-After", "0"))
if r.status_code in (429, 503) and attempt == 0 and wait <= 60:
time.sleep(wait) # 분당 한도나 대기열: 서버가 알려 준 만큼만 쉬고 한 번 더
continue
r.raise_for_status()
return r.json()
result = analyze("photo.jpg")
print(result["display_band"], result["display_score"])
응답에서 판정은 display_band로 읽습니다(판정 읽는 법).
키와 인증
지금은 키 없이도 부를 수 있고, 이때는 요청한 IP마다 횟수를 셉니다. 키를 받으면 그 키로 따로 셉니다. 키는 둘 중 한 헤더로 보냅니다.
X-API-Key: <키>
Authorization: Bearer <키>
- 등록되지 않은 키를 보내면
401 Invalid API key입니다. 키 없는 요청으로 바꿔서 처리하지 않습니다. - 서버가 키를 필수로 요구하도록 설정되어 있으면, 키 없는 요청은
401 API key required (X-API-Key header)를 받습니다. - 서버에는 키의 해시만 저장되어 있어 잃어버린 키를 다시 알려 드릴 수 없습니다. 새 키를 받아 주세요.
- 키를 앱이나 웹 페이지 코드에 넣지 마세요. 서버에서만 보내 주세요.
키 받기
키 요청 창구: oscar43@naver.com. 서비스 이름, 하루 예상 요청 수, 이용자 사진을 보내는지 여부를 적어 보내 주세요.
이용자의 사진을 A-EYE로 보내는 서비스라면, 그 이용자에게 사진이 A-EYE 분석 서버로 전송된다는 사실(처리 위탁과 국외 이전)을 알려야 합니다. A-EYE 쪽 처리는 개인정보처리방침에 있습니다. 키는 이 사실을 이용자에게 알리고 있는지, 평가·연구 목적인지 확인한 뒤 드립니다.
사용량 제한
| 구분 | 분당 | 하루 | 세는 단위 |
|---|---|---|---|
| 키 없이 | 20회 | 300회 | 요청한 IP |
| 키로 | 10회 | 1,000회 | 키 |
키는 같은 IP의 다른 이용자와 횟수를 나누지 않고 하루 1,000회까지 쓸 수 있습니다. 분당을 10회로 둔 것은 한 번에 한 장씩 분석하는 무료 서버를 A-EYE 앱 이용자와 나눠 쓰기 때문입니다.
- 분당 횟수는 그 창의 첫 요청부터 60초, 하루 횟수는 첫 요청부터 24시간(86,400초) 단위로 셉니다. 자정에 초기화되지 않습니다.
POST /api/analyze와POST /predict는 같은 횟수에서 셉니다. 키 확인을 통과한 요청은 형식 오류(400·413·415·422)나 대기열 초과(503)로 끝나도 한 번으로 셉니다. 받기 단계에서 거절된 요청(401, 그리고Content-Length헤더만 보고 거절한 400·413)과 429는 세지 않습니다. GET 주소는 세지 않습니다.- 횟수를 센 응답에는
X-RateLimit-Remaining(이번 분에 남은 횟수) 헤더가 붙습니다.429에는 창이 끝날 때까지 남은 초가Retry-After로 붙습니다. - 분석은 한 번에 한 장씩 합니다. 차례를 기다릴 수 있는 요청은 8건이고, 그 이상이면 바로
503(Retry-After: 2)입니다. 줄에 선 요청도 30초 안에 차례가 오지 않으면503(Retry-After: 30)입니다.
POST/api/analyze
요청
multipart/form-data, 파일 필드 이름은 정확히image하나입니다. 이름이 다르면422입니다.- 형식: JPEG, PNG, WebP. 확장자나
Content-Type이 아니라 파일 앞부분 바이트로 판단합니다. HEIC, GIF, 움직이는 PNG·WebP는415입니다. - 크기: 파일 29,360,128바이트(28 MiB)까지. 화소(가로×세로): JPEG 64,000,000, PNG·WebP 32,000,000까지.
- 파일은 받은 그대로 보내 주세요. 파일 안의 C2PA·메타데이터 기록도 판정에 쓰이고, 다시 저장하거나 압축하면 이 기록과 흔적이 줄어듭니다.
- 업로드한 파일 이름도 서버가 읽습니다. 생성 도구가 붙이는 이름이나 스크린샷 이름은 결과에 영향을 줄 수 있으니, 원래 이름이 필요 없으면
image.jpg처럼 중립적인 이름으로 보내 주세요.
응답 필드
아래 필드만 연동에 쓰세요. 응답에는 이 표에 없는 필드도 들어 있지만 내부 점검용이라 이름과 값이 예고 없이 바뀌거나 빠질 수 있습니다. 필드가 새로 생길 수 있으니 모르는 필드는 무시하도록 만들어 주세요.
| 필드 | 값 | 뜻 |
|---|---|---|
display_band | "real", "uncertain", "ai" | 최종 판정 세 갈래. 앱이 보여 주는 진짜 사진·판단 유보·AI 의심과 같습니다. 판정은 이 값으로 하세요. |
display_score | 정수, 100점 만점 | 앱이 보여 주는 AI 판별 점수이고, 사람에게 보여 줄 숫자는 이것 하나입니다. score에 100을 곱한 값이 아니라 언제나 display_band와 맞도록 다시 그린 값입니다. 클수록 AI가 만든 이미지 쪽이며, 정답률을 뜻하지 않습니다. |
verdict | "REAL", "AI" | 두 갈래 판정입니다. 지금 서버는 이 두 값만 보내고 셋째 값은 없습니다(판단 유보 같은 구분은 display_band로만 옵니다). 판단 유보도 "REAL"로 오므로 이 값만으로는 판단 유보를 알 수 없습니다. |
score | 0과 1 사이 실수 | 파일 속 기록과 서버 보정까지 반영한 점수입니다. 1에 가까울수록 AI 쪽이지만, 판단 유보나 진짜 사진으로 나온 결과에서도 1에 가까울 수 있으니 사람에게 그대로 보여 주지 마세요. 판정 경계는 보장하지 않으며 모델마다 바뀌므로, 이 값에 직접 문턱을 두지 말고 display_band를 쓰세요. |
basis | 문자열 | 판정의 근거입니다. "model"은 이미지 분석, "provenance"는 이미지 안의 C2PA 매니페스트가 AI 생성을 선언한 기록(서명은 검증하지 않음), "provenance_hint"는 서명 없는 기록(메타데이터 표시나 생성 도구 파일 이름)이 진짜 쪽 사진을 판단 유보로 올린 결과, "local_edit"는 부분 편집 의심입니다. 다른 값이 더 올 수 있습니다. 설명에만 쓰세요. |
partial_edit_suspect | true, false | 진짜 사진의 일부가 편집됐을 수 있다는 신호입니다. true이면 display_band는 "uncertain", basis는 "local_edit"입니다. |
heatmap_b64 | PNG, base64 | verdict가 "AI"이거나 partial_edit_suspect가 true이면 의심 영역 열지도(긴 변 768px 이하)입니다. 그렇지 않으면(verdict가 "AI"가 아니고 partial_edit_suspect가 false) 빈 문자열입니다. 화면에는 보낸 원본을 쓰세요. |
overlay_b64 | PNG, base64 | 같은 조건에서 원본 위에 의심 영역을 표시한 그림입니다. 그렇지 않으면 원본을 줄인 사진이며, 앞으로 빈 문자열이 될 수 있습니다. |
heatmap_style | "spotlight", "jet" | overlay_b64를 그린 방식입니다. spotlight는 의심 영역 밖을 어둡게 하고 경계선을 그리고, jet은 색 열지도를 겹칩니다. |
input_kind | "photo", "screen_capture", "unknown" | 서버가 본 입력 종류입니다. 판정 뒤에 따로 계산하는 참고값이고, 서버에서 이 기능이 꺼져 있으면 필드 자체가 없습니다. |
model_version | 문자열 | 판정한 모델의 이름입니다. 뒤에 붙는 표시를 포함해 형식은 바뀔 수 있습니다. 결과를 저장한다면 함께 저장해 두세요. |
elapsed_ms | 정수 | 서버 안에서 분석에 걸린 밀리초입니다. 줄을 서서 기다린 시간은 들어가지 않습니다. |
그림 필드에는 data: 접두어가 없습니다. 화면에 띄우려면 data:image/png;base64,를 앞에 붙이세요.
응답 예시(값은 설명용, 그림은 줄임)
{
"display_band": "uncertain",
"display_score": 49,
"verdict": "REAL",
"score": 0.91,
"basis": "model",
"partial_edit_suspect": false,
"heatmap_b64": "iVBORw0KGgo…",
"overlay_b64": "iVBORw0KGgo…",
"heatmap_style": "spotlight",
"input_kind": "photo",
"model_version": "best91(v6)+best91_loc",
"elapsed_ms": 3140,
…
}
이 예에서 판정은 판단 유보이고, 사람에게 보여 줄 숫자는 49입니다. verdict는 "REAL", score는 0.91이라 둘 중 하나만 보면 판정을 잘못 읽게 됩니다.
판정 읽는 법
display_band가 판정이고, basis는 그 이유를 덧붙입니다. A-EYE 앱은 이렇게 보여 줍니다.
display_band | 앱에 보이는 말 | 덧붙일 것 |
|---|---|---|
"real" | 진짜 사진 | AI로 만든 이미지의 특징이 약하거나 보이지 않았다는 뜻입니다. 진짜라는 증명은 아닙니다. |
"uncertain" | 판단 유보, 또는 basis가 "local_edit"이면 부분 편집 의심 | 어느 쪽이라고 말하기 어렵다는 뜻이며 오류가 아닙니다. 메신저로 여러 번 전달되거나 압축된 진짜 사진에서도 나옵니다. basis가 "provenance_hint"이면 사진 자체는 진짜 쪽이지만 서명 없는 AI 기록(메타데이터 표시나 파일 이름) 때문에 유보한 것입니다. |
"ai" | AI 의심, 또는 basis가 "provenance"이면 파일 속 C2PA 기록에 따른 결과 | "provenance"는 이미지 안의 C2PA 매니페스트가 AI 생성을 선언한 경우입니다. 서버는 그 기록의 서명을 검증하지 않습니다. 서명 없는 메타데이터 표시나 파일 이름만으로는 "ai"가 되지 않습니다. |
POST/predict
예전 앱이 쓰던 형식을 그대로 돌려주는 호환 주소입니다. 새로 연동한다면 /api/analyze를 쓰세요. 이 응답에는 display_band가 없어서 판단 유보를 구분할 수 없습니다.
- 요청은
/api/analyze와 같습니다. 같은image필드, 같은 크기 제한, 같은 횟수입니다. - 응답의
fake_prob은score에 100을 곱한 실수,real_prob은 100에서fake_prob을 뺀 값입니다.fake_prob을 사람에게 점수로 보여 주지 마세요. 판단 유보를 모르고,display_score와도 다릅니다.verdict,heatmap_b64,overlay_b64,heatmap_style,model_version,input_kind는/api/analyze와 같습니다. 그 밖의 필드는 내부 점검용입니다. - 오류가 하나 다릅니다. 분석 도중 실패하면
422가 아니라200과 함께{"error": "Invalid image"}를 돌려줍니다.200을 받으면 먼저error키가 있는지 보세요. - 빈 파일, 깨진 파일, 허용하지 않는 형식, 너무 큰 파일은
/api/analyze와 똑같이400,413,415로 거절됩니다.image필드가 없을 때도 똑같이422와 목록 형태의detail을 받습니다.
GET/api/health
200과 {"status": "ok"}는 서버가 시작을 마치고 모델을 다 불러왔다는 뜻입니다. 서버는 모델을 다 불러온 뒤에야 요청을 받기 시작하므로, 시작하는 동안에는 이 주소도 응답하지 않습니다. 다른 상태값은 없고, 대기열이 찼는지는 알려 주지 않습니다. 횟수에 들어가지 않습니다.
GET/api/version
지금 판정하는 모델의 이름을 model_version으로 돌려줍니다. 함께 오는 다른 필드는 운영 점검용입니다. 횟수에 들어가지 않습니다.
{"model_version": "best91(v6)+best91_loc", …}
오류
오류 본문은 {"detail": "…"}이고 detail은 문자열입니다. 예외는 둘입니다. 요청 형식이 틀렸을 때(FastAPI 검증 오류) 422의 detail은 목록이고, /predict의 분석 실패는 위에서 본 200 응답입니다.
| 코드 | detail | 뜻과 할 일 |
|---|---|---|
400 | Empty upload | 빈 파일입니다. |
400 | Invalid or corrupt image | 형식 표시는 맞지만 이미지로 열 수 없습니다. 파일이 깨졌는지 확인하세요. |
400 | Malformed Content-Length | Content-Length 헤더가 숫자가 아닙니다. HTTP 라이브러리가 정하게 두세요. |
401 | Invalid API key | 등록되지 않은 키입니다. |
401 | API key required (X-API-Key header) | 서버가 키를 필수로 요구하는데 키가 없습니다. |
404 | Not found | GET으로 부른 없는 주소입니다. GET /api/analyze처럼 분석 주소를 GET으로 불러도 이렇게 나옵니다. |
405 | Method Not Allowed | 주소가 틀렸거나(끝에 붙은 / 포함) 메서드가 틀렸습니다. 응답의 Allow 헤더가 GET이면 그 주소로는 POST할 수 없다는 뜻이니 주소의 철자와 끝의 /를 확인하고, POST이면 메서드를 POST로 바꾸세요. |
413 | Upload larger than 29360128 bytes | 파일이 28 MiB를 넘습니다. |
413 | Image has too many pixels (6000x6000); limit for PNG is 32000000 | 화소 수가 한도를 넘습니다(괄호 안은 보낸 크기, 한도는 JPEG 64000000, PNG·WEBP 32000000). 해상도를 줄이세요. |
413 | Image has too many pixels; limit is 32000000 | 파일 머리에 적힌 크기가 너무 커서 열기 전에 거절했습니다(이 경우 보낸 크기는 적히지 않고, 한도는 형식에 따라 다릅니다). 해상도를 줄이세요. |
415 | Only JPEG, PNG and WebP images are accepted | 허용하지 않는 형식입니다. HEIC 등은 JPEG로 바꿔 보내세요. |
415 | Animated images are not supported | 움직이는 PNG·WebP입니다. |
415 | Image structure not supported (more than 256 JPEG APPn/COM segments) | 메타데이터 덩어리가 지나치게 많거나 구조가 비정상인 파일입니다. 괄호 안 이유는 파일마다 다릅니다. 사진을 다시 저장(내보내기)해서 보내세요. |
422 | 목록(아래) | image 필드가 없습니다. 필드 이름을 확인하세요. |
422 | Image is too small (20x20); the shorter side must be at least 32 px | 짧은 변이 32px보다 작은 이미지는 분석하지 않습니다. |
422 | Image is too narrow (4000x32); the long side may be at most 100 times the short side | 긴 변이 짧은 변의 100배를 넘는 이미지는 분석하지 않습니다. |
422 | Image could not be analysed (ref 665a47c3) | 파일은 통과했지만 분석 중에 실패했습니다. 같은 파일을 다시 보내도 같을 가능성이 높으니 ref 값과 함께 알려 주세요. 위의 크기·비율 422와 달리 ref가 붙습니다. |
429 | Too many requests | 분당 한도를 넘었습니다. Retry-After초 뒤에 보내세요. |
429 | Daily request quota exceeded | 하루 한도를 넘었습니다. Retry-After는 그 창이 끝날 때까지의 초입니다. |
503 | Server busy, retry shortly | 대기열이 찼거나 30초 안에 차례가 오지 않았습니다. Retry-After초 뒤 한 번만 다시 보내세요. |
503 | Model not loaded | 모델이 준비되지 않았습니다. Retry-After: 10. |
500 | 없음(JSON 아님) | 예상하지 못한 서버 오류입니다. X-Request-Id와 함께 알려 주세요. |
502 504 | 없음 | A-EYE 서버 앞의 호스팅 단계에서 난 오류라 본문이 JSON이 아닐 수 있습니다. 잠시 뒤 한 번 다시 보내세요. |
요청 형식이 틀린 422의 본문은 이렇게 생겼습니다. detail이 문자열인지 목록인지로 분석 실패와 구분하세요.
{
"detail": [
{
"type": "missing",
"loc": ["body", "image"],
"msg": "Field required",
"input": null
}
]
}
A-EYE 서버의 응답에는 대부분 X-Request-Id 헤더(16진수 8자리)가 붙습니다. 분석 실패의 ref 값과 같은 값이니 문의할 때 함께 보내 주세요.
이용 조건
- 평가·시범용입니다. 가용성과 응답 시간을 보장하지 않고, 서비스 수준 계약(SLA)은 없습니다.
- 사진을 저장하지 않습니다. 보낸 사진은 결과를 돌려준 직후 폐기되고 모델 학습에 쓰지 않습니다.
- 결과를 다른 사람에게 보여 줄 때는 추정이라고 밝혀 주세요. 판정을 사실 확인이나 증거처럼 보이게 하지 마세요.
- 상업 서비스에서 쓰려면 먼저 문의해 주세요. 시범 기간에는 문의를 받은 뒤 개별로 판단합니다.
한계
- 판정은 추정입니다. 실제 사진을 AI로, AI 이미지를 실제로 판단할 수 있고, 새로 나온 생성 도구의 이미지일수록 놓치기 쉽습니다. 법적 증거, 보도, 인사 결정처럼 중요한 판단의 유일한 근거로 쓰지 말고 출처, 촬영 맥락, 메타데이터를 함께 확인해 주세요.
- 무료 서버 한 대입니다. CPU 서버 한 대가 한 번에 한 장씩 분석하고, A-EYE 앱 이용자와 같은 서버를 나눠 씁니다. 한꺼번에 많이 보내면 앱 이용자까지
503을 받습니다. 많은 양을 한 번에 처리해야 하면 먼저 문의해 주세요. 가용성은 보장하지 않으며, 서버가 다시 시작하는 동안에는 응답이 없습니다. - 화면 캡처는 판단 유보가 자주 나옵니다. 판정 기준이 카메라로 찍은 사진(메신저로 받은 사진 포함)에 맞춰져 있기 때문입니다. 화면 캡처로 보이는 일부 PNG는 서버의 화면 캡처 처리 단계에서 판단 유보가 진짜 사진으로 한 단계 내려갈 수 있고, 이때
score는 사진 기준 점수 그대로입니다. 판정은display_band를 따르세요. 진짜로 나와도 캡처 속 대화나 금액이 사실이라는 뜻은 아닙니다. - 브라우저에서 바로 부를 수 없습니다. 다른 웹사이트의 브라우저 코드에서 부르면 교차 출처(CORS) 설정에 막힙니다. 여러분의 서버에서 호출해 주세요.
- 사진은 남기지 않습니다. 보낸 사진은 분석하는 동안만 서버 메모리에 있고, 결과를 돌려준 직후 폐기되며 모델 학습에 쓰지 않습니다. 자세한 내용은 개인정보처리방침에 있습니다.
문의
API 문의: oscar43@naver.com. 오류를 알릴 때는 X-Request-Id와 시각을 함께 보내 주세요. 앱 사용 문의는 지원 페이지에 있습니다.