생성형 Ai를 이용해 이미지 대체 텍스트를 생성하여 접근성 개선하기

Jung Wook ParkJung Wook Park
6 min read

Table of contents

원문: Maurice Borgmeier, "Improving Accessibility by Generating Image-alt texts using GenAI"

저는 생성형 AI에 대해 꽤나 회의적입니다. 어떻게든 코파일럿을 멀리하고 그들이 내놓는 결과물을 역병마냥 피하려고 하죠. 코파일럿이 생산하는 텍스트는 영혼없고 지루하게 느껴지며 생성된 코드는 있지도 않은 API를 보여줍니다. AI가 생성한 콘텐츠를 흔히 슬롭(slop)이라고 부르는데 맞는 말인 것 같습니다.

이 정도면 제가 또 다른 AI 과대 광고를 하러 온 게 아니라는 것을 보여주는 불평불만으로는 충분할 듯 합니다. 저는 이전에 할 수 없었거나 하고 싶지 않았던 일을 할 수 있도록 해주는 것을 찾으려 노력하는데요. 그 중 하나가 접근성을 개선하는 것입니다. 제 RSS 리더에 이런 주제를 다루는 블로그가 몇 가지 있는데요. 최근 Marcus Herrmann이 80/20 접근성이라는 주제에 대해 짧은 게시글을 작성한 바 있습니다. 그가 정리한 목록 중 빠르게 해결할 수 있는 요소 중 하나로는 이미지의 대체 텍스트가 있죠.

이 속성은 스크린 리더를 사용하는 사람들로 하여금 이미지가 무엇을 나타내는지 이해할 수 있도록 돕습니다. 이는 콘텐츠에 더욱 접근하기 쉽게 만들어주지만 불행하게도 우리 게시글의 모든 이미지들에 HTML alt 속성이 지정되어 있지는 않습니다. 저는 생성형 AI가 이런 류의 문제를 해결하는 데 도움이 될 수도 있겠다는 생각이 듭니다. 왜냐면 수백 개의 게시글을 확인하며 이미지를 찾고 수동으로 속성을 설정하고 싶지는 않거든요.

결론이 뭔지 기다리시기 힘들다면 이 게시글에 대한 코드가 Github에 공개되어 있으니 확인해보세요.

이제 어떻게 작업을 진행할 지 이야기해 봅시다. 모든 훌륭한 계획이 그런 것처럼, 이 계획 또한 세 가지 단계로 이루어져 있습니다.

  1. 발견: 마크다운 게시글을 파싱하여 이미지 링크 추출하기

  2. 추론: 파운데이션 모델을 이용해 게시글의 문맥에 맞도록 이미지 표현하기

  3. 새로운 이미지 설명을 이용해 마크다운 갱신하기

잠시만요. 왜 마크다운이냐고요? 우리 블로그는 콘텐츠와 스타일링을 분리하기 위해 마크다운 파일을 이용하여 생성되며 (대충 그렇습니다) 마크다운 문서에는 이미지의 대체 표현이 추가되어야 합니다. 구체적으로는 다음과 같이 생긴 이미지 링크를 갱신해야 합니다.

![서술하는_텍스트](이미지_경로)

여기서 서술하는 텍스트는 HTML img 요소의 alt 속성에 저장되며 이미지 경로는 이미지에 대한 상대 또는 절대 경로입니다. 우리의 경우 정적 사이트 생성기 Hugo가 마크다운 파일과 애셋(이미지 등)을 파싱하여 정적 HTML 페이지로 변환해줍니다. 하지만 저는 제가 찾아낸 방법이 Hugo와는 상관없이 동작하기 바랍니다. 마크다운은 리드미 파일 같은 다른 곳에도 쓰이기 때문이죠. 우선은 http://https:// 링크같이 URL 형태의 외부 이미지는 무시하려 합니다. 대체로 우리가 사용하지 않기 때문이죠.

또다른 고려할 점은 비용입니다. 저는 이전에 봤던 모든 이미지들에 대해서 파운데이션 모델을 호출하지 않으면서 주기적으로 이 과정을 재실행하여 새로운 게시글을 갱신하고 싶습니다. 돈이 드는 일이니 말이죠. 이는 곧 무엇이 이미 처리되었는지 추적하기 위해 일종의 상태가 필요하다는 말입니다. 상태는 거의 항상 상황을 복잡하게 만드는데 여기서도 마찬가지입니다.

