이전 글은 여기~ (하루만에 추가된 내용 덧붙이려고 만들었습니다)
https://www.gpters.org/media/post/now-picture-has-resolved-QuTWZKBa27NaOAy
🔊 오디오 파이프라인 구축 (4/7~4/8)
TTS 연결 — Typecast → Google Cloud TTS
처음엔 Typecast API를 연결하려 했는데 API 키 인증이 계속 실패했어요. 키에 [대괄호]가 들어간 것도 있었고, 나중엔 Typecast API 플랜 문제로 판단해 방향을 바꿨어요.
대신 Google Cloud TTS로 전환했어요.
기존 Gemini API 키는 TTS 서비스가 차단돼 있어서 TTS 전용 키를 새로 발급
WaveNet 음성 월 100만 자 무료 — 숏폼엔 충분하고도 남음
독일어(de-DE-Wavenet-B/D) + 한국어(ko-KR-Wavenet-A) 모두 지원
curl -s -X POST \
"https://texttospeech.googleapis.com/v1/text:synthesize?key=$GOOGLE_TTS_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {"text": "Prost! Trink mit uns!"},
"voice": {"languageCode": "de-DE", "name": "de-DE-Wavenet-B"},
"audioConfig": {"audioEncoding": "MP3"}
}' | jq -r '.audioContent' | base64 --decode > scene_03_npc_de.mp3
한국어 나레이션 6개는 Typecast 웹에서 직접 생성해서 파일로 받고, 독일어 NPC 대사는 Google TTS로 자동 생성했어요. (어차피 독일어라는 컨텐츠 자체가 생소하기 가능했던 점.) (이 당시 사용한 타입캐스트 목소리는 "소이")
BGM + SFX 무료 소스 자동 다운로드
# BGM: Archive.org 퍼블릭 도메인 독일 민속음악 (2분 45초)
curl -O "https://archive.org/download/lp_german-beer-drinking-music_various/disc1/01.01.%20Watschentanz.mp3"
# 기차 소음 SFX (1분 17초, 루프 가능)
curl -O "https://www.orangefreesounds.com/wp-content/uploads/2017/03/Inside-train-sound-effect-loop.mp3"
# 유리잔 짠 SFX (2.3초)
curl -O "https://www.orangefreesounds.com/wp-content/uploads/2021/09/Glass-clink-sound-effect.mp3"
다 무료 도메인 사운드라고 해서 저작권 우려는 없을 것 같지만 후에 한번더 저작권 관련 체크를 해보는 것도 나쁘지 않을 것 같습니다.
ffmpeg 3트랙 믹싱
이전 결과물에서 문제로 생각했던 부분이 계속 새로 시작하는 비지엠이었는데, 아예 다 연결하는 짐에 씬별로 BGM이 끊기지 않게 클코에게 요구했습니다. 클코는 offset을 계산해서 잘랐어요.
씬1=0s, 씬2=8s, 씬3=16s, 씬4=24s, 씬5=34s, 씬6=41s
독일어 NPC 대사는 나레이션 끝난 후 타이밍에 맞게 adelay로 삽입:
ffmpeg -y \
-i veo_clip.mp4 \
-i narration.mp3 \
-i npc_de.mp3 \
-i bgm.mp3 \
-i sfx_train.mp3 \
-filter_complex "
[1:a]adelay=0|0[narr];
[2:a]adelay=2900|2900[npc];
[3:a]aloop=loop=-1:size=2e+09,atrim=start=16:duration=8,volume=0.2[bgm];
[4:a]aloop=loop=-1:size=2e+09,atrim=start=16:duration=8,volume=0.15[sfx];
[narr][npc][bgm][sfx]amix=inputs=4:duration=longest[a]
" \
-map 0:v:0 -map "[a]" -c:v copy -c:a aac -t 8 output.mp4
🎬 씬4~6 Veo 영상 생성
원래 만들어뒀던 씬 1-3은 그대로 적용했지만 4-6은 추가로 만들어야 했습니다!
Veo API 파라미터를 찾는 과정이 좀 험난했어요.
오류
원인
해결
inlineData isn't supported
Veo-3.1은 inlineData 불가
bytesBase64Encoded 방식으로 변경
numberOfVideos isn't supported
파라미터 제거 필요
삭제
durationSeconds needs to be a number
문자열로 넘겼음
숫자형으로 변경
allow_all not supported
personGeneration 제한
파라미터 제거
argument list too long
이미지가 커서 jq 한도 초과
Python으로 JSON 빌드로 전환
결국 최소 파라미터가 정답이었어요:
payload = {
"instances": [{"prompt": prompt, "image": {"bytesBase64Encoded": data, "mimeType": "image/png"}}],
"parameters": {"aspectRatio": "9:16", "durationSeconds": 8, "resolution": "720p"}
}
🎞️ Remotion 자막 시스템
이전 같은 스터티원 분의 리모션 사용기가 너무 마음에 들어서 차용했습니다.
기존 Remotion은 정지 이미지 기반이었는데 이번에 Veo 영상 위에 자막을 올리는 구조로 바꿨어요. 만들면서 중요하게 생각한 건 1. 잘 보이는 자막 2. 애니메이션과 어울리는 느낌 3. 독일어를 알아보고 알아들을 수 있게 하는 것. 이었습니다.
3종 자막 레이어:
나레이션 자막 — 하단 고정, 페이드인, 반투명 검정 배경
독일어 흔들 자막 — 파란 말풍선, sin 파형으로 흔들흔들, 씬 타이밍에 맞게 등장
한국어 번역 — 독일어 자막 바로 아래 작은 글씨로 따라다님
const wobbleX = Math.sin(localFrame * 0.6) * 3;
const wobbleY = Math.sin(localFrame * 0.9 + 1) * 2;
const wobbleRot = Math.sin(localFrame * 0.45) * 1.5;
독일어 대사 목록:
씬2: "Heute spielen die Bayern!" → 오 늘 바이에른 경기야!
씬3: "Prost! Trink mit uns!" → 건배! 같이 마셔요! / "Los geht's!" → 가자!
씬4: "Bayern! Bayern! Ja! Ja!" → 바이에른! 바이에른! 예스!
중간이 너무 빈 것 같아서 효과음도 이때 만들게 시켰는데 처음 넣은 부분보다는, 장면과 씽크가 맞았으면 해서 아예 원하는 시점을 클코에게 언급하고 넣게 시켰습니다.
씬5 맥주 부딪히는 장면엔 짠 SFX를 4초 지점에 삽입했어요 (adelay=4000).
✅ 최종 결과물
항목
내용
총 길이
46초
씬 수
6개 (씬13: Veo 애니메이션, 씬46: Veo 애니메이션)
오디오 레이어
나레이션 + 독일어 NPC + BGM + 기차SFX + 짠SFX
자막
나레이션 자막 + 독일어 흔들 자막 + 한국어 번역
파일 크기
31.8MB
길다면 길고 짧다면 짧은 스터디가 이렇게 마무리 작업을 향해 달려갑니다 ㅎㅎ
나름 수확이 있기도 했고 결과물이 마음에 들어서 (물론 총 35000원 짜리니 안 들면 안되지만 ㅎ) 만족하는 것 같아요~ 이제 남은 건 유튜브 업로드 인걸로!