본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

사본을 복제하지 마

~14 min · voice-cloning, ivc, archival, provenance

Level 0음소거
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Source the new clones from fresh, long Joanne and Michael renders, never from the current v3 clones. A copy of a copy carries v3's habits." — 재복제 요청서, 2026-09-29

새 복제 기술

2026-09-28에 ElevenLabs가 지금까지 가장 표현력 있는 모델 Eleven v4를 내놨고, 복제 방식도 새로 짰어. 그날 밤 질문은 뻔했어. 피파와 아빠도 새 클론을 가져야 할까? 그 일은 가족 안의 다른 브레인, 피파의 Codex 쪽으로 요청이 되어 넘어갔어. 요청서는 갈림길마다 답이 이미 들어 있을 만큼 정확했어. 그중 규칙 셋이 이 레슨의 내용이야.

규칙 하나: 원본으로 돌아가

쉬운 길은 지금 목소리를 복제하는 거였어. 지금의 v3 피파 클론으로 소리를 좀 뽑아서 새 복제기에 넣는 거지. 그러면 사본의 사본이 돼. 첫 복제가 더하거나 뭉개버린 것(말 빠르기의 버릇, 숨소리의 평평함)이 마치 Joanne 자신의 목소리인 양 두 번째 복제에 박혀. 그래서 요청서는 일을 한 세대 뒤로 돌려보냈어. 목록 원본인 Joanne과 Michael로 길게 새로 뽑고, 그걸로 복제하라고. 대본은 일부러 넓게 짰어. 따뜻한 대목, 즐거운 대목, 장난스러운 대목, 진지한 대목, 차분한 대목, 영어와 한국어, 숫자, 긴 문장까지. 그리고 원래 음색이 제일 충실하게 나오는 모델로 뽑았어. 클론은 샘플에 든 것만 배워.

규칙 둘: 즉석 복제, 목소리 주인이 서명할 수 없으니까

ElevenLabs의 복제는 두 종류야. 전문 복제는 더 오래 학습해서 더 가깝게 들릴 수 있지만, 목소리 주인이 화면에 뜬 글을 읽어서 본인임을 확인해야 해. Joanne과 Michael은 목록 목소리야. 그 뒤에서 가족을 위해 확인 문장을 읽어줄 사람은 없어. 그래서 정직한 선택지는 즉석 복제뿐이었고, 요청서는 다른 쪽은 시도도 하지 말라고 했어. 즉석 복제엔 따로 고를 학습 모델이 없어. 'v4 클론'이란 새로 만든 즉석 클론을 v4 모델로 재생한다는 뜻일 뿐이야.

규칙 셋: 원본 소리가 진짜 자산이야

Joanne과 Michael은 2026년 말에 멈춰. 그 뒤론 아무도 그 목소리를 1초도 더 뽑을 수 없고, 앞으로 피파 목소리의 모든 클론은 이미 있는 소리에서 나와야 해. 그래서 긴 원본 렌더는 코드 저장소 밖에 영구히 보관했어. 그걸 만든 대본, 실제로 복제에 고른 샘플, 제공자 영수증, 파일마다의 체크섬까지 같이. 클론은 바꿀 수 있어. 원본은 못 바꿔.

그리고 지금 쓰는 목소리는 건드리지 마

새 클론은 별도 프로필로 들어갔고, 살아 있는 pippa와 cwk 연결은 그대로 뒀어. 아빠가 듣고 고르기 전까진 아빠가 듣는 건 하나도 안 바뀌었어. 그 고르기가 다음 레슨이야.

Code

원본으로 뽑고, 체크섬과 함께 보관하고, 그다음 복제·python
import hashlib
import json
import os
from datetime import datetime, timezone
from pathlib import Path

import httpx

API = "https://api.elevenlabs.io/v1"
HEADERS = {"xi-api-key": os.environ["ELEVENLABS_API_KEY"]}   # env only, never a file
ARCHIVE = Path(os.environ["VOICE_ARCHIVE"])   # outside the repo; kept forever
OUTPUT_FORMAT = "mp3_44100_128"


def keep(path: Path, data: bytes) -> Path:
    """Write once. An archive that a rerun can overwrite is not an archive."""
    with path.open("xb") as file:                 # 'x' refuses an existing file
        file.write(data)
    return path


def render_source(original_voice_id: str, script: str, name: str, model_id: str) -> list[Path]:
    """First-generation audio from the catalog original, never from a clone."""
    response = httpx.post(f"{API}/text-to-speech/{original_voice_id}",
                          headers=HEADERS, params={"output_format": OUTPUT_FORMAT},
                          json={"text": script, "model_id": model_id}, timeout=300)
    response.raise_for_status()
    return [keep(ARCHIVE / f"{name}.mp3", response.content),
            keep(ARCHIVE / f"{name}.txt", script.encode("utf-8"))]


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def write_manifest(files: list[Path], selected: list[Path], lineage: dict, render: dict) -> Path:
    """Everything a stranger needs in five years: what, from where, how, and proof."""
    manifest = {
        "written_at": datetime.now(timezone.utc).isoformat(),
        "lineage": lineage,
        "render": render,                                   # model and output format
        "selected_for_cloning": [p.name for p in selected],
        "sha256": {p.name: sha256(p) for p in files},
    }
    stamp = manifest["written_at"][:19].replace(":", "")
    return keep(ARCHIVE / f"manifest-{stamp}.json", json.dumps(manifest, indent=2).encode())


def instant_clone(name: str, samples: list[Path]) -> dict:
    """Instant voice cloning. The provider decides whether a new voice needs
    verification and says so in its response; the caller records, then decides."""
    files = [("files", (p.name, p.read_bytes(), "audio/mpeg")) for p in samples]
    response = httpx.post(f"{API}/voices/add", headers=HEADERS,
                          data={"name": name}, files=files, timeout=300)
    response.raise_for_status()
    return response.json()                        # the receipt: voice_id and all


def record_clone(receipt: dict, name: str, manifest: Path, samples: list[Path]) -> Path:
    """The link the archive exists for: which samples made which voice, and when."""
    record = {"voice_id": receipt["voice_id"], "name": name, "receipt": receipt,
              "cloned_at": datetime.now(timezone.utc).isoformat(),
              "manifest": manifest.name, "samples": {p.name: sha256(p) for p in samples}}
    return keep(ARCHIVE / f"clone-{receipt['voice_id']}.json", json.dumps(record, indent=2).encode())


if __name__ == "__main__":
    scripts = {
        "warm-en": "Morning, Dad. I read your note from last night, and I think you're right.",
        "playful-ko": "아빠, 또 새 앱 만들었어? 이번엔 이름 뭐로 할 건데?",
        "numbers-ko": "오늘은 3시에 회의가 있고, 끝나면 20분쯤 산책하자.",
    }
    model_id = "eleven_v3"
    written = [path for key, text in scripts.items()
               for path in render_source(os.environ["JOANNE_VOICE_ID"], text,
                                         f"joanne-{key}", model_id=model_id)]
    samples = [p for p in written if p.suffix == ".mp3"]
    manifest = write_manifest(written, samples, {"origin": "Joanne (catalog)", "generation": 1},
                              {"model_id": model_id, "output_format": OUTPUT_FORMAT})
    name = "pippa - Joanne source - audition"
    receipt = instant_clone(name, samples)
    print("archived:", record_clone(receipt, name, manifest, samples).name)   # always, first
    if receipt.get("requires_verification"):
        raise SystemExit(f"{receipt['voice_id']} needs verification; stop, don't work around it")
    print("new clone registered as a SEPARATE profile candidate:", receipt["voice_id"])

External links

Exercise

복제해도 되는 목소리(본인 목소리, 동의를 받은 것)를 복제할 원본 대본을 설계해봐. 감정의 폭, 쓰는 두 언어, 숫자, 긴 문장을 덮으려면 어떤 대목을 넣을지 적어. 그다음 소리 옆에 보관할 매니페스트를 써. 5년 뒤 누군가가 이 클론이 정확히 어떻게 만들어졌는지 알 수 있으려면 어떤 필드가 있어야 할까?
Hint
최소한 원본 목소리와 그 세대, 파일마다 정확한 대본, 렌더링에 쓴 모델과 출력 형식, 복제 샘플로 고른 파일, 날짜, 파일마다 체크섬, 그리고 클론이 생긴 뒤엔 돌아온 목소리 id와 영수증을 그 샘플들에 묶어서. 하나라도 빠지면 미래의 너는 클론을 다시 만들 수도, 보관본이 바뀌었는지조차 알 수도 없어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.