Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Speakify

내 목소리로 드라마 명장면을 더빙하는 서비스. 프로메테우스 5팀 데모데이 부스용 (아이패드 기준).

관람객이 30초쯤 스크립트를 읽으면, 그 목소리로 드라마 주연의 대사를 다시 부르고 BGM 은 원본 그대로 남긴 영상을 돌려준다. 변환에 수 분이 걸리므로, 기다리는 동안 딥페이크 목소리 퀴즈를 풀게 해서 그 시간을 채운다.

랜딩 → 명장면 선택 → 목소리 녹음 → (변환 대기 중 딥페이크 퀴즈) → 결과 영상 + 저장 QR

미디어는 이 저장소에 없다. 데모에 쓴 드라마 영상과 연예인 음성은 저작물이고, 퀴즈용 합성 음성은 실존 인물의 목소리를 합성한 것이라 공개 배포하지 않는다. 코드와 문서, 그리고 화면 확인용 명장면 썸네일만 담겨 있다. 직접 돌려보려면 본인 영상을 넣어야 한다.


저장소 구조

프론트엔드와 백엔드를 따로 개발하다가 한 저장소로 합쳤다. 양쪽 커밋 기록이 그대로 남아 있다.

speakify/
├── frontend/           React + TypeScript + Vite. 아이패드에서 도는 화면 전부
├── backend/            FastAPI. 프론트가 호출하는 API (포트 8010)
├── cosyvoice-server/   CosyVoice 음성변환 서버 + 명장면 전처리 스크립트 (포트 9010)
├── tools/quiz-builder/ 딥페이크 퀴즈 음성을 만드는 스크립트
└── docs/RUN.md         ⭐ 실행 방법은 여기 (터미널 3개를 순서대로 띄운다)

CosyVoice 모델 체크아웃(5.5GB)만은 저장소에 넣을 수 없어서 저장소 바깥, 부모 폴더에 나란히 둔다. 자세한 건 docs/RUN.md.

TTS/
├── speakify/     ← 이 저장소
└── CosyVoice/    ← github.com/FunAudioLLM/CosyVoice (별도 설치)

어떻게 동작하나

서버가 두 개다. 하나로 합치지 않은 이유는 파이썬 버전이 다르기 때문이다 — CosyVoice 는 Python 3.10 / torch 2.3.1 에 묶여 있고, 여기서 버전을 올리면 변환이 통째로 깨진다. 그래서 API 서버(3.11)를 따로 두고 HTTP 로 부른다.

