Myna Player가 재생 전에 문맥을 인식하는 자막을 생성하는 방법
요약
Myna Player는 재생 전 오디오를 미리 처리하여 문맥을 반영한 고품질 자막을 생성하는 로컬 우선 AI 비디오 플레이어입니다. 실시간 번역의 속도와 문맥 사이의 트레이드오프를 해결하기 위해 재생 루프 외부에서 전사와 번역을 수행하는 아키텍처를 채택했습니다.
핵심 포인트
- 재생 시점보다 앞서 오디오를 처리하여 자막 타이밍 문제 해결
- 로컬 미디어 파일에 직접 접근하여 결정론적 시간 범위 확보
- 재생 루프 외부에서 비용이 큰 AI 작업을 수행하는 아키텍처
- 문맥 유지를 위해 정형 시간 창(canonical time window) 단위로 처리
자막에는 AI 번역이 파이프라인에 들어올 때 더욱 두드러지는 타이밍 문제가 있습니다.
모든 아주 작은 파편을 즉시 번역하면 텍스트가 제시간에 도착할 수는 있지만, 중요한 문맥(context)이 사라집니다. 더 긴 구절을 기다리면 번역은 더 일관성 있게 되지만, 자막이 이미 대화가 지나간 후에 나타날 수 있습니다.
이는 실질적인 트레이드오프(tradeoff)를 발생시킵니다:
속도냐 문맥이냐.
저는 미디어 플레이어가 재생 시점에 이러한 선택을 피할 수 있는지 확인하고 싶었습니다.
그 결과, 현재 재생 위치보다 앞서 오디오를 처리하고, 로컬에서 전사(transcription)하며, 주변 문맥과 함께 완성된 자막 큐(cue)를 번역하고, 이를 네이티브 플레이어 시계(clock)에 맞춰 렌더링하는 오픈 소스, 로컬 우선(local-first) AI 비디오 플레이어인 Myna Player가 탄생했습니다.
Myna Player는 현재 퍼블릭 알파(public alpha) 단계입니다. 핵심 macOS 애플리케이션은 작동하며, Windows 코드 및 패키징은 활발히 검증 중이며, 첫 번째 안정적인 퍼블릭 릴리스는 아직 게시되지 않았습니다.
이 글에서는 아키텍처, 자막 타이밍 계약(subtitle timing contract), 그리고 재생 전 처리가 왜 문제를 변화시키는지에 대해 설명합니다.
제품의 경계
Myna Player는 비디오 미리보기가 첨부된 범용 전사(transcription) 편집기가 아닙니다.
이것은 적시(just-in-time) 자막 파이프라인을 갖춘 미디어 플레이어입니다.
그 차이가 아키텍처에 영향을 미칩니다.
시스템은 동시에 여러 가지 사항을 충족해야 합니다:
- 재생(playback)이 반응성을 유지해야 함;
- 전사(transcription)가 시청자보다 앞서 나가야 함;
- 탐색(seeking)이 이전 작업 내용을 손상시키지 않아야 함;
- 완료된 자막 작업은 나중에 재개될 수 있어야 함;
- 번역이 충분한 문맥을 전달받아야 함;
- 번역된 큐(cue)가 원래의 타이밍을 유지해야 함;
- 로컬 미디어와 오디오가 장치에 그대로 남아 있어야 함;
- 렌더러(renderer)가 AI 요청을 위해 절대 기다리지 않아야 함.
단순화된 흐름은 다음과 같습니다:
native player clock
|
v
...
핵심 아이디어는 비용이 많이 드는 작업이 재생 루프(playback loop) 외부에서 수행된다는 것입니다.
시스템 오디오를 듣는 것이 잘못된 추상화인 이유
실시간 자막 (live-caption) 애플리케이션은 종종 현재 재생 중인 모든 오디오를 듣습니다.
소스(source)를 알 수 없거나 완전히 실시간인 경우에는 이것이 유용합니다.
Myna Player는 로컬 미디어 파일과 함께 작동합니다. 따라서 소스를 직접 읽을 수 있습니다.
이는 오디오를 분석하기 전에 소리가 스피커를 통해 출력될 때까지 기다릴 필요가 없음을 의미합니다. 파일의 나중 섹션에서 오디오를 추출하여 재생이 해당 부분에 도달하기 전에 자막을 준비할 수 있습니다.
직접적인 파일 접근은 다음과 같은 몇 가지 장점을 제공합니다:
- 결정론적 시간 범위 (deterministic time ranges);
- 반복 가능한 추출 (repeatable extraction);
- 정밀한 스트림 선택 (precise stream selection);
- 앞부분으로 건너뛰기 가능 (the ability to jump ahead);
- 재개 가능한 캐시 작업 (resumable cached work);
- 시스템 오디오 캡처 권한에 대한 의존성 없음;
- 알림음이나 다른 애플리케이션의 피드백 없음.
미디어 파일 자체가 신뢰할 수 있는 단일 원천 (source of truth)이 됩니다.
처리 단위는 정형 시간 창 (canonical time window)입니다
Myna Player는 결정론적이고 중복되지 않는 오디오 창 (audio windows)을 스케줄링합니다.
현재 설계는 정형화된 30초 범위를 사용합니다.
개념적으로는 다음과 같습니다:
00:00–00:30
00:30–01:00
01:00–01:30
...
정형 창 (Canonical windows)이 중요한 이유는 캐시에 안정적인 식별자 (identities)를 부여하기 때문입니다.
동일한 비디오를 다시 열 경우, 애플리케이션은 정확히 어떤 영역이 완료되었는지 판단할 수 있습니다. 사용자가 탐색 (seek)을 하더라도, 이미 완료된 창은 다시 전사 (transcribe)할 필요가 없습니다.
스케줄러는 재생 위치에 따라 우선순위를 할당합니다:
- 첫 번째로 필요한 창은 긴급함;
- 대략 그다음 90초 분량은 우선순위가 높음 (promoted);
- 이후의 창은 백그라운드 작업으로 유지됨.
그 결과는 단순히 "파일 전체를 가능한 한 빨리 전사하는 것"이 아닙니다.
그것은 다음과 같습니다:
시청자가 곧 도달할 영역을 준비해 두는 동시에, 여유 용량을 사용하여 캐시를 비디오의 더 먼 부분까지 확장하는 것.
미디어 지문 (Media fingerprints) 및 파이프라인 키 (pipeline keys)
파일 경로만으로는 안전한 캐시 키 (cache key)가 될 수 없습니다.
미디어가 변경될 수 있습니다. 선택된 오디오 스트림이 변경될 수 있습니다. 음성 모델 (speech model), 언어 모드 (language mode), VAD 모델, 청크 크기 (chunk size), 또는 큐 세그멘테이션 규칙 (cue-segmentation rules)이 변경될 수 있습니다.
Myna Player는 미디어 핑거프린트 (media fingerprint)를 생성하고, 다음과 같은 입력값을 포함하는 파이프라인 키 (pipeline key)와 결합합니다:
- 선택된 오디오 스트림 (selected audio stream);
- Whisper 런타임 (runtime) 및 모델 해시 (model hash);
- 전사 언어 모드 (transcription language mode);
- VAD 모델;
- 청크 (chunk) 및 추출 파라미터 (extraction parameters);
- 큐 세그멘테이션 (cue-segmentation) 버전.
정확히 일치하는 키를 가진 체크포인트 (checkpoints)만이 복구됩니다.
이는 서로 다른 파이프라인 설정에 의해 생성되었음에도 불구하고 오래된 자막 데이터가 유효한 것처럼 보이는 미묘한 유형의 버그를 방지합니다.
캐시는 단순히 작업이 수행되었다는 사실만을 기억해서는 안 됩니다. 어떤 가설(assumptions) 하에 어떤 작업이 수행되었는지를 기억해야 합니다.
탐색(Seeking)에는 취소뿐만 아니라 세대(generations)가 필요합니다
전사가 4분 지점을 처리하는 동안 시청자가 3분에서 48분으로 탐색(seek)할 수 있습니다.
현재의 워커 (worker)는 중단되어야 하지만, 취소(cancellation)만으로는 충분하지 않습니다.
느린 프로세스는 탐색 이후에도 여전히 결과를 반환할 수 있습니다. 만약 그 결과가 맹목적으로 수용된다면, 이전 재생 영역의 오래된 큐 (stale cues)가 새로운 세션을 덮어쓰거나 오염시킬 수 있습니다.
Myna Player는 활성 영역이 변경될 때 처리 세대 (processing generation)를 증가시킵니다.
탐색 흐름은 개념적으로 다음과 같습니다:
- 활성 ASR 및 번역 연결을 취소합니다;
- 세대 (generation)를 증가시킵니다;
- 완료된 캐시 항목을 보존합니다;
- 새로 요청된 영역을 승격 (promote) 시킵니다;
- 이전 세대에서 온 늦은 결과들을 거부합니다.
세대 (generations)는 취소를 검증 가능한 계약 (contract)으로 바꿉니다.
시스템은 모든 하위 프로세스가 즉각적으로 중단되었다고 가정할 필요가 없습니다. 대신 오래된 결과가 폐기된 계획 (obsolete plan)에 속해 있음을 인식할 수 있습니다.
문맥을 포함한 오디오 추출
각 정규 윈도우 (canonical window)는 추출 과정에서 약간 확장됩니다.
Myna Player는 정규 범위 주변의 인접 오디오 문맥 (audio context)을 약 2초 정도 읽어 다음과 같이 변환합니다:
mono
16 kHz
signed 16-bit PCM
이 문맥은 경계 부근의 음성 인식을 돕지만, 저장된 출력물은 여전히 정규 시간 범위 (canonical time range)에 속합니다.
이는 스트림 처리 (stream processing)에서 흔히 볼 수 있는 패턴입니다:
- 더 나은 로컬 결정을 위해 중첩되는 입력(overlapping input)을 읽습니다.
- 결정론적 저장(deterministic storage)을 위해 중첩되지 않는 정규 출력(non-overlapping canonical output)을 작성합니다.
경계 문맥(boundary context)이 없으면, 윈도우(window)에 걸쳐 분할된 단어들이 유실되거나 중복될 수 있습니다. 정규 출력 경계(canonical output boundaries)가 없으면, 중첩된 윈도우가 서로 충돌하는 큐(cue)를 생성할 수 있습니다.
장기 실행되는 로컬 Whisper 워커 (long-lived local Whisper worker)
매 30초 세그먼트마다 음성 인식 프로세스를 시작하는 것은 상당한 오버헤드(overhead)를 발생시킵니다.
Myna Player는 장기 실행되는 로컬 whisper.cpp 워커를 사용합니다.
이 워커는 다음과 같은 역할을 수행합니다:
- 임의의
127.0.0.1포트에만 바인딩(bind)합니다. - 선택된 모델을 한 번만 로드합니다.
- 추출된 오디오 윈도우를 수락합니다.
- Silero 음성 활동 감지 (Voice Activity Detection, VAD)를 적용할 수 있습니다.
- 단어 단위 타임스탬프(word-level timestamps)를 반환합니다.
- 취소되거나 애플리케이션이 종료될 때 종료됩니다.
언어 모델(language model)이 자막 타이밍을 임의로 만들어내서는 안 되기 때문에, 단어 타임스탬프는 필수적입니다.
전사(transcription) 레이어는 시간이 지정된 단어들을 생성합니다. 그런 다음 결정론적 세그먼테이션(deterministic segmentation) 단계가 해당 단어들을 읽기 쉽고 중첩되지 않는 자막 큐(subtitle cues)로 변환합니다.
이러한 분리를 통해, 번역 제공자에게 큐가 언제 나타나야 하는지를 결정하도록 요청하지 않고도 시스템이 시각적 자막 그룹화(visual subtitle grouping)를 개선할 수 있습니다.
음성 활동 감지(VAD)를 통한 불필요한 작업 감소
30초 길이의 미디어 윈도우에는 음악, 침묵 또는 음성이 아닌 소리가 포함될 수 있습니다.
선택 사항인 Silero VAD는 전사 전에 음성 영역을 식별합니다.
이를 통해 불필요한 추론(inference)을 줄이고, 침묵이나 배경 오디오로부터 불안정한 텍스트가 생성되는 것을 방지할 수 있습니다.
VAD는 파이프라인 지문(pipeline fingerprint)의 일부입니다. 음성 영역 감지기를 변경하면 결과적인 전사 내용과 큐 경계가 변경될 수 있기 때문입니다.
다시 한번 강조하지만, 캐시 키(cache key)는 소스 파일뿐만 아니라 실제 계산 과정을 나타내야 합니다.
소스 큐의 트랜잭션 기반 영속화 (Persisting source cues transactionally)
소스 윈도우가 전사 및 세그먼테이션된 후, Myna Player는 동일한 SQLite 트랜잭션 내에서 소스 큐를 저장하고 체크포인트(checkpoint)를 완료 상태로 표시합니다.
이는 다음과 같은 실패 상태를 방지합니다:
- 체크포인트는 완료되었다고 표시되었으나,
- 자막 행(subtitle rows)은 일부만 작성된 경우.
또는 그 반대의 경우:
- 자막 행(subtitle rows)은 존재하지만;
- 체크포인트는 여전히 미완료 상태로 표시되어;
- 애플리케이션이 작업을 반복하여 중복을 생성하는 경우.
저장 계층(storage layer)은 WAL 모드와 버전 관리된 마이그레이션(versioned migrations)을 사용하는 SQLite를 사용합니다.
데이터베이스에는 다음과 같은 로컬 정보가 포함됩니다:
- 미디어 지문 (media fingerprints);
- 재생 위치 (playback position);
- 처리 체크포인트 (processing checkpoints);
- 소스 전사 세그먼트 (source transcript segments);
- 제공자별 번역 (provider-specific translations);
- 모델 메타데이터 (model metadata);
- 사용자 수정 사항 (user corrections).
지속성 저장소(Persistent storage)는 룩어헤드 파이프라인(look-ahead pipeline)을 일시적인 라이브 효과에서 재개 가능한 제품 동작으로 전환하는 핵심 요소입니다.
전사와 번역은 별도의 워커(worker)입니다
번역이 소스 전사(source transcription)를 차단해서는 안 됩니다.
Myna Player는 ASR(자동 음성 인식)과 클라우드 번역을 독립적인 스케줄링 레인(scheduling lanes)으로 실행합니다.
소스 파이프라인이 시간 정보가 포함된 큐(timed cues)를 계속 준비하는 동안, 번역 워커는 완료된 큐의 제한된 그룹을 처리할 수 있습니다.
이는 클라우드 제공업체의 지연 시간(latency)이 가변적이기 때문에 중요합니다. 느린 번역 응답이 애플리케이션의 소스 언어 캐시(source-language cache) 구축을 중단시켜서는 안 됩니다.
또한 이는 사용자가 미디어를 다시 전사할 필요 없이 번역 제공자나 대상 언어를 전환할 수 있음을 의미합니다.
소스 텍스트와 제공자별 번역은 별도로 저장됩니다.
구조는 개념적으로 다음과 같습니다:
source cue
├── DeepL / French
├── Gemini / Turkish
...
소스 타이밍(source timing)은 공유된 기초로 유지됩니다.
번역 계약: 문맥은 단어를 바꿀 수 있지만, 시간은 바꿀 수 없습니다
이것이 번역 계층의 핵심 규칙입니다.
번역 제공자는 다음을 전달받습니다:
- 확정된 큐의 제한된 배치 (bounded batch of finalized cues);
- 안정적인 큐 ID (stable cue IDs);
- 제한된 이전 대화 문맥 (limited previous-dialogue context);
- 요청된 대상 언어;
- 요청된 모든 큐를 정확히 한 번씩 반환하라는 지침.
제공자는 문맥을 사용하여 어휘를 개선할 수 있습니다.
하지만 다음과 같은 행위는 할 수 없습니다:
- 큐 ID 병합;
- 하나의 큐를 새로운 ID로 분할;
- 큐 순서 변경;
- 큐 누락;
- 큐 중복;
- 타임스탬프 임의 생성;
- 소스 타이밍 변경.
응답은 저장되기 전에 검증됩니다.
누락되거나, 중복되거나, 알 수 없거나, 순서가 바뀐 ID는 거부됩니다.
이를 통해 유용한 책임 분리가 이루어집니다:
| 계층 (Layer) | 책임 (Responsibility) |
|---|---|
| ASR | 단어 및 타임스탬프 인식 |
| ... |
언어 모델 (Language model)은 언어를 처리합니다. 결정론적 코드 (Deterministic code)는 시간을 처리합니다.
주변 문맥이 여전히 중요한 이유
자막 큐 (Subtitle cue)는 종종 단독으로 정확하게 번역하기에는 너무 작습니다.
대명사, 격식 (Formality), 동사 시제, 성별, 관용구 및 모호한 단어들은 이전 대화에 의존할 수 있습니다.
모든 요청마다 영화 전체를 보내는 것은 비용이 많이 들고, 느리며, 불필요합니다.
Myna Player는 활성 배치 (Active batch)와 함께 제한된 범위의 이전 대화 문맥을 함께 보냅니다.
문맥적 텍스트는 제공자 (Provider)가 현재 줄을 해석하는 데 도움을 주지만, 요청된 큐 ID (Cue IDs)만이 출력으로 수락됩니다.
이는 **이해를 위한 문맥 (Context for understanding)**과 **변형을 위한 범위 (Scope for mutation)**를 분리합니다.
제공자는 다시 쓸 수 있도록 허용된 것보다 더 많은 내용을 읽을 수 있습니다.
이는 자막 이외에도 유용한 패턴입니다. AI 시스템은 좁고 검증된 출력 경계가 필요하면서도, 동시에 넓은 문맥으로부터 이득을 얻는 경우가 많습니다.
소스 편집은 번역을 무효화합니다
사용자는 자막 텍스트와 타이밍을 수정할 수 있습니다.
큐의 소스 텍스트가 변경되면, 이전 텍스트에서 파생된 모든 제공자 번역은 오래된 것 (Stale)이 됩니다.
Myna Player는 이를 조용히 계속 표시하는 대신 해당 번역들을 무효화 (Invalidate)합니다.
따라서 소스 데이터와 번역 데이터는 명시적인 의존 관계를 가집니다.
수정은 단순한 시각적 편집이 아닙니다. 이는 다운스트림 언어 처리 (Downstream language processing)로 들어가는 입력을 변경하는 것입니다.
Tauri Channels를 통한 증분 업데이트
UI는 워커 (Worker)가 창(Window)을 마칠 때마다 전체 자막 세트를 다시 로드하지 않습니다.
백엔드는 다음을 전송합니다:
- 하나의 전체 초기 스냅샷 (Initial snapshot);
- 순서가 지정된 증분 큐 패치 (Incremental cue patches).
이러한 업데이트는 Tauri Channels를 통해 전달됩니다.
소스 큐, 번역, 수정 및 무효화가 서로 다른 워커로부터 도착할 수 있기 때문에 순서가 중요합니다.
프론트엔드는 순서가 정해지지 않은 이벤트로부터 상태를 재구성하려고 시도하는 대신, 이미 알려진 패치(patches) 시퀀스를 적용합니다.
이를 통해 네이티브 백엔드(native backend)가 영속성(persistence)과 파이프라인 상태(pipeline state)를 관리하는 동안, 웹뷰(WebView)는 프레젠테이션(presentation)에만 집중할 수 있습니다.
네이티브 플레이어 클록(native player clock)을 기준으로 하는 렌더링
자막 렌더러(subtitle renderer)는 Whisper나 번역 API를 호출하지 않습니다.
대신 이미 캐시된 큐(cues)를 읽어 들여, 약 100ms 간격으로 업데이트되는 플레이어 클록(player clock)과 비교합니다.
큐 조회(Cue lookup) 시에는 매 틱(tick)마다 모든 자막을 스캔하는 대신 이진 탐색(binary search)을 사용합니다.
이는 다음과 같은 엄격한 런타임 규칙을 제공합니다:
백그라운드 AI 작업이 느리거나, 오프라인 상태이거나, 실패하더라도 재생 렌더링은 반드시 결정론적(deterministic)으로 유지되어야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기