지난 몇 달 동안 인스타 릴스와 유튜브 쇼츠에 올린 영상 48개에는 소리가 없었습니다. 카드뉴스 PNG를 이어붙여 만들었으니까요.
소리가 없다는 건 알고 있었습니다. 나중에 음악을 깔면 되겠지 하고 넘겼죠. 그런데 나레이션을 한 번 넣어보고 생각이 바뀌었습니다.
역할을 나눴습니다
카드뉴스 하나로 캐러셀과 릴스를 둘 다 해결하려던 게 문제였습니다. 둘은 소비 방식이 다릅니다. 캐러셀은 손가락으로 넘기며 읽고, 릴스는 스크롤하다 0.5초 안에 붙잡히지 않으면 지나갑니다.
| 채널 | 쓰는 것 |
|---|---|
| 인스타 캐러셀 | 카드뉴스 PNG 그대로 |
| 릴스 · 쇼츠 · 틱톡 | 음성 + 말에 맞춰 움직이는 자막 |
이런 소식, 남들보다 먼저 받으실 분?
직접 돌려보고 되는 것만 골라 매주 보내드립니다. 광고성 하이프는 거릅니다.
먼저, 카드뉴스 자동화
카드뉴스는 디자인 도구로 만들지 않습니다. HTML로 만들고 스크린샷을 찍습니다.
이유가 있습니다. 디자인 도구로 만들면 문구 하나 고칠 때마다 6장을 다시 내보내야 합니다. HTML이면 텍스트만 고치고 다시 캡처하면 끝입니다. 폰트·색·간격이 CSS 변수로 묶여 있으니 톤도 안 흔들립니다.
node tools/export-cards.mjs my-cardnews.html
Puppeteer가 헤드리스 크롬을 띄워 .card 요소를 하나씩 1080×1350으로 캡처합니다. 6장이면 몇 초입니다.
헤드리스 크롬에는 폰트가 없습니다. CSS의 font-family에 'Malgun Gothic'이나 'Apple SD Gothic Neo' 같은 시스템 폰트를 직접 이름으로 적어주면 해결됩니다.
진짜 문제는 나레이션 영상이었습니다
음성을 넣는 건 어렵지 않습니다. 어려운 건 자막을 음성에 맞추는 일입니다.
손으로 맞추면 처음엔 됩니다. 그런데 목소리를 바꾸면 전부 어긋납니다. 속도를 조절해도, 문장 하나를 고쳐도 마찬가지입니다. 결국 매번 처음부터 다시 맞추게 되고, 그러다 그만두게 됩니다.
그래서 타이밍을 만들지 않고 되찾았습니다
핵심은 이 한 줄입니다.
TTS는 "몇 초에 무슨 말을 했는지" 알려주지 않습니다. 그래서 만들어진 음성을 음성인식에 다시 넣어 문장별 시각을 되찾습니다. 타이밍이 손으로 적은 값이 아니라 계산된 값이 되니, 목소리를 바꾸든 속도를 바꾸든 자막이 알아서 따라옵니다.
사람이 하는 건 1번뿐입니다. 나머지는 명령 한 줄로 이어집니다.
읽는 글과 보여주는 글을 나눕니다
이건 직접 해보기 전엔 몰랐던 부분입니다. 대본 파일이 두 개입니다.
| 파일 | 내용 | 숫자 표기 |
|---|---|---|
script.txt | 음성이 읽을 문장 | 이천육백사십오 |
lines.json | 화면에 띄울 문장 | 2,645 |
하나로 합치면 둘 중 하나가 망가집니다. 2,645라고 쓰면 TTS가 "이, 육사오"처럼 읽고, 이천육백사십오라고 쓰면 화면이 읽기 힘들어집니다. 어느 쪽도 양보할 수 없어서 처음부터 갈랐습니다.
속도가 매번 달랐습니다
여기서 한 번 크게 막혔습니다.
같은 대본, 같은 설정인데 TTS가 읽는 길이가 매번 달랐습니다. 25.8초로 읽을 때도 있고 31.4초로 읽을 때도 있었습니다. 나중엔 39.7초까지 나왔습니다.
처음엔 "1.3배속" 같은 고정값을 걸어뒀는데, 원본 길이가 흔들리니 결과도 같이 흔들렸습니다. 어떤 편은 26초, 어떤 편은 40초가 나오는 겁니다.
초당 몇 글자로 읽을지를 목표로 잡고, 실제 음성 길이를 재서 배속을 매번 계산합니다. 저는 초당 7.17자로 맞췄습니다. 이러면 TTS가 어떻게 읽든 항상 같은 페이스가 나옵니다.
"빠르게 읽어줘"라고 지시하는 방법도 있지만 재현이 안 됩니다. 만들어놓고 배속을 거는 쪽이 값이 늘 같고, 음정도 유지돼서 목소리 성격이 안 변합니다.
실패에서 배운 것들
여기부터가 실제로 도움이 될 부분입니다. 만들면서 틀렸던 것들입니다.
카드뉴스를 배경으로 깔면 글자가 두 번 나옵니다
처음엔 카드뉴스를 배경으로 깔고 자막을 얹었습니다. 그런데 카드에 이미 나레이션과 같은 문장이 적혀 있었습니다. 같은 말이 화면에 두 번 나오는 겁니다.
게다가 카드뉴스는 1080×1350(4:5)이고 영상은 1080×1920(9:16)이라, 꽉 채우면 좌우가 반드시 잘립니다. 흐리게 처리해봤지만 그건 증상만 가린 거였습니다.
지금은 카드를 잘리지 않게 통째로 넣고 자막을 그 위 3분의 2 지점에 겹칩니다. 배경이 좀 가려져도, 말에 맞춰 움직이는 글씨가 시선을 잡아주기 때문에 값을 합니다.
밝은 카드 위에서 흰 글씨가 사라집니다
카드 6장 중 3장이 크림색 배경이었습니다. 어두운 카드로만 확인하고 넘어갔으면 절반이 망가진 채로 나갈 뻔했습니다.
처음엔 자막 뒤에 어두운 띠를 깔았습니다. 잘 보이긴 했는데, 카드를 가려버렸습니다. 카드를 살리자고 시작한 방식인데 그걸 덮으면 앞뒤가 안 맞죠.
답은 글자에 검정 테두리였습니다. 다만 그냥 넣으면 획이 안쪽으로 파여 글자가 얇아집니다. CSS의 paint-order: stroke fill로 테두리를 글자 뒤에 깔아야 원래 두께가 유지됩니다.
데스크톱 스크린샷은 세로 영상에서 안 읽힙니다
터미널 화면을 증거로 넣었는데 폰에서 글자가 보이지 않았습니다. 1280픽셀 폭 캡처를 1080 폭 영상에 넣으면 본문 글자가 몇 픽셀이 됩니다.
세로로만 잘라선 소용없습니다. 확대 배율은 가로 폭이 결정하기 때문에, 좌우도 같이 잘라내야 커집니다. 저는 여백을 걷어내고 결과 목록만 남긴 뒤 2배로 확대해 저장했습니다.
등속으로 움직이면 기계처럼 보입니다
모션이 부족해 보이는 원인이 두 가지였습니다. 하나는 모든 움직임이 등속이었던 것, 다른 하나는 줄이 통째로 한 번만 뜨고 끝났다는 것입니다.
감속 곡선을 걸고, 줄 안의 단어가 0.035초 간격으로 하나씩 올라오게 바꾸니 달라졌습니다. 여기에 음성 파형을 읽어서 말이 세지는 순간 글자가 살짝 커지게 했습니다. 단, 화면 전체가 소리에 흔들리면 싸구려로 보여서 지금 말하는 단어에만 걸었습니다.
결국 이렇게 됩니다
node draft-script.mjs <슬러그> # 블로그 글 → 대본 초안
node make-short.mjs <슬러그> # 대본 → 영상
블로그 글이 있으면 대본 초안까지 뽑아줍니다. 훅을 앞에 두고, 중간에 한 번 꺾고, 마지막에 댓글 키워드를 넣는 규칙이 들어 있습니다. 물론 초안이라 읽고 고쳐야 합니다.
음성은 Gemini TTS를 씁니다. 무료 한도가 하루 10회라 하루 10편이 상한입니다. 음성인식은 로컬에서 도는 faster-whisper라 무료이고 무제한입니다. 렌더는 Remotion으로 로컬에서 돕니다.
정리
가장 크게 배운 건 기술이 아니었습니다.
하나는 음악으로 메우려던 자리를 목소리가 채우니 영상이 다른 물건이 됐다는 것입니다. 소리가 없어도 음악을 깔면 되겠지 싶었는데, 나레이션이 들어가니 훨씬 보기 편하고 이해도 빨랐습니다. 자막만 있을 때는 눈으로 다 읽어야 했는데, 목소리가 붙으니 흘려들어도 내용이 들어옵니다.
다른 하나는 파이프라인이 편해야 오래 간다는 것입니다. 명령 두 줄로 줄인 게 기술적으로 대단해서가 아닙니다. 한 편 만드는 데 손이 많이 가면 결국 안 만들게 되니까요. 목소리를 바꿀 때마다 자막을 다시 맞춰야 했다면 저는 두세 편 만들고 그만뒀을 겁니다.
그래서 자동화하면서 렌더 전에 눈으로 볼 것들도 같이 목록으로 만들어뒀습니다. 자막이 중요한 걸 가리는지, 가장 밝은 화면에서도 글자가 보이는지 같은 것들입니다. 빠르게 만들 수 있게 되면 잘못된 것도 빠르게 만들게 되니까요.
이 파이프라인은 스킬로 묶어뒀습니다. 대본 규칙, 모션 설계, 지금까지 밟은 함정이 전부 문서로 들어 있습니다.
스킬은 마켓플레이스에서 US$19에 구할 수 있습니다 — 실행 스크립트, Remotion 화면 소스, 대본 규칙·모션 설계·문제 해결 문서까지 그대로 들어 있습니다.
GEMINI_API_KEY.음성만 유료 API를 쓰고 나머지는 전부 로컬에서 무료로 돕니다.
만드는 과정과 설정값은 이 블로그에 정리해서 씁니다. 새로 나온 도구나 그날그날의 실험처럼 빠른 소식은 인스타그램 @jarvis_studio_k에 먼저 올립니다.
실측 위주 AI 뉴스레터 — 직접 돌려보고 되는 것만 골라 매주 보내드립니다.