웹을 위한 신뢰할 수 있는 AI 비디오 더빙 파이프라인 구축하기
요약
실제 프로덕션 환경에서 신뢰할 수 있는 AI 비디오 더빙 파이프라인을 구축하기 위한 기술적 도전 과제와 구조화 방법을 다룹니다. 단순 모델 호출을 넘어 전사, 화자 탐지, 립싱크, 미디어 렌더링 등을 통합하는 시스템 설계 전략을 제시합니다.
핵심 포인트
- 단순 모델 호출이 아닌 다단계 워크플로우 통합이 핵심
- 비용 효율성을 위해 출력 모드(자막, 오디오, 립싱크 등)를 사전에 정의
- 파이프라인 진입 전 미디어 메타데이터 검사 및 정규화 필수
- 화자 분리, 타이밍, 배경음 보존 등 복합적인 기술적 문제 해결 필요
AI 비디오 더빙은 겉보기에는 간단해 보입니다:
- 비디오 업로드.
- 음성 번역.
- 새로운 목소리 생성.
- 더빙된 결과물 다운로드.
프로토타입은 정확히 이 흐름을 따를 수 있습니다.
하지만 프로덕션 시스템(Production system)은 그럴 수 없습니다.
실제 비디오에는 여러 명의 화자, 배경 음악, 일시 정지, 겹치는 대화, 일관되지 않은 녹음 품질, 빠른 컷 전환, 화면에 보이는 화자와 화면 밖의 화자, 그리고 원본 음성보다 훨씬 길거나 짧은 번역문 등이 포함되어 있습니다.
기술적으로 성공적인 파이프라인이라 할지라도 다음과 같은 경우 사용할 수 없는 결과물을 생성할 수 있습니다:
- 화자에게 잘못된 목소리가 할당됨
- 번역된 음성이 사람이 말을 멈춘 후에도 계속됨
- 배경 음악이 사라짐
- 목소리가 너무 빠르거나 로봇처럼 들림
- 입 모양(Lip movement)이 새로운 오디오와 일치하지 않음
- 두 화자가 잘못된 방식으로 서로의 말을 가로챔
- 실패한 작업에 대해 사용자에게 비용이 이중으로 청구됨
- 페이지를 새로고침하면 처리 결과가 손실됨
web dubbing 워크플로우를 구축하면서, 저는 어려운 부분이 단일 모델 호출(Model call)에 있는 것이 아니라는 것을 깨달았습니다. 진짜 도전 과제는 전사(Transcription), 화자 탐지(Speaker detection), 번역(Translation), 음성 생성(Speech generation), 타이밍(Timing), 입 모양 동기화(Lip synchronization), 미디어 렌더링(Media rendering), 그리고 작업 복구(Task recovery)를 하나의 신뢰할 수 있는 시스템으로 조정하는 것이었습니다.
이 글에서는 제가 해당 파이프라인을 어떻게 구조화할 것인지 설명합니다.
1. 출력 모드를 먼저 정의하기
“이 비디오를 더빙해줘”라는 말은 여러 가지 다른 의미를 가질 수 있습니다.
사용자는 다음과 같은 것을 원할 수 있습니다:
- 번역된 자막만
- 시각적 수정 없는 번역된 오디오
- 원본 배경음이 포함된 번역된 오디오
- 목소리를 보존하는 더빙(Voice-preserving dubbing)
- 완전한 시각적 입 모양 동기화(Full visual lip synchronization)
- 화면에 등장하는 인물이 말하는 새로운 스크립트
- 다운로드 가능한 전사(Transcript), 오디오 트랙 또는 최종 비디오
이러한 워크플로우는 서로 다른 비용과 기술적 요구 사항을 가집니다.
처리를 시작하기 전에 요청된 출력을 정의하십시오.
type DubbingMode =
| "subtitles"
| "audio_only"
...
이는 사용자가 번역된 오디오만 필요할 때 백엔드에서 비용이 많이 드는 립싱크 (lip-sync) 처리를 실행하는 것을 방지합니다.
또한 가격 책정 및 진행 상황 보고를 더 이해하기 쉽게 만들어 줍니다.
2. 처리하기 전에 미디어 검사하기
업로드된 파일을 더빙 파이프라인으로 즉시 보내지 마십시오.
먼저 다음 사항을 검사하십시오:
- 컨테이너 형식 (Container format)
- 비디오 코덱 (Video codec)
- 오디오 코덱 (Audio codec)
- 재생 시간 (Duration)
- 너비 및 높이 (Width and height)
- 프레임 레이트 (Frame rate)
- 오디오 스트림 수
- 오디오 스트림 존재 여부
- 회전 메타데이터 (Rotation metadata)
- 파일 크기
- 가변 또는 고정 프레임 레이트 (Variable or constant frame rate)
- 손상되었거나 불완전한 스트림
정규화된 메타데이터 객체는 다음과 같을 수 있습니다:
type MediaMetadata = {
durationSeconds: number;
width: number;
...
ffprobe를 사용하여 파일을 검사할 수 있습니다:
ffprobe \
-v error \
-show_entries format=duration \
...
비용이 발생하는 모델 작업을 시작하기 전에 지원되지 않거나 안전하지 않은 입력을 거부하십시오.
예를 들어:
function validateMedia(metadata: MediaMetadata) {
if (!metadata.hasAudio) {
throw new Error("업로드된 비디오에 오디오 트랙이 포함되어 있지 않습니다");
...
제한 사항은 제품 결정 사항입니다. 중요한 부분은 비용이 많이 드는 처리가 시작되기 전에 이를 적용하는 것입니다.
3. 입력을 한 번만 정규화하기
다양한 브라우저, 카메라, 화면 녹화기 및 편집 애플리케이션은 매우 다른 파일을 생성합니다.
소스(source)를 예측 가능한 내부 형식으로 정규화하십시오.
일반적인 내부 표현 방식은 다음과 같을 수 있습니다:
- MP4 컨테이너
- H.264 비디오
- 고정 프레임 레이트 (Constant frame rate)
- AAC 오디오
- 48 kHz 샘플 레이트 (Sample rate)
- 스테레오 오디오
- 교정된 회전 (Corrected rotation)
- 보존된 종횡비 (Aspect ratio)
예시:
ffmpeg \
-i input.mov \
-map 0:v:0 \
...
모든 처리 단계에서 원본 비디오를 반복적으로 트랜스코딩 (transcode)하지 마십시오.
하나의 정규화된 소스를 생성한 다음, 이를 기반으로 오디오, 미리보기, 얼굴 추적 (face-tracking) 및 최종 렌더링 (final-render) 에셋을 파생시키십시오.
반복적인 인코딩은 시간을 낭비하고 품질을 저하시킵니다.
4. 배경을 손상시키지 않고 음성 추출하기
기본적인 더빙 파이프라인은 기존의 오디오 트랙 전체를 교체합니다.
이 방식은 종종 다음 요소들을 제거합니다:
- 음악 (Music)
- 환경음 (Environmental sound)
- 효과음 (Sound effects)
- 관객의 반응 (Audience reactions)
- 공간의 분위기 (Room ambience)
더 나은 파이프라인은 가능한 경우 음성(spoken voice)을 나머지 오디오와 분리합니다.
개념적으로는 다음과 같습니다:
Original audio
↓
Source separation
...
음성 스템 (Speech stem)은 전사 (Transcription) 및 화자 분석 (Speaker analysis)에 사용됩니다.
배경 스템 (Background stem)은 보존되었다가 나중에 생성된 대상 언어 음성과 믹싱됩니다.
오디오 에셋 (Audio assets)을 명시적으로 표현하십시오:
type AudioAssets = {
originalAudioKey: string;
speechStemKey?: string;
...
음원 분리 (Source separation)가 항상 완벽한 것은 아닙니다.
음악이 음성 트랙으로 유입될 수 있고, 목소리가 배경 트랙에 남아 있을 수 있습니다. 따라서 시스템은 깨끗한 분리가 불가능할 때 원본 오디오를 낮은 볼륨으로 사용하는 폴백 (Fallback) 모드를 지원해야 합니다.
5. 전사 데이터를 시간 기반 세그먼트로 저장하기
단순한 텍스트 형태의 전사본만으로는 더빙에 충분하지 않습니다.
타이밍 정보가 필요합니다.
최소한 각 세그먼트는 다음을 포함해야 합니다:
- 시작 시간 (Start time)
- 종료 시간 (End time)
- 발화 텍스트 (Spoken text)
- 감지된 언어 (Detected language)
- 화자 식별자 (Speaker identifier)
- 사용 가능한 경우 단어 단위 타이밍 (Word-level timing)
- 신뢰도 정보 (Confidence information)
type TranscriptWord = {
text: string;
startMs: number;
...
예를 들면 다음과 같습니다:
{
"id": "segment_12",
"speakerId": "speaker_1",
...
단어 단위 타이밍은 다음 작업에 도움이 됩니다:
- 자막 생성 (Subtitle generation)
- 문장 분할 (Sentence splitting)
- 휴지기 보존 (Pause preservation)
- 립싱크 정렬 (Lip-sync alignment)
- 중첩된 음성 감지 (Detecting overlapping speech)
- 지속 시간을 고려한 번역 (Duration-aware translation)
전사 데이터는 임시 텍스트가 아니라 구조화된 프로젝트 데이터로 취급되어야 합니다.
6. 화자 식별 유지하기
다중 화자 비디오에는 화자 분할 (Speaker diarization)이 필요합니다.
시스템은 다음 질문에 답할 수 있어야 합니다:
각 시간 범위 동안 누가 말했는가?
A 화자 분할 서비스는 처음에 다음과 같은 레이블을 반환할 수 있습니다:
SPEAKER_00
SPEAKER_01
SPEAKER_02
이 레이블들은 프로젝트 전체에 걸쳐 안정적으로 유지되어야 합니다.
type SpeakerProfile = {
id: string;
sourceLabel: string;
...
모든 세그먼트(segment)마다 새로운 타겟 음성(target voice)을 독립적으로 할당하지 마세요.
그렇지 않으면, 동일한 화자가 문장마다 다르게 들릴 수 있습니다.
감지된 화자당 하나의 안정적인 음성 프로필(voice profile)을 사용하세요:
type SpeakerVoiceMap = Record<string, string>;
const speakerVoices: SpeakerVoiceMap = {
...
중첩된 대화(Overlapping dialogue)는 특별한 처리가 필요합니다.
두 화자가 동시에 말하는 경우, 모든 것을 하나의 연속된 내레이션으로 평탄화(flattening)하는 대신 별도의 세그먼트 트랙(segment tracks)을 유지하세요.
7. 번역은 길이를 고려해야 합니다
번역 품질은 단순히 의미론적 정확도(semantic accuracy)에 관한 것만이 아닙니다.
사용 가능한 발화 시간 창(speaking window)에도 맞아야 합니다.
원본 세그먼트가 2.4초 동안 지속된다고 가정해 봅시다.
직역을 할 경우 자연스럽게 말하는 데 4.1초가 필요할 수 있습니다.
이 경우 파이프라인에는 다음과 같은 몇 가지 좋지 않은 선택지들이 남게 됩니다:
- 음성을 너무 빠르게 재생함
- 원본 인물이 말을 마친 후에도 계속 말을 이어감
- 마지막 단어를 잘라버림
- 다음 화자의 시작을 지연시킴
- 눈에 띄게 부정확한 입 모양 동기화(lip synchronization)를 생성함
번역을 확정하기 전에 사용 가능한 지속 시간(duration)을 계산하세요.
type TranslationContext = {
sourceText: string;
sourceLanguage: string;
...
번역 프롬프트(translation prompt)에는 지속 시간 제약 조건(duration constraint)이 포함되어야 합니다:
function createTranslationInstruction(
context: TranslationContext
) {
...
이것이 완벽한 타이밍을 보장하지는 않지만, 번역 단계에 올바른 목표를 제공합니다.
8. 합성(synthesis) 전 음성 지속 시간 추정하기
번역이 너무 길다는 사실을 최종 렌더링(render) 단계에서 알게 될 때까지 기다리지 마세요.
먼저 타겟 언어의 음성 지속 시간을 추정하세요.
type DurationEstimate = {
estimatedDurationMs: number;
availableDurationMs: number;
...
간단한 정책은 다음과 같을 수 있습니다:
function chooseTimingStrategy(ratio: number) {
if (ratio <= 1.05) {
return "synthesize_normally";
...
이러한 임계값(thresholds)은 예시일 뿐이며, 실제 출력물을 사용하여 보정(calibrate)해야 합니다.
핵심 원칙은 공격적인 오디오 가속(audio acceleration)이 기본 타이밍 해결책이 아니라, 최후의 수단이 되어야 한다는 것입니다.
9. 목소리를 과속시키는 대신 번역문을 재작성하기
번역된 세그먼트(segment)가 너무 길 경우, 다음 전략들을 순서대로 시도해 보세요:
- 불필요한 단어 제거
- 더 짧고 자연스러운 표현 사용
- 격식 있는 표현을 구어체 표현으로 교체
- 불필요한 절(clause) 병합
- 자연스러운 휴지(pause)를 기준으로 번역된 문장 분할
- 세그먼트 전후의 사용되지 않는 무음(silence) 활용
- 말하기 속도를 약간 증가
- 필요한 경우에만 시각적 타이밍(visual timing) 조정
예시:
직역:
저희는 이제 귀하에게 새로운 계정 관리 시스템에 대한
상세한 설명을 제공하고자 합니다.
...
더 짧은 버전은 중요한 의미를 보존하면서도 원본의 전달 방식에 더 자연스럽게 맞춥니다.
단순히 단어 수(word count)만을 위해 최적화하지 마세요. 서로 다른 단어와 음소(phoneme)는 발음하는 데 각기 다른 시간이 걸립니다.
최종 길이는 반드시 합성된 오디오(synthesized audio)를 사용하여 측정해야 합니다.
10. 휴지(pause)와 전달 구조 보존하기
자연스러운 말하기는 끊임없이 이어지는 단어의 흐름이 아닙니다.
다음 요소들을 포함합니다:
- 호흡을 위한 휴지 (Breath pauses)
- 망설임 (Hesitation)
- 강조 (Emphasis)
- 문장 끝의 무음 (Sentence-final silence)
- 화자의 반응 시간 (Speaker reaction time)
- 중단 (Interruptions)
- 웃음 (Laughter)
- 비언어적 소리 (Non-verbal sounds)
소스 타임라인(source timeline)의 의미 있는 휴지를 보존하세요.
type SpeechTiming = {
preRollMs: number;
speechDurationMs: number;
...
사용 가능한 모든 밀리초(millisecond)를 채우려는 타겟 목소리는 종종 부자연스럽게 들립니다.
목표는 무음을 제거하는 것이 아닙니다. 목표는 장면의 리듬(rhythm)을 보존하는 것입니다.
11. 목소리 보존을 제어 가능한 기능으로 취급하기
사용자는 더빙된 화자가 다음과 같은 인식 가능한 특성을 유지하기를 원할 수 있습니다:
- 인지된 연령 (Perceived age)
- 음성 에너지 (Vocal energy)
- 말하기 스타일 (Speaking style)
- 음높이 범위 (Pitch range)
- 감정적 톤 (Emotional tone)
- 격식 있거나 격식 없는 전달 방식 (Formal or casual delivery)
이러한 속성들을 화자의 정체성(speaker identity)과 분리하여 표현하세요.
type VoiceStyle = {
energy: "low" | "medium" | "high";
pace: "slow" | "normal" | "fast";
...
파이프라인은 업로드된 모든 목소리가 복제(cloned)되거나 재사용될 수 있다고 가정해서는 안 됩니다.
목소리 보존 생성(voice-preserving generation)을 활성화하기 전에, 사용자가 필요한 권한과 동의를 보유하고 있는지 확인하는 절차를 거쳐야 합니다.
type VoiceConsent = {
confirmedByUser: boolean;
confirmationTimestamp: Date;
...
동의(Consent) 정보는 제출 후 사라지는 프론트엔드(frontend) 체크박스로만 표현되는 것이 아니라, 작업(task)과 함께 저장되어야 합니다.
12. 세그먼트별 음성 생성 (Generate speech per segment)
전체 비디오에 대해 하나의 긴 오디오 파일을 생성하면 타이밍 교정(timing correction)이 어려워집니다.
대상 음성을 세그먼트(segment) 또는 논리적 문장 그룹 단위로 생성하세요.
type GeneratedSpeechSegment = {
segmentId: string;
speakerId: string;
...
이점은 다음과 같습니다:
- 개별적으로 실패한 세그먼트만 재시도할 수 있음
- 타이밍을 로컬에서 교정할 수 있음
- 화자의 목소리를 관리하기가 더 쉬움
- 번역이 변경되어도 비디오 전체를 다시 생성할 필요가 없음
- 사용자가 문장 하나만 편집할 수 있음
- 오디오를 독립적으로 정렬(aligned)할 수 있음
너무 작은 세그먼트는 피하세요.
모든 단어를 개별적으로 생성하면 대개 자연스러운 운율(prosody)이 파괴됩니다. 타이밍이 허용하는 한 완전한 구절(phrase)이나 문장을 사용하세요.
13. 생성된 음성을 타임라인에 정렬하기 (Align generated speech to the timeline)
합성(synthesis) 후에는 모든 세그먼트가 실제 지속 시간(duration)을 갖게 됩니다.
이를 대상 윈도우(target window)와 비교하세요.
function calculateAlignment(
startMs: number,
targetEndMs: number,
...
작은 불일치는 다음과 같은 방법을 통해 처리할 수 있습니다:
- 자연스러운 침묵 (Natural silence)
- 미세한 속도 조절 (Minor rate adjustment)
- 약간의 일시 정지 재배치 (Slight pause redistribution)
- 세그먼트 경계 조정 (Segment boundary adjustment)
큰 불일치는 극단적인 시간 신축(time stretching)을 통해 숨기기보다는 번역 단계로 되돌아가야 합니다.
수정 이력(revision history)을 유지하세요:
type SegmentRevision = {
segmentId: string;
version: number;
...
이를 통해 디버깅과 사용자 편집이 훨씬 쉬워집니다.
14. 립 싱크(lip synchronization) 전 가시적 화자 추적하기
모든 발화 세그먼트에 립 싱크가 필요한 것은 아닙니다.
화자는 다음과 같은 상태일 수 있습니다:
- 화면 밖에 있음 (Off-screen)
- 카메라를 등지고 있음
- 프레임 내에서 너무 작음
- 다른 물체 뒤에 가려져 있음
- 문장의 일부 동안만 보임
- 빠른 컷 전환 시퀀스에 등장함
얼굴 추적 (face tracking)을 사용하여 가시적인 발화 구간을 식별하세요.
type FaceTrack = {
id: string;
speakerId?: string;
...
다음 조건이 충족될 때만 립 싱크 (lip synchronization)를 적용합니다:
- 화자가 보일 때
- 얼굴이 충분히 클 때
- 입 영역이 심하게 가려지지 않았을 때
- 화자와 얼굴 간의 매핑 (speaker-to-face mapping) 신뢰도가 충분할 때
- 샷 지속 시간 (shot duration)이 의미 있게 처리할 수 있을 만큼 길 때
그렇지 않은 경우에는 원본 시각 프레임을 유지하고 오디오만 교체합니다.
이렇게 하면 처리 시간을 절약하고 불필요한 아티팩트 (artifacts)를 줄일 수 있습니다.
15. 화자를 얼굴에 신중하게 매핑하기
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기