최대한 일을 간단하게 하기 위해 최소한의 메타데이터 저장 메커니즘을 추가하였습니다. 새로운 링크가 계속 발견된다는 점에서 착안했는데요. 이미 메타데이터 저장소에 있으면 생략합니다. 아니면 INFERENCE_REQUIRED 상태로 추가합니다. 그 다음 스크립트를 실행하여 대체 텍스트를 생성한 후 상태를 READY_FOR_UPDATE로 설정합니다. 대체 텍스트는 메타데이터 저장소에 함께 저장됩니다. 마지막 단계에서는 모든 READY_FOR_UPDATE 상태의 아이템을 읽고 상응하는 마크다운 문서를 갱신한 후 상태를 UPDATED로 설정합니다.

Description of the program flow.

이렇게 디스크에 저장된 메타데이터는 그냥 JSON 문서이기 때문에 수동으로 조정할 수 있습니다. 저장된 메타데이터를 버전 관리에 반영하여 모든 기여자가 동일한 데이터를 가지도록 할 수도 있지만 이는 선택 사항이죠.

모든 코드를 다 살펴보는 것은 너무 일이 많아질테니 (제 생각에) 흥미로운 부분에 집중하도록 하죠. 더 깊이 들여다보고 싶다면 Github에 있는 코드를 확인하세요. discover-image-links 유틸리티를 통해 이미지 링크를 꽤나 직관적으로 발견할 수 있습니다. 명명된 캡처 그룹이 있는 정규식을 이용하여 위에 기입한 패턴과 일치하는 모든 이미지 링크를 찾습니다.

import re

IMAGE_LINKS_PATTERN = re.compile(
    r"(?P<full_match>!\[(?P<alt_text>.*)\]\((?P<path>.*)\))"
)
LOGGER = logging.getLogger(__name__)


def extract_image_links_from_markdown_doc(
    doc: str,
) -> list[ImageLink]:

    links = []

    for match in IMAGE_LINKS_PATTERN.finditer(doc):

        captured_patterns = match.groupdict()
        full_match = captured_patterns["full_match"]
        alt_text = (
            captured_patterns["alt_text"]
            if captured_patterns["alt_text"] != ""
            else None
        )
        path = captured_patterns["path"]

        links.append(ImageLink(full_match, alt_text, path))

    return links

"제대로 된" 마크다운 파서를 사용할 수도 있었겠습니다만 코드 블럭 안의 마크다운 같은 경계 조건을 딱히 다루지 않아도 상관없었고 작업을 가볍게 유지하고 싶었습니다. 표준 라이브러리의 pathlib와 glob 패턴 지원 덕분에 파이썬을 이용해 디렉토리에서 마크다운 파일을 찾는 일은 사실 매우 편리했습니다.

article_base_bath: pathlib.Path = args.markdown_base_dir
markdown_docs = article_base_bath.glob("**/*.md", case_sensitive=False)

for path in markdown_docs:
    # 뭐든 하기

img/2024/08/dog.png와 같은 상대 경로 이미지 링크를 실제 물리적인 경로로 치환하는 것은 살짝 더 복잡한 일입니다. 하지만 저는 그냥 무차별 대입법을 통해 게시글 파일 자체에 상대적인 몇몇 경로와 현재 작업 디렉토리에서 찾는 방식을 사용하였습니다. 또한 --asset-base-dir 매개변수를 통해 더 많은 애셋 기본 디렉토리를 추가할 수 있는 방법을 지원하였습니다.

모든 이미지를 다 찾았다면 아마존 베드락의 API를 호출하여 LLM이 이를 표현할 수 있도록 할 차례입니다. 이는 generate-alt-texts 유틸리티를 통해 이루어집니다.

import boto3

