EU AI Act 제50조를 위한 AI 생성 이미지 및 비디오 마킹: 코드 구현 방식
요약
EU AI Act 제50조 규정에 따라 AI 생성 이미지 및 비디오에 기계 판독 가능한 마킹을 구현하는 기술적 방법을 다룹니다. CasaNova Labs의 사례를 통해 EXIF와 XMP를 활용한 메타데이터 기록 방식과 운영 안정성을 위한 폴백 전략을 설명합니다.
핵심 포인트
- EU AI Act 제50조 준수를 위한 기계 판독 가능 마킹 구현
- sharp 라이브러리를 이용한 EXIF 및 XMP 메타데이터 기록 방식
- 데이터 손실 방지를 위한 UserComment 필드 활용 및 멱등성 유지
- 마킹 실패가 서비스 중단으로 이어지지 않도록 하는 폴백 설계
EU AI Act의 제50조는 2026년 8월 2일부터 적용됩니다. 이 조항은 정책 문서라기보다 직접적인 코딩 작업으로 전환되는 몇 안 되는 규정 중 하나입니다. 즉, 이미지, 오디오 또는 비디오를 생성하는 AI 시스템은 인공적으로 생성되었음을 감지할 수 있는 출력을 생성해야 합니다. 우리는 부동산 사진 및 비디오 편집을 위한 AI 스튜디오인 CasaNova Labs를 구축했으며, 제품이 출력하는 모든 이미지와 비디오에 대해 해당 날짜 이전에 이 기능을 배포했습니다. 이것은 규정 준수 설명서가 아니라 실제 코드에 대한 설명입니다: 파일 경로, 함수 이름, 그리고 규정을 아무리 읽어도 알 수 없었던 우리가 맞닥뜨린 두 가지 실패 모드에 대해 다룹니다.
두 가지 의무, 두 명의 서로 다른 행위자
제50조는 서로 다른 당사자에게 부과되는 요구 사항들을 하나로 묶고 있습니다. 여기서 관련된 두 가지는 다음과 같습니다:
- §2 — 생성 시스템의 제공자 (provider) (우리)에 대한 의무. 모든 합성 출력물은 자동화된 도구에 의해 감지될 수 있는 기계 판독 가능 (machine-readable) 형식으로 마킹을 포함해야 합니다. 인간의 가시성에 관한 것이 아닙니다.
- §4 — 게시를 수행하는 **배포자 (deployer)**에 대한 의무. 콘텐츠가 제3조(60)에 따라 딥페이크(deepfake)로 분류될 때 — 즉, 기존의 사람, 사물 또는 장소와 유사하여 실제처럼 보일 수 있는 방식으로 조작된 콘텐츠인 경우 —
정지 이미지의 경우, src/lib/server/ai-marking.ts에 있는 markGeneratedImage 함수가 sharp(sharp@^0.35.3 버전 고정)를 통해 EXIF 및 XMP를 기록합니다. EXIF 측면은 다음과 같습니다:
function buildExifOptions(generationId: string) {
return {
IFD0: {
...
withExif()는 병합(merging) 대신 IFD0/IFD2를 완전히 교체하며, 이 방식 덕분에 재마킹(re-marking) 작업이 멱등성(idempotent)을 유지할 수 있습니다. 생성 ID(generation id)는 ImageUniqueID와 같이 더 눈에 띄는 필드가 아닌 UserComment (Exif sub-IFD, 태그 0x9286)에 저장됩니다. 코드 주석에 따르면 sharp가 해당 필드를 오류 없이 빈 값으로 기록하기 때문에, 데이터가 손실 없이 유지(round-trip)되지 않는 것을 확인한 후 제외되었습니다.
EXIF와 병행하여, 동일한 호출을 통해 IPTC의 DigitalSourceType 필드가 trainedAlgorithmicMedia로 설정된 XMP 패킷을 기록합니다. 이는 AI 콘텐츠 탐지기가 실제로 찾는 제어된 어휘(controlled-vocabulary) 값입니다:
const marked = await sharp(bytes)
.withExif(exifOptions)
.withXmp(DIGITAL_SOURCE_TYPE_XMP)
...
이 함수에는 자체 문서 주석(doc comment)에 명시된 한 가지 엄격한 규칙이 있습니다. 즉, 절대로 예외(throw)를 발생시켜서는 안 되며, 생성 과정을 실패하게 해서도 안 된다는 것입니다. 지원되지 않는 형식, 손상된 버퍼, sharp 오류 등 어떤 상황에서도 서비스 중단 대신 마킹되지 않은 원래의 바이트(bytes)를 반환하는 방식으로 폴백(fallback)합니다. 마킹이 누락되는 것은 규정 준수(compliance)의 공백이지만, 생성이 실패하는 것은 운영 사고(production incident)이며, 코드는 후자를 훨씬 더 심각한 문제로 취급합니다.
이 함수는 src/lib/server/uploads.ts의 persistGeneratedOutputFromUrl이라는 단일 병목 지점(choke point)에서 호출됩니다. 이는 생성된 바이트를 가져온 직후이자, 두 스토리지 백엔드(Supabase 또는 로컬)가 이를 기록하기 전 단계에서 실행됩니다. 따라서 호출을 중복하지 않고도 두 미디어 유형과 두 백엔드 모두에 마킹된 바이트를 전달할 수 있습니다.
동일한 작업을 수행하려는 분들이 주의해야 할 점이 하나 있습니다. sharp(bytes).resize({width: 32}).webp()는 기본적으로 모든 EXIF 데이터를 삭제합니다. 리사이즈(resize) 작업 이후에 .withMetadata()를 추가하면 데이터가 유지됩니다. 저희는 src/app/generated/[...slug]/route.ts와 src/app/[locale]/s/[token]/media/[assetId]/[variant]/route.ts라는 두 개의 썸네일 라우트(route)를 운영하고 있었는데, 이들이 해당 호출 없이 정확히 그 리사이즈 작업을 수행하고 있었습니다. 이는 원본 파일에 무엇이 포함되어 있었든 상관없이 모든 썸네일이 마킹되지 않은 채 조용히 배포되었음을 의미합니다. 이제 두 라우트 모두 .withMetadata()를 호출합니다.
비디오 경로
sharp는 MP4를 다룰 수 없으며, 저희는 단지 메타데이터를 추가하기 위해 비디오 툴체인(toolchain)에 대한 강력한 의존성을 갖고 싶지 않았습니다. MP4는 ISO-BMFF 박스(box)의 평면적인 시퀀스인 [4-byte big-endian size][4-byte type][payload]로 구성되며, XMP 명세(part 3, "Storage in Files")는 정확히 이 목적을 위해 고정된 UUID를 담는 최상위 uuid 박스를 예약해 두었습니다. 따라서 동일한 ai-marking.ts 파일 내의 markGeneratedVideo는 재인코딩(re-encode) 없이 바이트 레벨의 추가(append) 방식으로 마킹을 작성합니다:
const boxes = readTopLevelBoxes(bytes);
if (!boxes) return bytes;
if (hasXmpUuidBox(bytes, boxes)) return bytes;
...
박스가 어디에 위치하느냐는 보기보다 훨씬 중요합니다. moov 아톰(atom)의 stco 테이블은 mdat에 대한 절대 바이트 오프셋(offset)을 저장합니다. mdat 이전에 무엇인가를 삽입하면 모든 오프셋이 잘못된 지점을 가리키게 됩니다. 직접 측정해 본 결과, ftyp 바로 뒤에 박스를 삽입하면 0개의 프레임이 디코딩되는 파일(Invalid NAL unit size)이 생성되었습니다. 마지막 박스 뒤에 추가하면 아무것도 이동하지 않으며 디코딩에 영향을 주지 않습니다. 이러한 순서 제약 조건 때문에 단순히 자연스러워 보이는 삽입 지점을 선택하는 것만으로는 이 작업을 "명백하게" 구현할 수 없는 것입니다.
비디오에 대한 제4조(§4) 가시적 마킹(visible mark)은 src/lib/server/visible-ai-mark-video.ts에서 별도의 단계로 처리되며, 여기에는 ffmpeg가 필요합니다. 이는 모든 프레임 위에 렌더링된 라벨 PNG를 오버레이(overlay)하고 재인코딩합니다:
const { code, stderr } = await run("ffmpeg", [
"-y", "-i", inputFile, "-i", overlayFile,
"-filter_complex", `[0:v][1:v]overlay=${overlay.left}:${overlay.top}:format=auto`,
...
오버레이 자체, 즉 "AI-generated image/video"라고 적힌 알약 모양의 라벨은 visible-ai-mark.ts(buildVisibleAiMarkOverlay) 내에서 sharp를 통해 Pango 텍스트 메트릭(text metrics)을 사용하여 한 번 렌더링됩니다. 이 라벨은 이미지 합성 경로와 ffmpeg 필터 간에 공유되므로, 두 미디어 유형이 서로 다른 구현으로 인해 어긋나지 않고 동일한 라벨을 렌더링하게 됩니다.
ffmpeg는 MP4 컨테이너를 처음부터 다시 구축하기 때문에, 재인코딩(re-encode) 전에 추가된 XMP uuid 박스를 삭제해 버릴 것입니다. 따라서 persistGeneratedOutputFromUrl에서의 호출 순서는 다음과 같아야 합니다: 가시적 마킹(visible mark, ffmpeg)을 먼저 수행하고, 기계 판독형 마킹(machine mark, 바이트 추가)을 두 번째로 수행해야 합니다. 현재는 이 순서가 뒤바뀌어 있어, 모든 비디오에서 제2항(§2) 마킹이 소리 없이 사라지고 있습니다.
범위: 소스에서의 마킹
제50조 제2항(Article 50(2))은 출력물이 인공적으로 생성되었음을 나타내는 기계 판독 가능한 표시(machine-readable indication)를 포함할 것을 요구하며, 위의 코드가 정확히 그 역할을 수행합니다. 즉, 모든 출력이 통과하는 단일 지점에서 생성 시점에 파서(parser)가 읽을 수 있는 통제된 어휘(controlled vocabulary)로 기록됩니다. 이는 종종 혼동되는 인접한 문제, 즉 파일을 그 출처와 암호학적으로 결합(cryptographically binding)하는 것과는 구분할 가치가 있습니다. 암호학적 결합은 다른 위협 모델(threat model), 다른 툴체인(toolchain), 그리고 다른 비용 구조를 가집니다. 제2항(§2)은 기계적인 방식이며, 현재 실제로 적용되고 있는 방식입니다.
이 빌드에서 다른 것은 다 버리더라도 딱 하나 훔쳐올 만한 가치가 있는 점은 다음과 같습니다. 두 가지 가시적 마킹(visible-mark) 의존성(Pango를 위한 시스템 폰트, ffmpeg)이 없을 때 조용히(silently) 실패한다는 점입니다. 예외(exception)도, 로그(log)도 발생하지 않으며, 그저 마킹이 되지 않은 채 정상적으로 보이는 출력물만 생성될 뿐입니다. 로컬 테스트 스위트(test suite)로는 이를 잡아낼 수 없는데, 왜냐하면 두 패키지가 우연히 모두 설치되어 있는 환경에서 실행되기 때문입니다. 따라서 /api/health에서 marking.fonts와 marking.ffmpeg를 런타임 체크(runtime check) 항목으로 보고하게 함으로써, 이러한 유형의 실패를 프로덕션(production) 환경에서 보이지 않게 방치하는 대신 curl 명령 한 번으로 확인할 수 있는 상태로 만들 수 있습니다. 만약 제2항(§2)을 직접 구현한다면, 배포하기 전에 렌더링 의존성(rendering dependencies)에 대한 계측(instrument)을 반드시 수행하십시오.
결론 (Closing)
구현 수준에서의 핵심 교훈은, 제4항(§4)과 명확히 분리한다면 제50조 제2항(Article 50(2))은 작고 기계적인 요구사항이라는 점입니다. 즉, 모든 출력이 통과하는 단 하나의 지점에서, 자체적인 리사이즈(resize) 및 재인코딩(re-encode) 파이프라인에서도 살아남을 수 있는 형식으로 통제된 어휘(controlled-vocabulary) 메타데이터 필드를 작성하는 것입니다. 특히 비디오 경로는 무거운 의존성(heavyweight dependency)이 필요하지 않았습니다. 대신 파일을 손상시키지 않는 단 하나의 삽입 지점을 찾기 위해 ISO-BMFF 명세(spec)를 면밀히 읽는 과정이 필요했습니다.
우리는 CasaNova Labs가 생성하는 모든 이미지와 비디오에 대해 2026년 8월 2일 이전에 이 기능을 배포했습니다. 만약 여러분이 생성(generation) 측면에서 개발 중이며 메타데이터를 잡아먹는 리사이즈(resize)나 컨테이너 박스(container box)를 잡아먹는 재인코딩(re-encode)이라는 동일한 두 가지 함정에 빠졌다면, 해결책은 위에 언급한 두 가지 순서 규칙(ordering rules)을 따르는 것이며, 이는 반나절 정도의 시간만 투자하면 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기