FFmpeg

  • 자막이 카드 텍스트와 겹쳤다, y=h*0.8로 좌표 하나 바꿔 고친 이야기

    자막이 카드 텍스트와 겹쳤다, y=h*0.8로 좌표 하나 바꿔 고친 이야기

    버그는 항상 예상 못 한 타이밍에 나온다. 영상을 재생했더니 자막이 카드 문구 위에 그냥 올라가 있었다. 전형적인 자막 좌표 버그였다. 원인은 좌표 하나였다. 그런데 그 좌표를 고친 뒤 확인하는 과정이 더 흥미로웠다.

    aidojin-shorts-generator는 어떤 프로젝트인가?

    인스타그램 릴스 같은 숏폼 플랫폼에는 AI 도구 관련 주장이 넘쳐난다. “Claude Code로 이걸 했다”, “GPT로 저걸 만들었다” — 검증 없이 퍼진다. aidojin-shorts-generator는 그런 주장을 팩트체크하고, 결과를 숏폼 영상으로 자동 제작하는 파이프라인(pipeline, 여러 작업 단계를 자동으로 연결한 처리 흐름)이다.

    처음부터 만든 건 아니다. 건강 채널용 영상을 자동화하려고 만들어뒀던 shorts-generator를 포크(fork, 기존 코드를 복사해서 새 프로젝트로 분리하는 것)했다. Whisper(오디오에서 자막 타임스탬프를 추출하는 AI 모델) 기반 자막, Ken Burns 효과(이미지를 천천히 확대하거나 이동시키는 영상 기법), FFmpeg(영상·음성 합성에 쓰는 오픈소스 도구) 합성 , 이 핵심 로직은 그대로 가져왔다.

    첫 테스트 주제로 선택한 건 릴스에서 본 주장이었다. “Claude Code에 GitHub 저장소(repository, 코드를 저장하는 온라인 공간) 7개를 깔아뒀다.” 그중 하나가 anthropics/claude-code였다. GitHub API로 스타 수까지 직접 대조해서 검증했다. 영상으로 만들어보려고 실행했더니 문제가 생겼다.

    자막 좌표 버그가 나온 곳

    완성된 영상을 재생했다. 자막이 카드 헤드라인 문구 위에 그대로 올라가 있었다. 두 텍스트가 같은 자리에서 겹쳐 보였다. 읽을 수가 없었다.

    원인은 단순했다. 두 요소가 같은 y좌표(화면에서 세로 위치를 나타내는 값)를 가리키고 있었다. agents/videoEditor.ts의 자막 번인(burn-in, 자막을 영상에 직접 새겨넣는 처리) 필터는 자막을 y=(h-text_h)/2로 그리고 있었다. 화면 세로 정중앙이다. agents/evidenceCard.ts의 renderHookCard()는 카드 헤드라인 텍스트를 startY = HEIGHT/2 - blockHeight/2로 배치하고 있었다. 이것도 세로 정중앙이다.

    1080×1920 세로 영상에서 두 요소가 같은 y축을 놓고 부딪힌 거다. 수식만 달랐지 결과는 같은 위치였다. 버그의 원인치고는 웃겼다.

    같은 y좌표를 바라본 두 코드를 찾은 방법

    겹쳐 보인다는 건 알았다. 어느 파일에서 좌표를 건드려야 하는지가 문제였다.

    파이프라인에 파일이 여럿 있었다. 자막을 다루는 코드와 카드를 다루는 코드가 별도 에이전트 파일로 나뉘어 있었다. Claude Code에 상황을 설명하고 어느 파일에서 충돌이 발생하는지 찾아달라고 했다. videoEditor.ts와 evidenceCard.ts 두 파일을 바로 짚어줬다. 각각의 수식을 나란히 보여주니 충돌 구조가 한눈에 보였다.

    카드 위치는 건드리지 않기로 했다. 카드는 화면 중앙에 있어야 시각적으로 자연스럽다. 자막 좌표 버그를 없애려면 움직여야 하는 건 자막 쪽이었다.

    자막 위치를 옮겨 고치기, y=h*0.8-text_h/2의 의미

    videoEditor.ts의 자막 y좌표를 하나만 바꿨다.

    • 수정 전: y=(h-text_h)/2 , 화면 세로 정중앙
    • 수정 후: y=h*0.8-text_h/2 , 화면 세로 80% 지점

    h*0.8은 화면 전체 높이의 80% 위치다. 거기서 텍스트 높이의 절반(text_h/2)을 빼면 자막의 중심선이 정확히 80% 지점에 온다. 화면 아래쪽이다. x좌표(가로 위치)인 x=(w-text_w)/2는 가로 중앙이라 그대로 뒀다.

    수식 자체는 짧다. 이 한 줄이 자막을 화면 중앙에서 화면 하단으로 내려보냈다.

    자막 좌표 버그 수정 후 영상 프레임 ,  자막이 화면 하단에 위치해 카드 헤드라인과 겹치지 않는다
    수정 후 영상 프레임 ,  다른 장면에서도 자막이 카드 텍스트 아래쪽에 안전하게 배치됐다

    수정 후 실제로 뽑아본 영상 프레임 두 장이다. 장면이 바뀌어도 자막은 화면 하단에 고정돼 있고, 카드 헤드라인과 더 이상 겹치지 않는다.

    유료 API를 다시 부르지 않은 이유: 캐시의 힘

    좌표를 고쳤다. 이제 확인해야 한다. 영상을 다시 만들려면 처음부터 전부 돌려야 할까?

    그럴 필요가 없었다. 파이프라인이 이미 중간 산출물(결과물)을 output 폴더에 저장해두고 있었다. narration.mp3(ElevenLabs TTS 결과), script.json(Claude가 생성한 대본), narration.srt(Whisper가 뽑아낸 자막 타임스탬프 파일), 카드 PNG 이미지들 , 전부 있었다.

    임시 검증 스크립트를 하나 만들었다. 이 스크립트는 저장된 파일들만 읽어들이고, SRT(자막 정보와 시간 정보를 담은 텍스트 파일 형식)를 다시 파싱해서 FFmpeg 합성 단계인 assembleVideo와 burnNarrationSubtitles만 로컬에서 재실행했다. API(Application Programming Interface, 외부 서비스와 통신하는 창구) 호출이 하나도 없었다. 몇 초 만에 끝났다. 자막이 카드 텍스트를 더 이상 가리지 않는 걸 확인했다. 스크립트는 확인 후 바로 지웠다.

    전에도 비용을 아끼려고 AI 대신 코드로 썸네일을 자동 생성해서 비용을 0원으로 만든 적이 있다. 이번에도 같은 방향이었다. Claude 대본 생성, ElevenLabs TTS, Whisper 전사 , 이 세 단계는 돈이 나간다. 자막 좌표 수정은 돈이 안 나간다. 비싼 단계를 다시 밟을 이유가 없었다.

    렌더링 파이프라인을 나누면 반복 검증이 빨라진다

    이번 자막 좌표 버그를 고치면서 구조적으로 확인한 게 있다. 파이프라인에서 비용이 드는 단계와 그렇지 않은 단계가 명확히 분리돼 있다는 거다.

    단계 비용 재실행 필요 여부
    대본 생성 (Claude) 유료 API 대본이 바뀔 때만
    TTS 음성 생성 (ElevenLabs) 유료 API 음성이 바뀔 때만
    자막 타임스탬프 추출 (Whisper) 유료 API 음성이 바뀔 때만
    카드 합성, 자막 위치, Ken Burns 렌더링 무료 (로컬) 화면 배치가 바뀔 때마다 자유롭게

    화면 배치만 수정하는 작업이라면 비싼 단계를 건드릴 필요가 없다. 캐시된 중간 산출물만으로 몇 번이고 반복 검증할 수 있다. 자막 좌표를 바꾸거나, 카드 위치를 조정하거나, 렌더링 순서를 바꾸는 작업 모두 마찬가지다.

    파이프라인을 처음 설계할 때 이걸 의도했는지는 솔직히 기억이 잘 안 난다. 결과적으로 잘 나뉘어 있었다. 덕분에 이번 버그 수정이 빨리 끝났다.

    다음에는 자막 스타일 자체를 건드려볼 생각이다. 폰트 크기, 줄바꿈 처리, 글자 그림자 , 이것들도 전부 로컬 렌더링 단계라 캐시만 있으면 자유롭게 반복할 수 있다. 그쪽을 좀 더 다듬어야 영상이 볼 만해진다. 아직 거칠다.