def get_alt_text_for_image(
    article_content: str, image_link_record: ImageLinkRecord
) -> str:

    client = boto3.client("bedrock-runtime")

    # 이미지 링크를 감싸는 문자 1000개 추출
    article_content_short = #...

    prompt = f"""
    Summarize the following image into a single sentence and keep your summary as brief as possible.

    You can use this text between <start> and <end> as context, the image is refered to as {image_link_record['original_link_md']}
    <start>
    {article_content_short}
    <end>
    Exclusively output the content for an HTML Image alt-attribute, one line only, no code, no introduction, only the text, focus on accessibility.
    """

    # 이미지 파일 읽기
    with open(image_link_record["abs_img_path"], "rb") as image_file:
        image_data = image_file.read()

    payload = {
        "modelId": "anthropic.claude-3-haiku-20240307-v1:0",
        "input": {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 512,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {
                            "type": "image",
                            "source": {"type": "base64", "media_type": "image/jpeg",
                                "data": base64.b64encode(image_data).decode("utf-8"),
                            },
                        },
                    ],
                }
            ],
        },
    }

    response = client.invoke_model(
        modelId=payload["modelId"], body=json.dumps(payload["input"])
    )
    response_body = response["body"].read()

    result = json.loads(response_body.decode("utf-8"))
    alt_tag = result.get("content", [{}])[0].get("text")

    return alt_tag

이 함수는 Anthropics Claude 3 Haiku가 짧은 설명과 접근성에 중점을 두고 이미지를 설명하도록 지시합니다. 추가로 이미지 링크를 감싸는 마크다운 문서의 일부분을 제공하여 때때로 설명을 개선할 수 있도록 합니다. 프롬프트를 최적화하여 꽤 괜찮은 결과를 얻을 수 있게 도와준 제 동료 Frank Awounang Nekdem에게 특별한 감사의 말을 남깁니다.

재밌는 사실 한 가지를 알려드리자면 저는 처음에 이 부분을 마이크로소프트 코파일럿을 이용해 생성하려 시도했습니다. 클로드의 JSON 인터페이스에 몸을 맡기고 싶지 않았거든요. 그런데 결과적으로 코파일럿은 존재하지 않는 API와 포맷을 만들어냈고 아마존 Q는 제 말을 전혀 듣지 않더군요. 이대로는 생성형 AI에 대한 불평을 멈출 수 없을 것 같으니 원래 주제로 돌아갑시다.

마지막 부분은 그저 새로운 대체 텍스트로 마크다운 문서를 갱신하는 것뿐입니다. 이는 update-alt-texts 유틸리티가 다루는 부분인데 기본적으로 문서 내용 자체를 변경하는 방식으로 동작합니다. 따라서 깃 저장소 또는 비슷한 버전 관리 메커니즘 하에서 작업하시는 것을 추천드립니다. 그냥 찾아 바꾸기라서 흥미로운 일은 없습니다. 실제로 어떻게 동작하는지 보죠.

tecRacer 블로그부터 시작하지는 않기로 결정했습니다. 블로그에 콘텐츠가 많기도 했고 이 작업이 잘 동작하는지 먼저 확인해줄 사람이 필요했거든요. 대신 제 개인 블로그 저장소에 먼저 시험해보았습니다.

# 단계 1: 이미지 링크 발견
$ discover-image-links --asset-base-dir static content
# 단계 2: 대체 텍스트 생성
$ generate-alt-texts
# 단계 3: 마크다운 갱신
$ update-image-links

전체 절차가 몇 분도 채 지나지 않아 끝났고 작업 이전과 이후를 확인하여 원본 이미지와 비교해본 결과 만족할 수 있었습니다. 누락된 대체 텍스트를 다루는 방식을 본 후 제 게시글의 모든 대체 텍스트를 다시 생성하기로 결정했습니다. 첫번째 명령을 아래의 명령으로 바꾸기만 하면 되죠.

# 단계 1: 대체 텍스트가 있어도 이미지 링크 발견
# --ue 또는 --update-existing
$ discover-image-links --asset-base-dir static content --ue

조심하세요. 시간도 더 걸리고 돈도 더 내야 할 겁니다. 설명 몇 가지가 좀 이상하긴 하지만 그래도 제가 일반적으로 작성하는 세 가지 단어 대체 텍스트보다는 훨씬 나아서 수작업으로 고쳤습니다. 마지막에 좀 만져줄 필요가 없다면 생성형 AI라고 할 수 없겠죠.

Github에 있는 코드를 확인하시고 여러분의 웹사이트 접근성을 개선하세요.

3
Subscribe to my newsletter

Read articles from Jung Wook Park directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jung Wook Park
Jung Wook Park

Code Tinker. Interested in user interface software development. Trying to think functionally.