아이패드 Safari
      │  https, /api/* (Vite 프록시)
      ▼
frontend           React. 녹음(MediaRecorder) · 폴링 · 퀴즈 · 결과 재생
      │  POST /convert  →  job_id  →  2초마다 GET /convert/{job_id}
      ▼
backend  :8010     FastAPI. 작업 접수 · 진행상황 관리 · 결과 영상 중계
      │  POST /vc  →  vc_job_id  →  GET /vc/{vc_job_id}
      ▼
cosyvoice-server :9010
      │            CosyVoice2-0.5B 를 메모리에 올려두고 재사용
      ▼
      완성 영상 (mp4)

변환이 실제로 하는 일

명장면을 통째로 모델에 넣지 않는다. CosyVoice 가 30초 넘는 오디오를 못 다루기 때문이다(51초짜리 신데렐라 언니는 그냥 실패한다). 그래서 미리 쪼개둔다.

전처리 (최초 1회, 오래 걸림 — cosyvoice-server/)

순서 스크립트 하는 일
1 preprocessing.py 오디오 추출 → Demucs 로 BGM / 목소리 분리 (장면당 3~5분)
2 diarize_simple.py 목소리를 발화 단위로 쪼개고 화자 2명으로 나눔
3 make_label_page.py 자동 판별이 안 된 장면을 귀로 듣고 지정 (화자확인.html)
4 refine_boundaries.py 잘려나간 말끝 되살리기 — 반드시 마지막에

결과는 scene-output/scene_N/manifest.json + 발화별 wav 로 남는다.

실행 시 (요청마다)

  1. 녹음 파일을 16kHz 모노 WAV 로 변환 (브라우저는 WebM/Opus 로 녹음하는데 CosyVoice 가 못 읽는다)
  2. manifest 의 주연 배우 구간만 관람객 목소리로 재합성. 상대 배우 구간은 원본 그대로 통과
  3. 분리해둔 BGM 트랙과 다시 합치고, 원본 영상에 얹어서 mp4 로 렌더

상대 배우 구간을 건너뛰는 건 자연스러움 때문만이 아니라 변환할 오디오가 줄어서 그만큼 빨라지기 때문이다.

딥페이크 퀴즈

연예인 5명 × (진짜 목소리 1개 + AI 합성 4개) = 5라운드. 진짜를 골라내면 정답.

원본을 그대로 쓰면 듣지 않고도 정답을 맞힐 수 있었다 — 진짜 목소리만 코덱이 다르고(AAC 44.1kHz vs PCM 24kHz), 음량이 38 LUFS 더 크고, 파일명에 GT 가 박혀 있었다. tools/quiz-builder/build_quiz.py 가 전부 24kHz 모노 / MP3 128k / -20 LUFS 로 통일하고 파일명을 c1c5 로 익명화한다.

정답표는 실행할 때마다 새로 섞이므로, 다시 만들면 frontend/src/api/mock.tsMOCK_QUIZ_ANSWERSbackend/app/routers/quiz.py_ANSWERS 를 함께 고쳐야 한다. 자세한 건 tools/quiz-builder/README.md.


이 저장소에 없는 것

데모에 실제로 쓴 미디어는 전부 빼두었다. 저작물이고, 퀴즈 음성은 실존 인물의 목소리를 합성한 것이라 배포하지 않는다.

없는 것 원래 위치 직접 채우려면
드라마 원본 클립 5편 cosyvoice-server/scene-input/scene_N.mp4 아무 영상이나 넣으면 된다. 규격은 scene-input/README.md
전처리 산출물 cosyvoice-server/scene-output/ 위 영상으로 preprocessing.py 부터 돌리면 생긴다
퀴즈 음성·영상·얼굴 사진 frontend/public/quiz/ tools/quiz-builder/README.md
화자 확인 페이지 cosyvoice-server/화자확인.html make_label_page.py 가 만든다 (구간 오디오가 박혀 있어 제외)

명장면 카드 썸네일(frontend/public/scenes/*.jpg)만은 화면이 깨지지 않도록 남겨두었다.

내 영상으로 돌려보기

  1. 16:9 / 1280x720 mp4 를 cosyvoice-server/scene-input/scene_1.mp4 로 넣는다 (파일 이름이 곧 scene_id 다)
  2. preprocessing.pydiarize_simple.pymake_label_page.pyrefine_boundaries.py 순서로 전처리한다. 자세한 건 docs/RUN.md
  3. scene-output/scene_1/manifest.json"target_speaker" 를 직접 넣는다. 이 필드가 없으면 engine.py 가 에러를 낸다
  4. backend/app/routers/scenes.pySCENES 목록을 내 영상에 맞게 고친다

전처리가 만드는 manifest.json 은 이런 모양이다. 변환은 이 파일만 보고 돈다.

{
  "scene_name": "scene_1",
  "source_video": ".../scene-input/scene_1.mp4",
  "duration_sec": 29.1,
  "vocals_16k": ".../vocals_16k.wav",
  "bgm_16k": ".../bgm_16k.wav",
  "speakers": ["TARGET", "OTHER"],
  "target_speaker": "TARGET",
  "segments": [
    { "speaker": "TARGET", "start": 2.16, "end": 3.24,
      "source_audio": ".../segments/0000_TARGET.wav" }
  ]
}

target_speaker 와 같은 speaker 를 가진 구간만 사용자 목소리로 바뀌고, 나머지는 원본 그대로 나간다. 절대경로로 저장되지만 engine.rehome_path 가 실행하는 컴퓨터 기준으로 다시 맞춰준다.

퀴즈 음성이 없을 때

선택지를 눌러도 소리가 안 나고 얼굴 사진 자리가 빈다. 채점과 화면 전환은 정상 동작하므로 변환 흐름 자체는 끝까지 돌아간다.

퀴즈 화면을 그냥 건너뛰려 하면 안 된다. QuizScreen 이 변환 작업 폴링도 함께 맡고 있어서(onDone(url) 으로 결과 주소를 넘긴다), 이 단계를 빼면 결과 화면이 영상을 못 받는다. 빼려면 폴링(usePolling)을 옮겨 붙여야 한다.


실행

전체 절차는 docs/RUN.md 에 있다. 순서가 중요하다 — CosyVoice 가 모델을 다 올린 뒤에 변환 요청이 들어가야 한다. 터미널 3개를 이 순서로 띄운다.

# A. CosyVoice 서버 (제일 먼저, 모델 로딩에 1~2분)
cd cosyvoice-server
../../CosyVoice/.venv/bin/python -m uvicorn server:app --host 127.0.0.1 --port 9010

# B. 백엔드 API
cd backend
COSYVOICE_URL=http://127.0.0.1:9010 .venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8010

# C. 프론트엔드 (데모 당일에는 이것만)
cd frontend
./demo-start.sh

COSYVOICE_URL 을 빼먹으면 기본값 9000 으로 붙는데 그 포트는 다른 서비스라 변환이 이상하게 실패한다. 반드시 붙일 것.

포트

이 맥은 8000·9000 이 이미 점유돼 있어서 8010(백엔드) / 9010(CosyVoice) / 5199(프론트) 를 쓴다.

백엔드 없이 화면만 보기

cd frontend
npm install
cp .env.example .env      # VITE_USE_MOCK=true 로 되어 있다
NO_SSL=1 npm run dev      # 맥에서 localhost 로만 볼 때

목업 분기가 src/api/ 레이어에만 있어서 화면 코드는 그대로 돈다.


API

기본 주소 http://<백엔드-호스트>:8010. CORS 는 * 로 열려 있다.

메서드 경로 설명
GET /health 살아있는지 확인
GET /scenes 명장면 5개 (id · 작품 · 배우 · 썸네일 · 길이)
POST /convert 변환 시작 (multipart: audio, scene_id) → 202 { job_id }
GET /convert/{job_id} 폴링. pending → processing → done / failed, progress 0→50→100
GET /videos/{filename} 결과 영상 중계. ?download=1 이면 첨부파일로
GET /quiz 퀴즈 5라운드 (선택지 5개 + 얼굴 사진)
POST /quiz/{round_id}/answer 채점. 시도 2번까지, final: true 면 정답 공개

응답 형태는 frontend/SETUP.md 에 예시까지 적혀 있다.

결과 영상을 백엔드가 중계하는 이유

CosyVoice 가 만든 영상은 http://127.0.0.1:9010/videos/*.mp4 에 생기지만 이 주소를 프론트에 그대로 내려주지 않는다. 두 가지 이유다.

  • 아이패드에서 127.0.0.1아이패드 자기 자신을 가리킨다. 맥의 CosyVoice 에 닿지 않는다.
  • 프론트가 https 로 뜨면 http 영상은 mixed-content 로 차단된다.

/api/videos/<파일명> 으로 중계하면 영상이 프론트와 같은 오리진에서 오므로 둘 다 해결된다. <video> 탐색(seek)을 위해 Range 요청도 그대로 전달한다.


알아둘 것

  • 녹음은 https 여야 한다. iOS Safari 는 https 가 아니면 마이크(getUserMedia)를 안 열어준다. 게다가 자체서명 인증서에 박힌 IP 와 접속 주소가 다르면 화면은 뜨는데 마이크만 막힌다 — 원인을 찾기 어렵다. 맥의 IP 는 와이파이가 바뀔 때마다 바뀌므로 장소를 옮길 때마다 인증서를 다시 만들어야 한다. frontend/demo-start.sh 가 알아서 한다.
  • torch 는 2.3.1 이어야 한다. pip install pyannote.audio 를 실행하면 torch 가 2.13 으로 강제 업그레이드되면서 변환이 통째로 깨진다. 화자분리는 pyannote 없이 diarize_simple.py 가 처리하므로 설치할 필요가 없다.
  • 작업 기록이 메모리에만 있다 (JOBS = {}). 백엔드를 재시작하면 진행 중이던 작업이 전부 날아간다. 데모 중엔 재시작하지 말 것.
  • 변환이 느리다. CUDA 없는 맥이라 29초 장면 하나에 수 분 걸린다. 퀴즈로 가리는 구조이긴 하지만 긴 장면(51초)은 더 걸린다.
  • 녹음 포맷 수정은 브라우저로 확인할 것. curl 로 wav 를 올려 테스트하면 WebM/Opus 문제가 안 드러난다.

막히는 곳별 원인은 docs/RUN.md 의 "자주 막히는 곳" 표에 정리돼 있다.


기술 스택

프론트 React 18 · TypeScript 5.6 · Vite 6 · CSS Modules (런타임 의존성은 react / react-dom / qrcode.react 뿐)
백엔드 Python 3.11 · FastAPI · httpx · pytest
음성변환 Python 3.10 · CosyVoice2-0.5B · torch 2.3.1 · Demucs · librosa / scikit-learn
영상 ffmpeg

문서

문서 내용
docs/RUN.md ⭐ 설치부터 실행까지 전 과정, 전처리 절차, 트러블슈팅
frontend/README.md 프론트 구조 · 개발용 딥링크 · 미디어 파일
frontend/SETUP.md 스택 선택 이유와 API 명세 (초기 설계 기록)
frontend/NEXT.md 백엔드 연결 체크리스트 (초기 설계 기록)
tools/quiz-builder/README.md 퀴즈 음성 생성 · 정규화가 필요했던 이유
cosyvoice-server/scene-input/README.md 명장면 원본 클립과 썸네일

About

내 목소리로 드라마 명장면을 더빙하는 Speakify — React + FastAPI + CosyVoice (프로메테우스 5팀)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages