본문 바로가기
C.W.K.
Stream
Lesson 05 of 06 · published

목소리를 남기기로 한 날

~14 min · voice-chat, retention, recording, privacy, audio

Level 0불씨 없음
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"기본으로 잊는다는 규칙엔 이유가 있었어. 한 표면에서 그 이유가 바뀌니까 규칙도 거기서, 딱 거기서만 바뀌었어."

절대 안 남기다가, 늘 남기기로

2026-09-26 에 보이스 챗이 나왔을 때 받아쓰기 규칙을 그대로 물려받았어. 오디오는 버린다. 여드레 뒤에 cwkPippa 가 대화 기록을 훑어보니 받아쓴 Firekeeper 턴 55개 중에 녹음이 붙은 게 0개였어. Firekeeper 가 그렇게 설계됐으니까. 문제는 새 목표가 생겼다는 거야. 어떤 대화든 나중에 영상이 될 수 있어야 한다. 따로 녹화를 안 했어도. 그러려면 그 대화의 진짜 기준, 그러니까 아빠의 실제 목소리가 살아남아야 해. 아빠는 같은 날 Firekeeper 랑 내비 앱 둘 다에 판정을 내렸어. "둘 다 목소리 남겨." 왜 16 kHz 로 남기냐는 데엔 이렇게 되물었고. "48khz 로 남겨서 문제가 안 되는데 왜 16으로 남기냐."

이제 턴 하나가 남기는 것

발화가 끝나면 Firekeeper 는 파일 두 개를 챙겨. 전사기가 들은 16 kHz 녹음, 그리고 보이스 키트의 테이프에서 같은 구간을 마이크 원래 샘플레이트로 뜬 사본. 둘 다 cwkPippa 의 /api/upload 로 올라가고, 턴은 input_audio_ids 로 그 둘의 이름을 불러. 턴에 붙는 기록이지, 모델이 읽는 게 아니야. cwkPippa 는 샘플레이트로 둘을 구별해. 서버가 잠깐 자리를 비우면 듣기 허가와 같은 규칙으로 다시 올려 보고, 그 밖의 이유로 실패하면 "The recording was not kept." 라는 메모를 남기고 말은 그대로 나가. Mac 엔 아무것도 안 남아. 받아쓰기는 하나도 안 바뀌었어. 받아쓰기 오디오는 파일로 나가지 않고, 전사가 끝나면 버려져. 트랙 8 에서 말한 제한된 실패 구조 하나만 빼고. 최대 열 개, 그것도 히스토리가 켜져 있을 때만.

발화 끝
  -> voice-<stamp>.wav       16 kHz: 전사기가 들은 것 (재생 타이밍, 음량)
  -> voice-hq-<stamp>.wav    마이크 원래 레이트, 보이스 프로세싱 거침 (MacBook 마이크, AirPods)
     voice-raw-<stamp>.wav   마이크 원래 레이트, 마이크 날소리 (오피스)
  -> POST /api/upload  (비공개 대화는 자기 폴더 이름을 실어)
  -> turn.input_audio_ids = [ids]

이 Mac 엔: 아무것도 없음.   받아쓰기: 그대로 (버림; 실패 구조만 예외).

오피스에선 마이크 날소리

대화로 만든 첫 영상이 전화 통화처럼 들렸어. 아빠는 더 나은 소리를 원했는데 조건이 하나 붙었지. 그걸 고치느라 끼어들기를 건드릴 거면 고치지 말라고. WebUI 는 깨끗한 사본을 얻으려고 기기를 처리 안 한 채로 한 번 더 열어야 했어. 거긴 대화 스트림이 브라우저 에코 제거를 거치니까. Firekeeper 는 새로 할 게 없었어. 오피스의 32입력 인터페이스는 보이스 프로세싱을 아예 안 돌리니까, 대화 채널 자체가 이미 48 kHz 마이크 날소리였던 거야. 앱 자체 경로 안에서 재 보니 테이프가 24 kHz 까지 평평했고 방 소음은 −54.5 dBFS 쯤이었어. 그러니 캡처는 안 바뀌었고, 끼어들기는 건드릴 수조차 없었고, 바뀐 건 이름 하나뿐이야. 보이스 프로세싱 없이 뜬 사본은 voice-raw-…, 거친 사본은 voice-hq-….

사본은 듣기가 시작된 순간부터

날소리 사본은 방이 들은 걸 다 품어. 모니터에서 나온 Pippa 자기 목소리까지. 처음엔 테이프 끝에서 '발화 길이 더하기 반 초' 만큼 잘라 냈는데, 그 반 초가 듣기 시작 전으로 거슬러 올라갔어. 그날 저녁 WebUI 날소리 사본 하나는 말소리만큼 큰 Pippa 목소리 0.4초로 시작했어. 지금은 이번 듣기의 시작 시각을 알면, 사본 길이가 그 녹음이 시작된 뒤로 흐른 시간을 못 넘어. 그러니 Pippa 답장 속으로 거슬러 올라갈 수가 없지. 시작 시각이 없으면 함수는 예전의 '길이 더하기 반 초' 로 돌아가. 그날 밤 스피커 시험에선 사본마다 16 kHz 짝이랑 상관계수 0.99–1.00 으로, 시작점 45 ms 안에서 맞았어.

보존 규칙은 여전히 서 있어

트랙 8 은 보존이 이유가 붙은 예외여야 하고 범위가 있어야 한다고 했지. 이게 딱 그거야. 밝힌 이유(대화는 영상이 될 수 있어야 함), 정해진 범위(대화는 남기고, 받아쓰기는 제한된 실패 구조 말고는 안 남기고, 대화는 Mac 엔 아무것도 안 남김), 그리고 저장소 하나(cwkPippa. WebUI 랑 폰이 쓰는 바로 그 이음새라서 기록 점검이 Firekeeper 대화를 새 코드 없이 읽어). 비공개 대화의 녹음은 그 대화 자기 폴더에 들어가고, 대화가 끝나면 같이 끝나.

프라이버시 기본값은 표면마다, 이유를 붙여서 바꿔. '전부 잊기' 는 입력기한텐 맞는 규칙이었어. 영상이 될 수도 있는 대화는 목적이 다르니까 자기 기준을 남겨. 어느 표면도 남의 규칙을 빌리지 않아. 저마다 자기 규칙을 밝혀.
날소리 사본은 방 전체를 들어. 처리 안 한 녹음은 전부 듣기가 시작된 순간에 묶어 둬. 반 초만 거슬러 가는 여유로도 다른 사람 목소리가 한 사람 말이라고 붙은 파일에 섞여 들어가.

Code

턴 하나가 남기는 것에 이름 붙이기 (FirekeeperKit VoiceChatTurn)·swift
// 보이스 프로세싱 없이 뜬 사본은 마이크 날소리: "voice-raw-".
// 처리된 사본(내장 마이크, AirPods)은 "voice-hq-".
public static func recordingFiles(recording: Data?, original: Data?,
                                  originalIsRaw: Bool, stamp: Int) -> [SharedFile] {
    var files: [SharedFile] = []
    if let original, !original.isEmpty {
        let kind = originalIsRaw ? "raw" : "hq"
        files.append(SharedFile(name: "voice-\(kind)-\(stamp).wav",
                                contentType: "audio/wav", data: original))
    }
    if let recording, !recording.isEmpty {   // 16 kHz, 전사기가 들은 것
        files.append(SharedFile(name: "voice-\(stamp).wav",
                                contentType: "audio/wav", data: recording))
    }
    return files
}

// 이번 듣기의 시작을 알면 원본은 그보다 길어지지 않아;
// 모르면 예전의 길이 + 0.5초 여유로 돌아가.
public static func originalSeconds(recordingSeconds: Double, sinceRecordingBegan: Double?) -> Double {
    guard recordingSeconds > 0 else { return 0 }
    let wanted = recordingSeconds + originalMarginSeconds        // 0.5초 여유
    guard let since = sinceRecordingBegan, since.isFinite, since > 0 else { return wanted }
    return min(wanted, since)
}

External links

Exercise

네가 지은 것에서 프라이버시 기본값 하나를 골라봐('X 는 안 남겨'). X 를 남겨야 하는 새 쓰임을 떠올려. 책임 있는 변경이라면 밝혀야 할 세 가지를 써 봐. 이유, 범위(어느 표면, 어느 저장소, 어디엔 안 남기는지), 그리고 여전히 절대 안 바뀌는 것.
Hint
망하는 모양은 표면 하나가 필요하다고 기본값을 모든 데서 바꾸는 거야. 좋은 변경은 데이터를 남기는 표면, 그게 저장되는 단 한 곳, 그리고 옛 규칙을 그대로 지키는 표면에 이름을 붙여. 여기선 대화는 남기고, 받아쓰기는 제한된 실패 구조 말고는 안 남기고, 대화는 Mac 엔 하나도 안 남겨.

Progress

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

댓글 0

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

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